Problem Statement
The oc-fastify-server-adapter currently hard-codes Fastify's ignoreTrailingSlash: true router option. This is needed today because OC (both the registry router and the browser client) constructs and consumes URLs that contain trailing slashes, and Fastify's native routing treats /foo and /foo/ as two distinct routes (unlike Express' default non-strict routing, which the registry historically relied on).
Enabling ignoreTrailingSlash is slightly less performant than Fastify's native routing, and it forces the adapter to deduplicate routes that only differ by a trailing slash (otherwise Fastify throws on duplicate registration). Registry operators who care about routing performance have no way to opt out, and the behavior is invisible/undocumented.
Solution
Make trailing-slash handling a first-class, documented, configurable concern:
- Expose
ignoreTrailingSlash as a configurable option on the Fastify adapter, defaulting to false (Fastify's faster native routing). Operators who want Express-compatible non-strict matching can opt in with ignoreTrailingSlash: true.
- Stop OC's own code from ever emitting trailing slashes so the
false default is safe out of the box. This spans three areas:
- the registry router (route registration + root redirect),
- the server-side URL builder (component preview URLs),
- the browser client URL construction (
oc.build).
The net user-visible outcome: the default OC + Fastify setup uses faster routing, all first-party endpoints and links keep working, and non-strict matching becomes an explicit opt-in.
User Stories
- As a registry operator using the Fastify adapter, I want trailing-slash handling to default to Fastify's faster native routing, so that my registry serves requests as efficiently as possible without extra configuration.
- As a registry operator, I want to opt in to Express-like non-strict trailing-slash matching via a documented
ignoreTrailingSlash option, so that I can preserve backwards-compatible behavior for consumers that send trailing slashes.
- As a registry operator, I want the adapter to validate the
ignoreTrailingSlash option, so that a misconfiguration fails fast with a clear error rather than silently misbehaving.
- As a registry operator, I want the option documented in the adapter README, so that I understand the performance/compatibility trade-off before changing it.
- As a component author, I want components to render correctly through the browser client regardless of the adapter's trailing-slash setting, so that my components keep working after the registry upgrades.
- As a component author, I want component preview links to work under the default configuration, so that previews are not broken by the routing change.
- As a consumer embedding
<oc-component> tags, I want hrefs generated by the client (oc.build) to resolve against the registry under the default configuration, so that server-side and client-side rendering behave identically.
- As an OC maintainer, I want OC's own routes registered without trailing slashes, so that the registry does not depend on
ignoreTrailingSlash to function.
- As an OC maintainer, I want the root-to-prefix redirect to target a real (registered) route, so that a prefixed registry's landing page works under strict routing.
- As an OC maintainer, I want route deduplication in the adapter to only run when
ignoreTrailingSlash is enabled, so that the default path stays simple and two genuinely-distinct routes are not silently dropped.
- As an OC maintainer, I want tests that pin both the default (strict) and opt-in (non-strict) behaviors, so that future changes cannot silently regress either mode.
- As a registry operator upgrading OC, I want a clear changelog/changeset entry describing the new default and how to restore the old behavior, so that I can make an informed upgrade decision.
- As an OC maintainer, I want any change to public URL shapes (e.g.
.../version/, .../~preview/) to be called out explicitly, so that downstream cache keys and stored hrefs are considered before release.
Implementation Decisions
- Fastify adapter option: Add
ignoreTrailingSlash?: boolean to the adapter's public options type, default false. Thread it into Fastify's routerOptions.ignoreTrailingSlash. Extend the adapter's option-validation guard to accept a boolean (or undefined) and reject other types.
- Adapter route dedup: The existing "deduplicate routes that differ only by a trailing slash" logic should only apply when
ignoreTrailingSlash is enabled (that is the only mode where Fastify throws on the duplicate). When disabled, both variants are allowed to register as distinct routes.
- Registry router: Normalize every route path so OC never registers a path with a trailing slash (root
/ preserved). Register the index/components routes at the normalized (no-trailing-slash) prefix and drop the now-redundant separate prefix-index registration. Change the root redirect to point at the prefix without a trailing slash so it lands on a registered route.
- Server URL builder: Component preview URL construction currently always appends a trailing slash after
~preview. Change it so preview URLs do not end in a trailing slash (parameters still appended as a query string). Confirm the non-preview component URL builder already emits no trailing slash and leave it as-is.
- Browser client (
oc.build): URL construction currently forces a trailing slash on the baseUrl, name, and version segments, producing hrefs like .../name/version/. Change it so the generated component href does not end in a trailing slash, while remaining tolerant of a baseUrl that already ends in /.
- Backwards compatibility: Because
oc.build and preview URLs are long-standing public URL shapes, treat this as a potentially breaking change to URL formatting. If strict backwards compatibility for already-emitted trailing-slash URLs is required, operators can set ignoreTrailingSlash: true; document this explicitly.
- Changesets: Add a
minor changeset for oc-fastify-server-adapter (new option + default change) and appropriate changesets for oc and oc-client-browser describing the URL normalization.
Testing Decisions
- Good tests here assert externally observable HTTP behavior (status codes, matched params,
Allow/redirect headers) and generated URL strings — not internal route bookkeeping.
- Fastify adapter (
packages/oc-fastify-server-adapter): use the existing app.inject-based unit-test seam. Cover: (a) default false treats /foo vs /foo/ as distinct (trailing-slash variant 404s); (b) opt-in true matches both variants; (c) opt-in true deduplicates a /x + /x/ double registration without throwing; (d) invalid option values are rejected. The existing integration test (registry-node.ts, which boots a real registry, including a prefixed registry) should continue to pass under the new default.
- Registry router (
packages/oc): rely on the existing acceptance/integration seam that boots a registry and issues real requests. Add/adjust coverage for a prefixed registry: index reachable at the no-slash prefix, and the root redirect landing on a registered route. Prior art: existing acceptance registry tests and the Fastify registry-node.ts integration test.
- Server URL builder (
packages/oc): extend the existing registry-domain-url-builder unit tests to pin the new preview URL shape (no trailing slash, with and without parameters) and to confirm component/info URLs are unchanged.
- Browser client (
packages/oc-client-browser): add unit coverage for oc.build asserting the generated href has no trailing slash, including when baseUrl already ends in / and when version is present/absent. Use the highest available seam (the public oc.build).
- Prefer keeping all HTTP-level assertions at the adapter/registry seam rather than introducing new lower-level seams.
Out of Scope
- Redesigning the registry route table or URL scheme beyond removing trailing slashes.
- Changing the Express adapter's behavior (Express default non-strict routing already tolerates trailing slashes).
- Adding trailing-slash normalization/redirect middleware for arbitrary user-defined routes.
- Any caching/CDN cache-key migration work for previously emitted trailing-slash URLs (call out the risk; do not implement migrations here).
Further Notes
- Context: this was discovered while reviewing an uncommitted fix that hard-coded
ignoreTrailingSlash: true plus route dedup and a trailing-slash-tolerant OPTIONS/Allow matcher in the Fastify adapter. That fix should remain as the stopgap until this issue is implemented.
- Concrete first-party trailing-slash sources identified so far: the registry router (routes built from a prefix that ends in
/, and the root redirect), the server URL builder's component preview function, and the browser client's oc.build. A broader sweep for other emitters (server-side rendering, nested component hrefs, docs/examples) should be part of implementation.
- Note on labels: the
ready-for-agent triage label referenced by the spec workflow does not exist in this repository; existing repo labels were applied instead.
Problem Statement
The
oc-fastify-server-adaptercurrently hard-codes Fastify'signoreTrailingSlash: truerouter option. This is needed today because OC (both the registry router and the browser client) constructs and consumes URLs that contain trailing slashes, and Fastify's native routing treats/fooand/foo/as two distinct routes (unlike Express' default non-strict routing, which the registry historically relied on).Enabling
ignoreTrailingSlashis slightly less performant than Fastify's native routing, and it forces the adapter to deduplicate routes that only differ by a trailing slash (otherwise Fastify throws on duplicate registration). Registry operators who care about routing performance have no way to opt out, and the behavior is invisible/undocumented.Solution
Make trailing-slash handling a first-class, documented, configurable concern:
ignoreTrailingSlashas a configurable option on the Fastify adapter, defaulting tofalse(Fastify's faster native routing). Operators who want Express-compatible non-strict matching can opt in withignoreTrailingSlash: true.falsedefault is safe out of the box. This spans three areas:oc.build).The net user-visible outcome: the default OC + Fastify setup uses faster routing, all first-party endpoints and links keep working, and non-strict matching becomes an explicit opt-in.
User Stories
ignoreTrailingSlashoption, so that I can preserve backwards-compatible behavior for consumers that send trailing slashes.ignoreTrailingSlashoption, so that a misconfiguration fails fast with a clear error rather than silently misbehaving.<oc-component>tags, I want hrefs generated by the client (oc.build) to resolve against the registry under the default configuration, so that server-side and client-side rendering behave identically.ignoreTrailingSlashto function.ignoreTrailingSlashis enabled, so that the default path stays simple and two genuinely-distinct routes are not silently dropped..../version/,.../~preview/) to be called out explicitly, so that downstream cache keys and stored hrefs are considered before release.Implementation Decisions
ignoreTrailingSlash?: booleanto the adapter's public options type, defaultfalse. Thread it into Fastify'srouterOptions.ignoreTrailingSlash. Extend the adapter's option-validation guard to accept a boolean (or undefined) and reject other types.ignoreTrailingSlashis enabled (that is the only mode where Fastify throws on the duplicate). When disabled, both variants are allowed to register as distinct routes./preserved). Register the index/components routes at the normalized (no-trailing-slash) prefix and drop the now-redundant separate prefix-index registration. Change the root redirect to point at the prefix without a trailing slash so it lands on a registered route.~preview. Change it so preview URLs do not end in a trailing slash (parameters still appended as a query string). Confirm the non-preview component URL builder already emits no trailing slash and leave it as-is.oc.build): URL construction currently forces a trailing slash on the baseUrl, name, and version segments, producing hrefs like.../name/version/. Change it so the generated component href does not end in a trailing slash, while remaining tolerant of a baseUrl that already ends in/.oc.buildand preview URLs are long-standing public URL shapes, treat this as a potentially breaking change to URL formatting. If strict backwards compatibility for already-emitted trailing-slash URLs is required, operators can setignoreTrailingSlash: true; document this explicitly.minorchangeset foroc-fastify-server-adapter(new option + default change) and appropriate changesets forocandoc-client-browserdescribing the URL normalization.Testing Decisions
Allow/redirect headers) and generated URL strings — not internal route bookkeeping.packages/oc-fastify-server-adapter): use the existingapp.inject-based unit-test seam. Cover: (a) defaultfalsetreats/foovs/foo/as distinct (trailing-slash variant 404s); (b) opt-intruematches both variants; (c) opt-intruededuplicates a/x+/x/double registration without throwing; (d) invalid option values are rejected. The existing integration test (registry-node.ts, which boots a real registry, including a prefixed registry) should continue to pass under the new default.packages/oc): rely on the existing acceptance/integration seam that boots a registry and issues real requests. Add/adjust coverage for a prefixed registry: index reachable at the no-slash prefix, and the root redirect landing on a registered route. Prior art: existing acceptance registry tests and the Fastifyregistry-node.tsintegration test.packages/oc): extend the existingregistry-domain-url-builderunit tests to pin the new preview URL shape (no trailing slash, with and without parameters) and to confirm component/info URLs are unchanged.packages/oc-client-browser): add unit coverage foroc.buildasserting the generated href has no trailing slash, including when baseUrl already ends in/and when version is present/absent. Use the highest available seam (the publicoc.build).Out of Scope
Further Notes
ignoreTrailingSlash: trueplus route dedup and a trailing-slash-tolerantOPTIONS/Allowmatcher in the Fastify adapter. That fix should remain as the stopgap until this issue is implemented./, and the root redirect), the server URL builder's component preview function, and the browser client'soc.build. A broader sweep for other emitters (server-side rendering, nested component hrefs, docs/examples) should be part of implementation.ready-for-agenttriage label referenced by the spec workflow does not exist in this repository; existing repo labels were applied instead.