@gradial/aci 0.1.30 → 0.1.33

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.
Files changed (164) hide show
  1. package/README.md +4 -4
  2. package/bin/aci-darwin-arm64 +0 -0
  3. package/bin/aci.js +33 -32
  4. package/dist/astro/assets.d.ts +46 -0
  5. package/dist/astro/assets.js +51 -0
  6. package/dist/astro/index.d.ts +2 -0
  7. package/dist/astro/index.js +8 -0
  8. package/dist/astro/page.d.ts +88 -0
  9. package/dist/astro/page.js +117 -0
  10. package/dist/content/provider.d.ts +7 -0
  11. package/dist/dev/content-watch.d.ts +1 -0
  12. package/dist/dev/content-watch.js +1 -1
  13. package/dist/next/config.d.ts +6 -0
  14. package/dist/next/config.js +20 -13
  15. package/dist/next/content-watch.d.ts +1 -0
  16. package/dist/next/content-watch.js +2 -2
  17. package/dist/next/index.d.ts +1 -0
  18. package/dist/next/index.js +1 -0
  19. package/dist/next/middleware.d.ts +3 -2
  20. package/dist/next/middleware.js +40 -7
  21. package/dist/next/page.d.ts +13 -5
  22. package/dist/next/page.js +42 -17
  23. package/dist/next/server.d.ts +1 -1
  24. package/dist/next/server.js +33 -9
  25. package/dist/providers/s3.d.ts +7 -0
  26. package/dist/providers/s3.js +20 -0
  27. package/dist/testing/conformance/checks.d.ts +12 -0
  28. package/dist/testing/conformance/checks.js +483 -0
  29. package/dist/testing/conformance/index.d.ts +3 -0
  30. package/dist/testing/conformance/index.js +2 -0
  31. package/dist/testing/conformance/preset.d.ts +14 -0
  32. package/dist/testing/conformance/preset.js +98 -0
  33. package/dist/testing/conformance/types.d.ts +45 -0
  34. package/dist/testing/conformance/types.js +7 -0
  35. package/package.json +17 -9
  36. package/src/cli/materialize-template.mjs +133 -0
  37. package/src/cli/validate-content.mjs +0 -0
  38. package/src/cli/verify-next-cdn-routes.mjs +10 -19
  39. package/templates/astro/starter-content/src/cms/contracts/components/button.contract.ts +19 -0
  40. package/templates/astro/starter-content/src/cms/contracts/components/container.contract.ts +19 -0
  41. package/templates/astro/starter-content/src/cms/contracts/components/featureCard.contract.ts +15 -0
  42. package/templates/astro/starter-content/src/cms/contracts/components/featureGrid.contract.ts +16 -0
  43. package/templates/astro/starter-content/src/cms/contracts/components/homeHero.contract.ts +36 -0
  44. package/templates/astro/starter-content/src/cms/contracts/components/index.ts +21 -0
  45. package/templates/astro/starter-content/src/cms/contracts/components/statsBar.contract.ts +19 -0
  46. package/templates/astro/starter-content/src/cms/registry.ts +28 -0
  47. package/templates/astro/template/{.aci.yaml → .aci.yaml.tmpl} +1 -1
  48. package/templates/astro/template/README.md.tmpl +53 -0
  49. package/templates/astro/template/gitignore +12 -0
  50. package/templates/astro/template/package.json.tmpl +5 -3
  51. package/templates/astro/template/src/cms/contracts/components/index.ts +3 -0
  52. package/templates/astro/template/src/cms/registry.ts +3 -26
  53. package/templates/astro/template/src/pages/[...slug].astro +13 -35
  54. package/templates/astro/template/tests/conformance.test.ts.tmpl +12 -0
  55. package/templates/common/.agents/AGENTS.md +3 -5
  56. package/templates/common/.agents/skills/aci/SKILL.md +72 -23
  57. package/templates/common/.agents/skills/authoring/SKILL.md +53 -14
  58. package/templates/common/.agents/skills/authoring/references/block-registry.md +109 -15
  59. package/templates/common/.agents/skills/authoring/references/contracts.md +23 -7
  60. package/templates/common/.agents/skills/authoring/references/repo-structure.md +35 -19
  61. package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +75 -17
  62. package/templates/common/.agents/skills/migration/SKILL.md +7 -1
  63. package/templates/common/.agents/skills/migration/references/canon-migration.md +471 -0
  64. package/templates/common/.content/config/site.json.tmpl +15 -0
  65. package/templates/common/.content/pages/home/_index.json +4 -103
  66. package/templates/common/.nvmrc +1 -0
  67. package/templates/common/README.md.tmpl +32 -11
  68. package/templates/common/contracts/components/index.ts +1 -19
  69. package/templates/common/design-system/tokens/colors.css +34 -10
  70. package/templates/common/starter-content/.content/config/robots.txt +2 -0
  71. package/templates/common/starter-content/.content/pages/home/_index.json +112 -0
  72. package/templates/common/starter-content/.content/schemas/site.schema.json +138 -0
  73. package/templates/nextjs/starter-content/src/cms/contracts/components/container.contract.ts +19 -0
  74. package/templates/nextjs/starter-content/src/cms/contracts/components/featureCard.contract.ts +15 -0
  75. package/templates/nextjs/starter-content/src/cms/contracts/components/featureGrid.contract.ts +16 -0
  76. package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +21 -0
  77. package/templates/nextjs/starter-content/src/cms/contracts/components/siteFooter.contract.ts +45 -0
  78. package/templates/nextjs/starter-content/src/cms/contracts/components/siteNavigation.contract.ts +30 -0
  79. package/templates/nextjs/starter-content/src/cms/contracts/components/statsBar.contract.ts +19 -0
  80. package/templates/nextjs/starter-content/src/cms/registry.ts +28 -0
  81. package/templates/nextjs/template/{.aci.yaml → .aci.yaml.tmpl} +1 -1
  82. package/templates/nextjs/template/README.md.tmpl +52 -0
  83. package/templates/nextjs/template/gitignore +15 -0
  84. package/templates/nextjs/template/next.config.mjs +1 -6
  85. package/templates/nextjs/template/package.json.tmpl +5 -5
  86. package/templates/nextjs/template/src/app/gradial/assets/[...path]/route.ts +1 -4
  87. package/templates/nextjs/template/src/app/layout.tsx +1 -3
  88. package/templates/nextjs/template/src/cms/contracts/components/index.ts +1 -13
  89. package/templates/nextjs/template/src/cms/registry.ts +3 -26
  90. package/templates/nextjs/template/src/design-system/styles.css +346 -0
  91. package/templates/nextjs/template/src/design-system/tokens/colors.css +110 -0
  92. package/templates/nextjs/template/src/design-system/tokens/components.css +86 -0
  93. package/templates/nextjs/template/src/design-system/tokens/index.css +6 -0
  94. package/templates/nextjs/template/src/design-system/tokens/radii.css +5 -0
  95. package/templates/nextjs/template/src/design-system/tokens/shadows.css +9 -0
  96. package/templates/nextjs/template/src/design-system/tokens/spacing.css +26 -0
  97. package/templates/nextjs/template/src/design-system/tokens/typography.css +32 -0
  98. package/templates/nextjs/template/src/middleware.ts.tmpl +12 -0
  99. package/templates/nextjs/template/tests/conformance.test.ts.tmpl +12 -0
  100. package/templates/nextjs/template/src/app/_components/ConsentManager.tsx +0 -132
  101. package/templates/nextjs/template/src/cms/contracts/components/container.contract.ts +0 -20
  102. package/templates/nextjs/template/src/middleware.ts +0 -7
  103. /package/templates/{nextjs/template → astro/starter-content}/src/cms/contracts/components/siteFooter.contract.ts +0 -0
  104. /package/templates/{nextjs/template → astro/starter-content}/src/cms/contracts/components/siteNavigation.contract.ts +0 -0
  105. /package/templates/astro/{template → starter-content}/src/components/chrome/Footer.astro +0 -0
  106. /package/templates/astro/{template → starter-content}/src/components/chrome/Header.astro +0 -0
  107. /package/templates/astro/{template → starter-content}/src/components/elements/AtomicBlock.astro +0 -0
  108. /package/templates/astro/{template → starter-content}/src/components/elements/AtomicBlocks.astro +0 -0
  109. /package/templates/astro/{template → starter-content}/src/components/elements/Button.astro +0 -0
  110. /package/templates/astro/{template → starter-content}/src/components/elements/ButtonBlock.astro +0 -0
  111. /package/templates/astro/{template → starter-content}/src/components/elements/Container.astro +0 -0
  112. /package/templates/astro/{template → starter-content}/src/components/elements/Icon.astro +0 -0
  113. /package/templates/astro/{template → starter-content}/src/components/elements/Logo.astro +0 -0
  114. /package/templates/astro/{template → starter-content}/src/components/elements/RemoteImage.astro +0 -0
  115. /package/templates/astro/{template → starter-content}/src/components/sections/ContainerSection.astro +0 -0
  116. /package/templates/astro/{template → starter-content}/src/components/sections/FeatureCard.astro +0 -0
  117. /package/templates/astro/{template → starter-content}/src/components/sections/FeatureGrid.astro +0 -0
  118. /package/templates/astro/{template → starter-content}/src/components/sections/HomeHero.astro +0 -0
  119. /package/templates/astro/{template → starter-content}/src/components/sections/StatsBar.astro +0 -0
  120. /package/templates/astro/{template → starter-content}/src/fixtures/atomicBlocks.ts +0 -0
  121. /package/templates/astro/{template → starter-content}/src/fixtures/composition.ts +0 -0
  122. /package/templates/astro/{template → starter-content}/src/fixtures/index.ts +0 -0
  123. /package/templates/astro/{template → starter-content}/src/fixtures/sections.ts +0 -0
  124. /package/templates/astro/{template → starter-content}/src/render/RenderPage.astro +0 -0
  125. /package/templates/astro/{template → starter-content}/src/stories/CompositionStoryFrame.astro +0 -0
  126. /package/templates/astro/{template → starter-content}/src/stories/HomeHero.stories.ts +0 -0
  127. /package/templates/astro/{template → starter-content}/src/stories/SectionStoryFrame.astro +0 -0
  128. /package/templates/astro/{template → starter-content}/src/stories/StoryDocFrame.astro +0 -0
  129. /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
  130. /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
  131. /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
  132. /package/templates/common/{.content → starter-content/.content}/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
  133. /package/templates/common/{.content → starter-content/.content}/config/site.json +0 -0
  134. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/asset.json +0 -0
  135. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/editorial.json +0 -0
  136. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/governance.json +0 -0
  137. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -0
  138. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/asset.json +0 -0
  139. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/editorial.json +0 -0
  140. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/governance.json +0 -0
  141. /package/templates/common/{.content → starter-content/.content}/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -0
  142. /package/templates/common/{.content → starter-content/.content}/fragments/navbar/_index.json +0 -0
  143. /package/templates/common/{.content → starter-content/.content}/fragments/site-footer/_index.json +0 -0
  144. /package/templates/common/{.content → starter-content/.content}/pages/about/_index.json +0 -0
  145. /package/templates/common/{public → starter-content/public}/assets/Dwire-model.png +0 -0
  146. /package/templates/common/{public → starter-content/public}/assets/seattle.avif +0 -0
  147. /package/templates/nextjs/{template → starter-content}/src/cms/contracts/components/button.contract.ts +0 -0
  148. /package/templates/nextjs/{template → starter-content}/src/cms/contracts/components/homeHero.contract.ts +0 -0
  149. /package/templates/nextjs/{template → starter-content}/src/components/chrome/Footer.tsx +0 -0
  150. /package/templates/nextjs/{template → starter-content}/src/components/chrome/Header.tsx +0 -0
  151. /package/templates/nextjs/{template → starter-content}/src/components/elements/AtomicBlock.tsx +0 -0
  152. /package/templates/nextjs/{template → starter-content}/src/components/elements/Button.tsx +0 -0
  153. /package/templates/nextjs/{template → starter-content}/src/components/elements/ButtonBlock.tsx +0 -0
  154. /package/templates/nextjs/{template → starter-content}/src/components/elements/Container.tsx +0 -0
  155. /package/templates/nextjs/{template → starter-content}/src/components/elements/Icon.tsx +0 -0
  156. /package/templates/nextjs/{template → starter-content}/src/components/elements/Logo.tsx +0 -0
  157. /package/templates/nextjs/{template → starter-content}/src/components/sections/ContainerSection.tsx +0 -0
  158. /package/templates/nextjs/{template → starter-content}/src/components/sections/FeatureCard.tsx +0 -0
  159. /package/templates/nextjs/{template → starter-content}/src/components/sections/FeatureGrid.tsx +0 -0
  160. /package/templates/nextjs/{template → starter-content}/src/components/sections/HomeHero.tsx +0 -0
  161. /package/templates/nextjs/{template → starter-content}/src/components/sections/StatsBar.tsx +0 -0
  162. /package/templates/nextjs/{template → starter-content}/src/stories/Button.stories.tsx +0 -0
  163. /package/templates/nextjs/{template → starter-content}/src/stories/Header.stories.tsx +0 -0
  164. /package/templates/nextjs/{template → starter-content}/src/stories/HomeHero.stories.tsx +0 -0
@@ -2,9 +2,15 @@ import { NextResponse } from 'next/server.js';
2
2
  import { getUncachedEdgeConfigValue } from './edge-config.js';
3
3
  import { PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, extractPreviewToken, resolvePreviewContext, createPreviewCookies, previewSignKey, } from '../preview/core.js';
4
4
  const RELEASE_SCOPED_PREFIX = '/__r';
5
+ // CMS document paths (robots.txt, sitemap.xml) pass through the middleware
6
+ // and are served by Next.js rewrites configured in withAci().
7
+ const CMS_DOCUMENT_PATHS = new Set(['/robots.txt', '/sitemap.xml']);
5
8
  const PUBLIC_PATHS = new Set(typeof __ACI_PUBLIC_PATHS__ !== 'undefined' ? __ACI_PUBLIC_PATHS__ : []);
6
9
  const PASSTHROUGH_ROUTES = (typeof __ACI_PASSTHROUGH_ROUTES__ !== 'undefined' ? __ACI_PASSTHROUGH_ROUTES__ : []);
7
- export function createGradialMiddleware(config = {}) {
10
+ const ASSET_PREFIX = typeof __ACI_ASSET_PREFIX__ !== 'undefined'
11
+ ? __ACI_ASSET_PREFIX__
12
+ : '/.gradial-dam';
13
+ export function createGradialMiddleware(config) {
8
14
  return async function middleware(request) {
9
15
  const url = request.nextUrl;
10
16
  // ?leave_preview=1 → clear cookies, redirect to clean URL
@@ -18,6 +24,14 @@ export function createGradialMiddleware(config = {}) {
18
24
  if (PUBLIC_PATHS.has(url.pathname)) {
19
25
  return NextResponse.next();
20
26
  }
27
+ if (CMS_DOCUMENT_PATHS.has(url.pathname)) {
28
+ return NextResponse.next();
29
+ }
30
+ // DAM asset requests are NOT release-scoped; they rewrite to S3 via
31
+ // withAci() rewrites. Pass them through so the rewrite fires.
32
+ if (isAssetPath(url.pathname)) {
33
+ return NextResponse.next();
34
+ }
21
35
  if (isPassthroughRoute(url.pathname)) {
22
36
  return NextResponse.next();
23
37
  }
@@ -31,11 +45,24 @@ export function createGradialMiddleware(config = {}) {
31
45
  // Try preview token first; if valid, use that release.
32
46
  // If no token or invalid, fall through to active release from edge config.
33
47
  const preview = await resolvePreviewFromRequest(request, config);
48
+ // Local file-backed development has no release-indexed artifact tree. A
49
+ // valid preview token is still useful there—it proves signing, expiry, and
50
+ // cookie propagation—but rewriting to /__r/{releaseId}/{path} would create
51
+ // a path the file provider cannot serve. Keep the public route and mark
52
+ // the response private, matching preview's cache semantics.
53
+ if (process.env.ACI_CONTENT_PROVIDER === 'file') {
54
+ const response = NextResponse.next();
55
+ const previewReleaseID = preview?.releaseId;
56
+ if (preview?.token && preview.isPreview && previewReleaseID) {
57
+ response.headers.set('Cache-Control', 'private, no-store');
58
+ for (const cookie of createPreviewCookies(preview.token, previewReleaseID)) {
59
+ response.cookies.set(cookie.name, cookie.value, cookie.options);
60
+ }
61
+ }
62
+ return response;
63
+ }
34
64
  const releaseId = preview?.releaseId || await activeReleaseFromEdgeConfig(config);
35
65
  if (!releaseId) {
36
- if (process.env.ACI_CONTENT_PROVIDER === 'file') {
37
- return NextResponse.next();
38
- }
39
66
  if (!siteIdForConfig(config)) {
40
67
  return new Response('ACI site id is not configured', {
41
68
  status: 500,
@@ -61,7 +88,7 @@ export function createGradialMiddleware(config = {}) {
61
88
  return response;
62
89
  }
63
90
  // Rewrite public URL to release-scoped internal path for CDN cache isolation.
64
- // Each release gets a unique URL (/__r/{releaseId}/routes/...) so the CDN
91
+ // Each release gets a unique URL (/__r/{releaseId}/...) so the CDN
65
92
  // cache key is per-release. New release = new URL = cache miss = fresh content.
66
93
  const response = rewriteToReleaseScopedPath(request, releaseId, Boolean(preview?.isPreview));
67
94
  // Persist preview token as cookie so navigation stays in preview
@@ -159,7 +186,7 @@ async function activeReleaseFromEdgeConfig(config) {
159
186
  return clampRelease(active.releaseId, active.sequence || 0, compatibility);
160
187
  }
161
188
  function siteIdForConfig(config) {
162
- return config.siteId || process.env.ACI_SITE_ID || '';
189
+ return config.siteId;
163
190
  }
164
191
  async function getEdgeConfigJSON(edgeConfig, key) {
165
192
  const value = await getEdgeConfigValue(edgeConfig, key);
@@ -216,6 +243,9 @@ function clampRelease(activeReleaseId, activeSequence, compatibility) {
216
243
  function edgeKeySegment(value) {
217
244
  return value.replace(/[^A-Za-z0-9_-]/g, '_');
218
245
  }
246
+ function isAssetPath(pathname) {
247
+ return pathname === ASSET_PREFIX || pathname.startsWith(`${ASSET_PREFIX}/`);
248
+ }
219
249
  function isPassthroughRoute(pathname) {
220
250
  return PASSTHROUGH_ROUTES.some((route) => routeMatches(pathname, route));
221
251
  }
@@ -241,6 +271,9 @@ function normalizeRoutePath(value) {
241
271
  const pathname = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
242
272
  return pathname.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
243
273
  }
274
+ export const aciMiddlewareMatcher = [
275
+ '/((?!_next(?:/|$)|api(?:/|$)|favicon\\.ico$|\\.gradial-dam(?:/|$)).*)',
276
+ ];
244
277
  export const config = {
245
- matcher: ['/((?!_next(?:/|$)|api(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)'],
278
+ matcher: aciMiddlewareMatcher,
246
279
  };
@@ -30,11 +30,19 @@ export interface CreatePageOptions {
30
30
  * When the site config enables personalization, the page tree also carries
31
31
  * the collect script tag exactly once per page (design/04 contract tag).
32
32
  *
33
- * Release-scoped routes: when the middleware rewrites a public URL to
34
- * `/__r/{releaseId}/routes/{path}`, the catch-all slug becomes
35
- * `['__r', releaseId, 'routes', ...pathSegments]`. createPage detects this
36
- * prefix, extracts the releaseId, and passes it to the content loader. This
37
- * gives each release a unique CDN cache key without a separate route folder.
33
+ * Release-scoped routes (production): the middleware rewrites a public URL
34
+ * to `/__r/{releaseId}/routes/{path}`. The optional catch-all `[[...slug]]`
35
+ * receives `['__r', releaseId, 'routes', ...pathSegments]`. createPage
36
+ * extracts the releaseId and passes it to the content loader, giving
37
+ * each release a unique CDN cache key without a separate route folder.
38
+ * The `routes` namespace segment leaves room for future siblings under
39
+ * the release prefix (e.g. `/__r/{releaseId}/assets/*`).
40
+ *
41
+ * Dev pass-through (ACI_CONTENT_PROVIDER=file): the middleware passes
42
+ * requests through unrewritten, so `/` arrives with an empty/undefined
43
+ * slug and `/about` arrives as `['about']`. The optional catch-all
44
+ * `[[...slug]]` handles both cases. No releaseId is present; the file
45
+ * provider serves content directly from disk.
38
46
  *
39
47
  * Layout slots: createPage loads the page's layout, iterates all slots, and
40
48
  * fills each from the page's regions. If a slot is empty, it falls back to
package/dist/next/page.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { notFound } from 'next/navigation.js';
3
3
  import { Fragment } from 'react';
4
- import { PageNotFoundError, routeFromNextParams, getRenderInput, getLayout, getFragment, } from './server.js';
4
+ import { PageNotFoundError, getRenderInput, getLayout, getFragment, } from './server.js';
5
5
  import { routeMetadataForContent, resolveSlots, } from '../content/index.js';
6
6
  import { renderBlocks } from './render.js';
7
7
  import { CollectScript } from './collect-script.js';
@@ -25,11 +25,19 @@ import { CollectScript } from './collect-script.js';
25
25
  * When the site config enables personalization, the page tree also carries
26
26
  * the collect script tag exactly once per page (design/04 contract tag).
27
27
  *
28
- * Release-scoped routes: when the middleware rewrites a public URL to
29
- * `/__r/{releaseId}/routes/{path}`, the catch-all slug becomes
30
- * `['__r', releaseId, 'routes', ...pathSegments]`. createPage detects this
31
- * prefix, extracts the releaseId, and passes it to the content loader. This
32
- * gives each release a unique CDN cache key without a separate route folder.
28
+ * Release-scoped routes (production): the middleware rewrites a public URL
29
+ * to `/__r/{releaseId}/routes/{path}`. The optional catch-all `[[...slug]]`
30
+ * receives `['__r', releaseId, 'routes', ...pathSegments]`. createPage
31
+ * extracts the releaseId and passes it to the content loader, giving
32
+ * each release a unique CDN cache key without a separate route folder.
33
+ * The `routes` namespace segment leaves room for future siblings under
34
+ * the release prefix (e.g. `/__r/{releaseId}/assets/*`).
35
+ *
36
+ * Dev pass-through (ACI_CONTENT_PROVIDER=file): the middleware passes
37
+ * requests through unrewritten, so `/` arrives with an empty/undefined
38
+ * slug and `/about` arrives as `['about']`. The optional catch-all
39
+ * `[[...slug]]` handles both cases. No releaseId is present; the file
40
+ * provider serves content directly from disk.
33
41
  *
34
42
  * Layout slots: createPage loads the page's layout, iterates all slots, and
35
43
  * fills each from the page's regions. If a slot is empty, it falls back to
@@ -87,26 +95,43 @@ export function createPage(registry, options) {
87
95
  };
88
96
  }
89
97
  /**
90
- * Resolves the logical route and optional releaseId from Next.js catch-all
91
- * params. When the middleware rewrites to /__r/{releaseId}/routes/{path},
92
- * the slug is ['__r', releaseId, 'routes', ...pathSegments]. Otherwise the
93
- * slug is treated as the public route directly.
98
+ * Resolves the logical route and releaseId from Next.js catch-all params.
99
+ *
100
+ * Two shapes are supported:
101
+ *
102
+ * 1. **Rewritten (production):** middleware rewrites a public URL to
103
+ * `/__r/{releaseId}/routes/{path}`, so slug is
104
+ * `['__r', releaseId, 'routes', ...pathSegments]`. The releaseId is
105
+ * extracted; the remaining segments form the content route. The `routes`
106
+ * segment is the namespace for page routes (siblings like `assets/` live
107
+ * under the same release prefix).
108
+ *
109
+ * 2. **Dev pass-through (ACI_CONTENT_PROVIDER=file):** middleware passes
110
+ * requests through unrewritten, so the slug is the raw public path
111
+ * segments (possibly empty/undefined for `/`). No releaseId is present;
112
+ * the file provider serves content from disk.
94
113
  */
95
114
  async function resolveRouteAndRelease(params) {
96
115
  const resolved = await params;
97
- const slug = resolved?.slug;
98
- if (slug && slug.length >= 3 && slug[0] === '__r' && slug[2] === 'routes') {
116
+ const slug = resolved.slug ?? [];
117
+ if (slug.length >= 2 && slug[0] === '__r') {
99
118
  const releaseId = decodeURIComponent(slug[1]);
100
119
  if (!releaseId || releaseId.includes('/') || releaseId === '.' || releaseId === '..') {
101
120
  return { route: '/' };
102
121
  }
103
- const pathSegments = slug.slice(3);
104
- if (pathSegments.length === 0)
122
+ // Skip the 'routes' namespace segment when present.
123
+ const rest = slug.length >= 3 && slug[2] === 'routes' ? slug.slice(3) : slug.slice(2);
124
+ if (rest.length === 0)
105
125
  return { route: '/', releaseId };
106
- if (pathSegments.some((s) => s === '' || s === '.' || s === '..'))
126
+ if (rest.some((s) => s === '' || s === '.' || s === '..'))
107
127
  return { route: '/' };
108
- return { route: `/${pathSegments.join('/')}/`, releaseId };
128
+ return { route: `/${rest.join('/')}/`, releaseId };
109
129
  }
110
- const route = await routeFromNextParams(params);
130
+ const route = routeFromSlug(slug);
111
131
  return { route };
112
132
  }
133
+ function routeFromSlug(slug) {
134
+ if (slug.length === 0)
135
+ return '/';
136
+ return `/${slug.join('/')}/`;
137
+ }
@@ -34,7 +34,7 @@ export declare function getRoutes(config?: FetchConfig): Promise<string[]>;
34
34
  export declare function getRenderInput<TPage, TSite extends SiteConfig = SiteConfig>(route?: string, config?: FetchConfig): Promise<ServerRenderInput<TPage, TSite>>;
35
35
  export declare function getPageRuntimeRenderInput(): Promise<RenderInput | null>;
36
36
  export { isPreviewMode } from './preview-mode.js';
37
- export declare function routeFromNextParams(params?: Promise<{
37
+ export declare function routeFromNextParams(params: Promise<{
38
38
  slug?: string[];
39
39
  }> | {
40
40
  slug?: string[];
@@ -10,6 +10,9 @@ export async function getPage(route = '', config = {}) {
10
10
  if (runtimeInput)
11
11
  return runtimeInput.page;
12
12
  const provider = await resolveProviderWithFallback(config);
13
+ const variantPage = await pageFromFunctionRef(provider);
14
+ if (variantPage !== null)
15
+ return variantPage;
13
16
  return await provider.getPage(route);
14
17
  }
15
18
  export async function getSiteConfig(config = {}) {
@@ -53,7 +56,7 @@ export async function getRenderInput(route = '/', config = {}) {
53
56
  // Cache shared resources (manifest, siteConfig) but not per-page content
54
57
  // Page-level caching is handled by CDN
55
58
  const [page, siteConfig] = await Promise.all([
56
- provider.getPage(route),
59
+ pageFromFunctionRef(provider).then((variantPage) => variantPage ?? provider.getPage(route)),
57
60
  getCachedSiteConfig(provider),
58
61
  ]);
59
62
  return {
@@ -73,23 +76,44 @@ export async function getPageRuntimeRenderInput() {
73
76
  return null;
74
77
  }
75
78
  }
79
+ // The personalization edge proxies framework-origin requests with
80
+ // X-Baremetal-Function-Ref naming the exact page payload its decision
81
+ // selected (base or variant). When present, the SSR render must use that
82
+ // payload — otherwise every visitor gets the base page and edge decisioning
83
+ // is cosmetic. The S3 provider only serves refs the release manifest
84
+ // declares.
85
+ const BAREMETAL_FUNCTION_REF_HEADER = 'x-baremetal-function-ref';
86
+ async function pageFromFunctionRef(provider) {
87
+ if (!(provider instanceof S3ContentProvider))
88
+ return null;
89
+ let ref = '';
90
+ try {
91
+ const current = await headers();
92
+ ref = (current.get(BAREMETAL_FUNCTION_REF_HEADER) || '').trim();
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ if (!ref)
98
+ return null;
99
+ return await provider.getPageByRef(ref);
100
+ }
76
101
  export { isPreviewMode } from './preview-mode.js';
77
102
  export async function routeFromNextParams(params) {
78
- if (!params)
79
- return '/';
80
103
  const resolved = await params;
81
- if (!resolved.slug || resolved.slug.length === 0)
104
+ const slug = resolved.slug ?? [];
105
+ if (slug.length === 0)
82
106
  return '/';
83
- return `/${resolved.slug.join('/')}/`;
107
+ return `/${slug.join('/')}/`;
84
108
  }
85
109
  export async function generateStaticParams() {
86
110
  const { FileContentProvider } = await import('../providers/file.js');
87
111
  const provider = new FileContentProvider();
88
112
  const routes = await provider.listRoutes();
89
- return routes
90
- .filter((entry) => entry.path !== '/')
91
- .map((entry) => ({
92
- slug: entry.path.replace(/^\/|\/$/g, '').split('/'),
113
+ return routes.map((entry) => ({
114
+ slug: entry.path === '/'
115
+ ? ['index']
116
+ : entry.path.replace(/^\/|\/$/g, '').split('/'),
93
117
  }));
94
118
  }
95
119
  export class ReleaseAssetNotFoundError extends Error {
@@ -16,6 +16,13 @@ export declare class S3ContentProvider implements ContentProvider {
16
16
  listRoutes(): Promise<RouteEntry[]>;
17
17
  getSiteConfig<T = unknown>(): Promise<T>;
18
18
  getPage<T = unknown>(route: string): Promise<T>;
19
+ /**
20
+ * getPageByRef serves the exact payload the edge selected (the
21
+ * X-Baremetal-Function-Ref contract): base page or variant. Only refs the
22
+ * release manifest declares are served — the header names published
23
+ * content, it cannot address arbitrary keys.
24
+ */
25
+ getPageByRef<T = unknown>(payloadRef: string): Promise<T>;
19
26
  getFragment<T = unknown>(id: string): Promise<T>;
20
27
  getLayout<T = unknown>(name: string): Promise<T>;
21
28
  manifest(): Promise<CompiledManifest>;
@@ -32,6 +32,26 @@ export class S3ContentProvider {
32
32
  throw new PageNotFoundError(route, error);
33
33
  }
34
34
  }
35
+ /**
36
+ * getPageByRef serves the exact payload the edge selected (the
37
+ * X-Baremetal-Function-Ref contract): base page or variant. Only refs the
38
+ * release manifest declares are served — the header names published
39
+ * content, it cannot address arbitrary keys.
40
+ */
41
+ async getPageByRef(payloadRef) {
42
+ const manifest = await this.manifest();
43
+ const known = Object.values(manifest.routes).some((entry) => entry.payloadRef === payloadRef ||
44
+ Object.values(entry.variants ?? {}).some((variant) => variant.payloadRef === payloadRef));
45
+ if (!known) {
46
+ throw new PageNotFoundError(payloadRef);
47
+ }
48
+ try {
49
+ return await this.getJSON(payloadRef);
50
+ }
51
+ catch (error) {
52
+ throw new PageNotFoundError(payloadRef, error);
53
+ }
54
+ }
35
55
  async getFragment(id) {
36
56
  const manifest = await this.manifest();
37
57
  const entry = manifest.fragments[id];
@@ -0,0 +1,12 @@
1
+ import type { ConformanceCheckResult, ConformanceCheckName, ConformanceContext } from './types.js';
2
+ export declare function checkConfig(ctx: ConformanceContext): ConformanceCheckResult[];
3
+ export declare function checkRegistry(ctx: ConformanceContext): ConformanceCheckResult[];
4
+ export declare function checkPageEntry(ctx: ConformanceContext): ConformanceCheckResult[];
5
+ export declare function checkMiddleware(ctx: ConformanceContext): ConformanceCheckResult[];
6
+ export declare function checkNoFileContentProvider(ctx: ConformanceContext): ConformanceCheckResult[];
7
+ export declare function checkNoCustomContent(ctx: ConformanceContext): ConformanceCheckResult[];
8
+ export declare function checkNoLegacyAssetRoute(ctx: ConformanceContext): ConformanceCheckResult[];
9
+ export declare function checkBuildSucceeds(ctx: ConformanceContext): ConformanceCheckResult[];
10
+ export declare function checkContentCompile(ctx: ConformanceContext): ConformanceCheckResult[];
11
+ export declare function checkSdkVersion(ctx: ConformanceContext): ConformanceCheckResult[];
12
+ export declare const CHECK_REGISTRY: Record<ConformanceCheckName, (ctx: ConformanceContext) => ConformanceCheckResult[]>;