@ultimat3/render 27.6.1 → 27.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -588,7 +588,9 @@ a job boundary the class is gone and the `code` is what survives — match on th
588
588
  `DEFAULT_ISR_REGENERATE_DEADLINE_MS` (30 s; `isrController({ regenerateDeadlineMs })`),
589
589
  so a render that never settles no longer pins the page for the life of the process. A stored
590
590
  path takes its TTL and tags from the most specific matching route (static > `:param` >
591
- `*catch-all`, per segment), never the first in table order.
591
+ `*catch-all`, per segment), never the first in table order. The route is matched on the path
592
+ with its query and its routed locale prefix removed (`unlocalizedPath`), as the router matches
593
+ it — the key keeps both, so `/en/blog/a` is its own entry under `/blog/:slug`'s TTL and tags.
592
594
  - **A tag-revalidated ISR document carries its purge keys while it is shared-cacheable**,
593
595
  `As of 2026-10-02` — `@ultimat3/http`'s `cache-headers` stage rewrites the response to `private`
594
596
  for a signed-in visitor and strips both headers: `Surrogate-Key`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/render",
3
- "version": "27.6.1",
3
+ "version": "27.7.0",
4
4
  "description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,11 +40,11 @@
40
40
  "test": "bun test"
41
41
  },
42
42
  "dependencies": {
43
- "@ultimat3/cache": "27.6.1",
44
- "@ultimat3/core": "27.6.1",
45
- "@ultimat3/http": "27.6.1",
46
- "@ultimat3/i18n": "27.6.1",
47
- "@ultimat3/seo": "27.6.1",
43
+ "@ultimat3/cache": "27.7.0",
44
+ "@ultimat3/core": "27.7.0",
45
+ "@ultimat3/http": "27.7.0",
46
+ "@ultimat3/i18n": "27.7.0",
47
+ "@ultimat3/seo": "27.7.0",
48
48
  "sass": "1.105.1"
49
49
  }
50
50
  }
@@ -12,6 +12,8 @@ export interface IsrEntry {
12
12
  * The store key: the request's pathname AND its query, params sorted. Not the route's pattern
13
13
  * and not the bare pathname — `/blog?page=2` and `/blog?page=3` render different documents, and
14
14
  * keying both as `/blog` served the second visitor the first one's HTML (#171).
15
+ * A routed locale prefix stays on it (`/en/blog`): the controller removes it only to find the
16
+ * route, so a store may never use this as a route path.
15
17
  */
16
18
  readonly path: string;
17
19
  readonly html: string;
package/src/render-isr.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  } from '@ultimat3/cache';
19
19
  import type { Scheduler } from '@ultimat3/core';
20
20
  import { finiteCount, logger, renderThrowable, singleFlight } from '@ultimat3/core';
21
+ import { unlocalizedPath } from '@ultimat3/i18n';
21
22
  import { parseTtlMs } from './duration';
22
23
  import { finiteStatus, isRenderStatus } from './finite-status';
23
24
  import type { RouteDescriptor } from './registry';
@@ -64,15 +65,22 @@ export function isrKey(url: URL, locale: string): string {
64
65
  }
65
66
 
66
67
  /**
67
- * The route pattern a key belongs to. Every lookup that asks the ROUTE TABLE a question — the
68
- * descriptor, and therefore the TTL — has to strip the query first: `descriptorFor('/blog?page=2')`
69
- * matches no route, so the entry would silently fall back to `ttlMs: null` and a declared
70
- * `revalidate: { ttl: '5m' }` would become tag-only. Keying without this split is a half-fix that
71
- * trades a leak for a wrong TTL.
68
+ * The path the ROUTE TABLE knows a key by. Every lookup that asks the table a question — the
69
+ * descriptor, and therefore the TTL and the tags — has to undo what the key carries beyond the
70
+ * route, in the order the router does before it matches (`@ultimat3/http`'s `routing` stage):
71
+ *
72
+ * - the query: `descriptorFor('/blog?page=2')` matches no route;
73
+ * - the routed locale prefix: `/en/blog/a` is the `/blog/:slug` route in English. The key KEEPS the
74
+ * prefix — it is the URL the document was rendered for, `canonical` and all — and the table has
75
+ * never heard of it. Left on, a prefixed page matched no route: `ttlMs: null` and no tag edge, so
76
+ * it was fresh forever while the default locale's copy of the same page went stale on schedule.
77
+ *
78
+ * `unlocalizedPath` is i18n's, the one the router's own split is built on: a segment that names no
79
+ * routed locale is left alone, so `/fr/blog` is still no route.
72
80
  */
73
81
  function routePathOf(key: string): string {
74
82
  const query = key.indexOf('?');
75
- return query === -1 ? key : key.slice(0, query);
83
+ return unlocalizedPath(query === -1 ? key : key.slice(0, query));
76
84
  }
77
85
 
78
86
  /**