@gradial/aci 0.1.20-preview.1 → 0.1.20-preview.2

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 (57) hide show
  1. package/README.md +10 -8
  2. package/bin/aci +0 -0
  3. package/dist/astro/index.d.ts +0 -2
  4. package/dist/astro/index.js +0 -4
  5. package/dist/content/cache.test.d.ts +1 -0
  6. package/dist/content/cache.test.js +86 -0
  7. package/dist/content/index.d.ts +0 -1
  8. package/dist/content/index.js +0 -1
  9. package/dist/content/provider.d.ts +0 -19
  10. package/dist/content/provider.js +0 -7
  11. package/dist/content/types.d.ts +1 -1
  12. package/dist/content/validation.js +1 -0
  13. package/dist/define-component.js +6 -0
  14. package/dist/define-layout.d.ts +0 -2
  15. package/dist/define-layout.js +0 -3
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +0 -1
  18. package/dist/next/asset-route.d.ts +0 -2
  19. package/dist/next/asset-route.js +2 -10
  20. package/dist/next/config.js +1 -1
  21. package/dist/next/middleware.d.ts +2 -6
  22. package/dist/next/middleware.js +2 -7
  23. package/dist/next/page.d.ts +15 -16
  24. package/dist/next/page.js +9 -27
  25. package/dist/next/preview-banner.d.ts +4 -0
  26. package/dist/next/preview-banner.js +51 -0
  27. package/dist/next/preview.d.ts +7 -0
  28. package/dist/next/preview.js +37 -0
  29. package/dist/next/server.d.ts +2 -3
  30. package/dist/next/server.js +2 -12
  31. package/dist/providers/file.d.ts +0 -2
  32. package/dist/providers/file.js +1 -61
  33. package/dist/providers/s3.d.ts +0 -1
  34. package/dist/providers/s3.js +1 -14
  35. package/dist/sveltekit/index.d.ts +0 -2
  36. package/dist/sveltekit/index.js +0 -4
  37. package/dist/testing/index.d.ts +0 -3
  38. package/dist/testing/index.js +2 -15
  39. package/dist/types/component.d.ts +9 -0
  40. package/dist/types/config.d.ts +1 -0
  41. package/dist/types/page.d.ts +2 -1
  42. package/dist/types/render-mode.d.ts +1 -0
  43. package/package.json +7 -18
  44. package/src/cli/compile-registry.mjs +18 -127
  45. package/src/cli/verify-renderer.mjs +73 -0
  46. package/src/types/component.ts +10 -0
  47. package/src/types/config.ts +1 -0
  48. package/src/types/page.ts +2 -1
  49. package/src/types/render-mode.ts +8 -0
  50. package/dist/content/resolve-slots.d.ts +0 -9
  51. package/dist/content/resolve-slots.js +0 -24
  52. package/dist/next/render.d.ts +0 -5
  53. package/dist/next/render.js +0 -15
  54. package/dist/next/root-layout.d.ts +0 -4
  55. package/dist/next/root-layout.js +0 -5
  56. package/dist/registry.d.ts +0 -15
  57. package/dist/registry.js +0 -10
package/README.md CHANGED
@@ -10,9 +10,9 @@ content provider helpers, and framework-specific subpaths from one package.
10
10
  The fastest way to create a new ACI site is via the scaffolding script:
11
11
 
12
12
  ```bash
13
- pnpm exec tsx scripts/create-site.ts --framework astro --name my-site --dir ./my-site
13
+ npx tsx scripts/create-site.ts --framework astro --name my-site --dir ./my-site
14
14
  cd my-site
15
- pnpm run dev
15
+ npm run dev
16
16
  ```
17
17
 
18
18
  Available frameworks: `astro`, `nextjs`, `sveltekit`.
@@ -28,8 +28,8 @@ See the [templates directory](templates/) for the canonical starter source.
28
28
  ## Installation
29
29
 
30
30
  ```bash
31
- pnpm add @gradial/aci zod@^4
32
- pnpm add -D typescript tsx
31
+ npm install @gradial/aci zod@^4
32
+ npm install -D typescript tsx
33
33
  ```
34
34
 
35
35
  Common imports:
@@ -78,6 +78,7 @@ export const heroContract = defineComponentContract({
78
78
  headline: z.string(),
79
79
  image: GradialImageSchema,
80
80
  }),
81
+ renderModes: { canStatic: true, canSSR: true, canClientIsland: false },
81
82
  });
82
83
 
83
84
  export type HeroProps = z.infer<typeof heroContract.schema>;
@@ -106,6 +107,7 @@ const content = new FixtureContentProvider({
106
107
  $type: 'page',
107
108
  status: 'published',
108
109
  layout: 'marketing',
110
+ renderMode: 'static',
109
111
  metadata: { title: 'Test Home' },
110
112
  regions: { main: [] }
111
113
  }
@@ -121,7 +123,7 @@ Use this in unit tests when you want the same provider API as
121
123
  Publish from this package directory:
122
124
 
123
125
  ```bash
124
- pnpm publish --access public
126
+ npm publish --access public
125
127
  ```
126
128
 
127
129
  The package includes a `prepack` script that builds `dist` before packing or
@@ -129,15 +131,15 @@ publishing.
129
131
 
130
132
  ## Verify the Published Shape Locally
131
133
 
132
- Before publishing, inspect the exact tarball pnpm will upload. This catches
134
+ Before publishing, inspect the exact tarball npm will upload. This catches
133
135
  package `files` mistakes where local source imports work from the repo but fail
134
- after pnpm filters the packed files.
136
+ after npm filters the packed files.
135
137
 
136
138
  From `packages/sdk`:
137
139
 
138
140
  ```bash
139
141
  tmpdir="$(mktemp -d /tmp/gradial-aci-pack.XXXXXX)"
140
- filename="$(pnpm pack --pack-destination "$tmpdir" | tail -n 1)"
142
+ filename="$(npm pack --pack-destination "$tmpdir" | tail -n 1)"
141
143
  tar -tzf "$tmpdir/$filename" | sort | rg '^(package/src/cli/|package/src/types/|package/dist/types/)'
142
144
  echo "tarball=$tmpdir/$filename"
143
145
  ```
package/bin/aci CHANGED
Binary file
@@ -33,8 +33,6 @@ export declare function getGradialStaticPaths(): Promise<Array<{
33
33
  };
34
34
  }>>;
35
35
  export declare function getPageRuntimeRenderInput(request: Request): RenderInput | null;
36
- export { createRegistry, registryLookup } from '../registry.js';
37
- export type { Registry, RegistryEntry, AnyComponent } from '../registry.js';
38
36
  export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, type PreviewContext, type PreviewCookie, } from './preview.js';
39
37
  export interface GradialAstroOptions extends GradialContentWatchOptions {
40
38
  /** Path to compiled content root. Defaults to ACI_CONTENT_ROOT env or '.aci/compiled' */
@@ -32,10 +32,6 @@ export function getPageRuntimeRenderInput(request) {
32
32
  return getPendingRenderInputFromHeaders(request.headers);
33
33
  }
34
34
  // ---------------------------------------------------------------------------
35
- // Registry helpers
36
- // ---------------------------------------------------------------------------
37
- export { createRegistry, registryLookup } from '../registry.js';
38
- // ---------------------------------------------------------------------------
39
35
  // Preview support
40
36
  // ---------------------------------------------------------------------------
41
37
  export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, } from './preview.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, beforeEach } from '@jest/globals';
2
+ import { getCachedSiteConfig, getCachedManifest, clearProviderCache, getCacheStats, resetCacheStats } from './cache.js';
3
+ describe('Content Cache', () => {
4
+ let mockProvider;
5
+ let getSiteConfigCallCount = 0;
6
+ let manifestCallCount = 0;
7
+ beforeEach(() => {
8
+ getSiteConfigCallCount = 0;
9
+ manifestCallCount = 0;
10
+ resetCacheStats();
11
+ mockProvider = {
12
+ getSiteConfig: async () => {
13
+ getSiteConfigCallCount++;
14
+ return { title: 'Test Site' };
15
+ },
16
+ manifest: async () => {
17
+ manifestCallCount++;
18
+ return {
19
+ routes: {},
20
+ fragments: {},
21
+ siteConfigRef: 'site.json'
22
+ };
23
+ },
24
+ getPage: async () => ({}),
25
+ getFragment: async () => ({}),
26
+ listRoutes: async () => []
27
+ };
28
+ });
29
+ it('should cache siteConfig across multiple calls', async () => {
30
+ const result1 = await getCachedSiteConfig(mockProvider);
31
+ const result2 = await getCachedSiteConfig(mockProvider);
32
+ expect(getSiteConfigCallCount).toBe(1); // Only called once
33
+ expect(result1).toEqual(result2);
34
+ const stats = getCacheStats();
35
+ expect(stats.siteConfig.hits).toBe(1);
36
+ expect(stats.siteConfig.misses).toBe(1);
37
+ });
38
+ it('should cache manifest across multiple calls', async () => {
39
+ const result1 = await getCachedManifest(mockProvider);
40
+ const result2 = await getCachedManifest(mockProvider);
41
+ expect(manifestCallCount).toBe(1); // Only called once
42
+ expect(result1).toEqual(result2);
43
+ const stats = getCacheStats();
44
+ expect(stats.manifest.hits).toBe(1);
45
+ expect(stats.manifest.misses).toBe(1);
46
+ });
47
+ it('should clear cache for a provider', async () => {
48
+ await getCachedSiteConfig(mockProvider);
49
+ expect(getSiteConfigCallCount).toBe(1);
50
+ clearProviderCache(mockProvider);
51
+ await getCachedSiteConfig(mockProvider);
52
+ expect(getSiteConfigCallCount).toBe(2); // Called again after clear
53
+ const stats = getCacheStats();
54
+ expect(stats.siteConfig.misses).toBe(2);
55
+ });
56
+ it('should not cache rejected promises', async () => {
57
+ const errorProvider = {
58
+ getSiteConfig: async () => {
59
+ getSiteConfigCallCount++;
60
+ throw new Error('S3 timeout');
61
+ },
62
+ getPage: async () => ({}),
63
+ getFragment: async () => ({}),
64
+ listRoutes: async () => []
65
+ };
66
+ await expect(getCachedSiteConfig(errorProvider)).rejects.toThrow('S3 timeout');
67
+ expect(getSiteConfigCallCount).toBe(1);
68
+ // Second call should retry (not cached)
69
+ await expect(getCachedSiteConfig(errorProvider)).rejects.toThrow('S3 timeout');
70
+ expect(getSiteConfigCallCount).toBe(2);
71
+ });
72
+ it('should cache separately per provider instance', async () => {
73
+ const provider2 = {
74
+ getSiteConfig: async () => ({ title: 'Site 2' }),
75
+ getPage: async () => ({}),
76
+ getFragment: async () => ({}),
77
+ listRoutes: async () => []
78
+ };
79
+ const result1 = await getCachedSiteConfig(mockProvider);
80
+ const result2 = await getCachedSiteConfig(provider2);
81
+ expect(result1).toEqual({ title: 'Test Site' });
82
+ expect(result2).toEqual({ title: 'Site 2' });
83
+ const stats = getCacheStats();
84
+ expect(stats.siteConfig.misses).toBe(2); // Both are cache misses
85
+ });
86
+ });
@@ -2,4 +2,3 @@ export * from './provider.js';
2
2
  export * from './routes.js';
3
3
  export * from './types.js';
4
4
  export * from './cache.js';
5
- export * from './resolve-slots.js';
@@ -2,4 +2,3 @@ export * from './provider.js';
2
2
  export * from './routes.js';
3
3
  export * from './types.js';
4
4
  export * from './cache.js';
5
- export * from './resolve-slots.js';
@@ -12,33 +12,17 @@ export interface FragmentEntry {
12
12
  digest: string;
13
13
  fragmentRefs?: string[];
14
14
  }
15
- export interface LayoutEntry {
16
- name: string;
17
- slots: LayoutSlot[];
18
- defaults?: Record<string, FragmentRef[]>;
19
- }
20
- export interface LayoutSlot {
21
- name: string;
22
- required: boolean;
23
- }
24
- export interface FragmentRef {
25
- kind: string;
26
- fragmentId: string;
27
- inline: boolean;
28
- }
29
15
  export interface CompiledManifest {
30
16
  manifestVersion: string;
31
17
  buildDigest: string;
32
18
  siteConfigRef: string;
33
19
  routes: Record<string, RouteEntry>;
34
20
  fragments: Record<string, FragmentEntry>;
35
- layouts?: Record<string, LayoutEntry>;
36
21
  }
37
22
  export interface ContentProvider {
38
23
  getSiteConfig<T = unknown>(): Promise<T>;
39
24
  getPage<T = unknown>(route: string): Promise<T>;
40
25
  getFragment<T = unknown>(id: string): Promise<T>;
41
- getLayout<T = unknown>(name: string): Promise<T>;
42
26
  listRoutes(): Promise<RouteEntry[]>;
43
27
  }
44
28
  export declare class PageNotFoundError extends Error {
@@ -47,9 +31,6 @@ export declare class PageNotFoundError extends Error {
47
31
  export declare class FragmentNotFoundError extends Error {
48
32
  constructor(id: string, cause?: unknown);
49
33
  }
50
- export declare class LayoutNotFoundError extends Error {
51
- constructor(name: string, cause?: unknown);
52
- }
53
34
  export interface RenderInputOptions {
54
35
  domain?: string;
55
36
  locale?: string;
@@ -16,13 +16,6 @@ export class FragmentNotFoundError extends Error {
16
16
  this.cause = cause;
17
17
  }
18
18
  }
19
- export class LayoutNotFoundError extends Error {
20
- constructor(name, cause) {
21
- super(`Layout not found: ${name}`);
22
- this.name = 'LayoutNotFoundError';
23
- this.cause = cause;
24
- }
25
- }
26
19
  import { getCachedSiteConfig } from './cache.js';
27
20
  export async function loadRenderInput(provider, route = '/', options = {}) {
28
21
  const normalized = normalizeRoute(route);
@@ -55,6 +55,7 @@ export interface KernelPage {
55
55
  $type: 'page';
56
56
  status: string;
57
57
  layout: string;
58
+ renderMode: string;
58
59
  metadata: KernelPageSeo;
59
60
  regions: Record<string, KernelPageBlock[]>;
60
61
  }
@@ -92,7 +93,6 @@ export interface RenderInput<TPage extends KernelPage = KernelPage, TSiteConfig
92
93
  siteConfig: TSiteConfig;
93
94
  page: TPage | null;
94
95
  fragments?: Record<string, unknown>;
95
- layouts?: Record<string, unknown>;
96
96
  }
97
97
  export interface RenderOutput {
98
98
  route: string;
@@ -121,6 +121,7 @@ export function createContentSchemas(contracts = {}) {
121
121
  $type: z.literal('page'),
122
122
  status: z.string().min(1),
123
123
  layout: z.string().min(1),
124
+ renderMode: z.string().min(1),
124
125
  metadata: z.looseObject({
125
126
  title: z.string().optional(),
126
127
  description: z.string().optional(),
@@ -1,6 +1,12 @@
1
1
  export function defineComponent(definition) {
2
+ const renderModes = definition.renderModes ?? definition.render;
3
+ if (!renderModes) {
4
+ throw new Error(`component ${definition.name} must define renderModes`);
5
+ }
2
6
  return {
3
7
  ...definition,
8
+ renderModes,
9
+ render: renderModes,
4
10
  varyDimensions: definition.varyDimensions ?? definition.vary,
5
11
  vary: definition.vary ?? definition.varyDimensions,
6
12
  };
@@ -1,6 +1,4 @@
1
1
  import type { LayoutContract, LayoutDefinition, LayoutSlot } from './types/layout.js';
2
- import type { FragmentRefNode } from './types/page.js';
3
2
  export declare function defineLayout(definition: LayoutDefinition): LayoutContract;
4
3
  export declare const defineLayoutContract: typeof defineLayout;
5
4
  export declare function slot(name: string, required?: boolean): LayoutSlot;
6
- export declare function fragmentDefault(fragmentId: string): FragmentRefNode;
@@ -9,6 +9,3 @@ export const defineLayoutContract = defineLayout;
9
9
  export function slot(name, required = false) {
10
10
  return { name, required };
11
11
  }
12
- export function fragmentDefault(fragmentId) {
13
- return { kind: 'fragment-ref', fragmentId, inline: true };
14
- }
package/dist/index.d.ts CHANGED
@@ -2,5 +2,4 @@ export * from './block-ref.js';
2
2
  export * from './define-component.js';
3
3
  export * from './define-data-model.js';
4
4
  export * from './define-layout.js';
5
- export * from './registry.js';
6
5
  export * from './types/index.js';
package/dist/index.js CHANGED
@@ -2,7 +2,6 @@ export * from './block-ref.js';
2
2
  export * from './define-component.js';
3
3
  export * from './define-data-model.js';
4
4
  export * from './define-layout.js';
5
- export * from './registry.js';
6
5
  export * from './types/index.js';
7
6
  // Note: content/ is NOT re-exported here because it imports node:path,
8
7
  // making it incompatible with client-side bundlers. Import server-only
@@ -5,7 +5,5 @@ type AssetRouteParams = {
5
5
  releaseId?: string;
6
6
  path?: string[];
7
7
  };
8
- export declare const runtime = "nodejs";
9
- export declare const dynamic = "force-dynamic";
10
8
  export declare function GET(_request: Request, { params }: AssetRouteContext): Promise<Response>;
11
9
  export {};
@@ -1,19 +1,11 @@
1
- import { FileContentProvider } from '../providers/file.js';
2
1
  import { getReleaseAssetResponse } from './server.js';
3
- const LOCAL_RELEASE_ID = 'local';
4
- export const runtime = 'nodejs';
5
- export const dynamic = 'force-dynamic';
6
2
  export async function GET(_request, { params }) {
7
3
  const resolvedParams = await params;
8
4
  const releaseId = resolvedParams.releaseId || '';
9
- const assetPath = '/' + (resolvedParams.path || []).join('/');
10
- if (!releaseId || assetPath === '/') {
5
+ const assetPath = (resolvedParams.path || []).join('/');
6
+ if (!releaseId || !assetPath) {
11
7
  return new Response('asset not found', { status: 404 });
12
8
  }
13
- if (releaseId === LOCAL_RELEASE_ID) {
14
- const provider = new FileContentProvider();
15
- return provider.fetchRaw(assetPath);
16
- }
17
9
  try {
18
10
  return await getReleaseAssetResponse(releaseId, assetPath);
19
11
  }
@@ -9,7 +9,7 @@ export function withGradialAci(nextConfig = {}, options = {}) {
9
9
  const gradialRewrites = [
10
10
  {
11
11
  source: `${assetPrefix}/:path*`,
12
- destination: `/gradial/assets/local${assetPrefix}/:path*`,
12
+ destination: `/api/aci-assets${assetPrefix}/:path*`,
13
13
  },
14
14
  ];
15
15
  if (!userRewrites)
@@ -1,11 +1,7 @@
1
1
  import { NextResponse, type NextRequest } from 'next/server.js';
2
2
  export interface GradialMiddlewareConfig {
3
- siteId?: string;
3
+ siteId: string;
4
4
  edgeConfig?: string;
5
- previewSignKey?: string;
6
5
  deploymentId?: string;
7
6
  }
8
- export declare function createGradialMiddleware(config?: GradialMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
9
- export declare const config: {
10
- matcher: string[];
11
- };
7
+ export declare function createGradialMiddleware(config: GradialMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
@@ -3,7 +3,7 @@ import { getUncachedEdgeConfigValue } from './edge-config.js';
3
3
  import { PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, PREVIEW_HEADER, RELEASE_HEADER, extractPreviewToken, resolvePreviewContext, createPreviewCookies, previewSignKey, } from '../preview/core.js';
4
4
  const ROUTE_HEADER = 'x-gradial-route';
5
5
  const DISPATCH_HEADER = 'x-gradial-dispatch';
6
- export function createGradialMiddleware(config = {}) {
6
+ export function createGradialMiddleware(config) {
7
7
  return async function gradialMiddleware(request) {
8
8
  const url = request.nextUrl;
9
9
  // ?leave_preview=1 → clear cookies, redirect to clean URL
@@ -38,11 +38,6 @@ export function createGradialMiddleware(config = {}) {
38
38
  return response;
39
39
  };
40
40
  }
41
- export const config = {
42
- matcher: [
43
- '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
44
- ],
45
- };
46
41
  function gradialHeaders(headers) {
47
42
  const next = new Headers(headers);
48
43
  next.delete(RELEASE_HEADER);
@@ -64,7 +59,7 @@ async function resolvePreviewFromRequest(request, config) {
64
59
  console.warn('[gradial/mw] GRADIAL_PREVIEW_SIGN_KEY not set — preview tokens will be ignored');
65
60
  return null;
66
61
  }
67
- return await resolvePreviewContext({ token, siteId: config.siteId || '', signKey });
62
+ return await resolvePreviewContext({ token, siteId: config.siteId, signKey });
68
63
  }
69
64
  function cookieMap(cookies) {
70
65
  const map = new Map();
@@ -1,39 +1,38 @@
1
+ import type { ComponentType } from 'react';
1
2
  import type { Metadata } from 'next';
2
- import { type KernelPage } from '../content/index.js';
3
- import type { Registry } from '../registry.js';
3
+ import { type KernelPage, type KernelSiteConfig } from '../content/index.js';
4
+ export interface AciPageProps {
5
+ page: KernelPage;
6
+ siteConfig: KernelSiteConfig;
7
+ }
8
+ export interface CreateAciPageOptions {
9
+ /** The site's CmsPage component that renders chrome + content blocks. */
10
+ CmsPage: ComponentType<AciPageProps>;
11
+ }
4
12
  type NextPageProps = {
5
13
  params: Promise<{
6
14
  slug?: string[];
7
15
  }>;
8
16
  };
9
- export type SlotPropsFn = (slotName: string, page: KernelPage) => Record<string, unknown> | undefined;
10
- export interface CreatePageOptions {
11
- slotProps?: SlotPropsFn;
12
- }
13
17
  /**
14
18
  * Creates the catch-all route handler for a Next.js ACI site.
15
19
  *
16
20
  * Usage in `src/app/[[...slug]]/page.tsx`:
17
21
  *
18
22
  * ```tsx
19
- * import { createPage } from '@gradial/aci/next/page';
20
- * import { registry } from '@/cms/registry';
23
+ * import { createAciPage } from '@gradial/aci/next/page';
24
+ * import { CmsPage } from '@/components/CmsPage';
21
25
  *
22
- * const page = createPage(registry);
26
+ * const page = createAciPage({ CmsPage });
23
27
  * export const dynamic = page.dynamic;
24
28
  * export const generateMetadata = page.generateMetadata;
25
29
  * export default page.default;
26
30
  * ```
27
31
  *
28
32
  * The SDK owns param normalization, content loading, metadata generation,
29
- * notFound handling, layout resolution, and block rendering via the registry.
30
- *
31
- * Layout slots: createPage loads the page's layout, iterates all slots, and
32
- * fills each from the page's regions. If a slot is empty, it falls back to
33
- * the layout's fragment defaults (loaded via getFragment). This lets chrome
34
- * be CMS-managed fragments instead of hardcoded component imports.
33
+ * and notFound handling. The site only provides its CmsPage component.
35
34
  */
36
- export declare function createPage(registry: Registry, options?: CreatePageOptions): {
35
+ export declare function createAciPage({ CmsPage }: CreateAciPageOptions): {
37
36
  dynamic: "force-dynamic";
38
37
  generateMetadata: ({ params }: NextPageProps) => Promise<Metadata>;
39
38
  default: ({ params }: NextPageProps) => Promise<import("react").JSX.Element>;
package/dist/next/page.js CHANGED
@@ -1,33 +1,26 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { notFound } from 'next/navigation.js';
3
- import { Fragment } from 'react';
4
- import { PageNotFoundError, routeFromNextParams, getRenderInput, getLayout, getFragment, } from './server.js';
5
- import { routeMetadataForContent, resolveSlots, } from '../content/index.js';
6
- import { renderBlocks } from './render.js';
3
+ import { PageNotFoundError, routeFromNextParams, getRenderInput, } from './server.js';
4
+ import { routeMetadataForContent, } from '../content/index.js';
7
5
  /**
8
6
  * Creates the catch-all route handler for a Next.js ACI site.
9
7
  *
10
8
  * Usage in `src/app/[[...slug]]/page.tsx`:
11
9
  *
12
10
  * ```tsx
13
- * import { createPage } from '@gradial/aci/next/page';
14
- * import { registry } from '@/cms/registry';
11
+ * import { createAciPage } from '@gradial/aci/next/page';
12
+ * import { CmsPage } from '@/components/CmsPage';
15
13
  *
16
- * const page = createPage(registry);
14
+ * const page = createAciPage({ CmsPage });
17
15
  * export const dynamic = page.dynamic;
18
16
  * export const generateMetadata = page.generateMetadata;
19
17
  * export default page.default;
20
18
  * ```
21
19
  *
22
20
  * The SDK owns param normalization, content loading, metadata generation,
23
- * notFound handling, layout resolution, and block rendering via the registry.
24
- *
25
- * Layout slots: createPage loads the page's layout, iterates all slots, and
26
- * fills each from the page's regions. If a slot is empty, it falls back to
27
- * the layout's fragment defaults (loaded via getFragment). This lets chrome
28
- * be CMS-managed fragments instead of hardcoded component imports.
21
+ * and notFound handling. The site only provides its CmsPage component.
29
22
  */
30
- export function createPage(registry, options) {
23
+ export function createAciPage({ CmsPage }) {
31
24
  const dynamic = 'force-dynamic';
32
25
  async function generateMetadata({ params }) {
33
26
  const route = await routeFromNextParams(params);
@@ -50,18 +43,7 @@ export function createPage(registry, options) {
50
43
  const input = await getRenderInput(route);
51
44
  if (!input.page || input.page.status !== 'published')
52
45
  notFound();
53
- const layout = await getLayout(input.page.layout);
54
- const resolvedSlots = await resolveSlots(layout, input.page.regions || {}, (id) => getFragment(id));
55
- return (_jsx(_Fragment, { children: layout.slots.map((slot) => {
56
- const blocks = resolvedSlots[slot.name];
57
- if (!blocks?.length)
58
- return null;
59
- const extraProps = options?.slotProps?.(slot.name, input.page);
60
- if (slot.name === 'main') {
61
- return (_jsx("main", { id: "content-main", children: renderBlocks(blocks, registry, extraProps) }, "main"));
62
- }
63
- return (_jsx(Fragment, { children: renderBlocks(blocks, registry, extraProps) }, slot.name));
64
- }) }));
46
+ return _jsx(CmsPage, { page: input.page, siteConfig: input.siteConfig });
65
47
  }
66
48
  catch (error) {
67
49
  if (error instanceof PageNotFoundError)
@@ -0,0 +1,4 @@
1
+ export interface PreviewBannerProps {
2
+ releaseId?: string;
3
+ }
4
+ export declare function PreviewBanner({ releaseId }: PreviewBannerProps): import("react").JSX.Element | null;
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function PreviewBanner({ releaseId }) {
3
+ if (!releaseId)
4
+ return null;
5
+ const shortId = releaseId.length > 20
6
+ ? `${releaseId.slice(0, 8)}\u2026${releaseId.slice(-6)}`
7
+ : releaseId;
8
+ return (_jsxs("div", { role: "alert", style: {
9
+ position: 'fixed',
10
+ top: 0,
11
+ left: 0,
12
+ right: 0,
13
+ zIndex: 9999,
14
+ display: 'flex',
15
+ alignItems: 'center',
16
+ justifyContent: 'center',
17
+ gap: '12px',
18
+ padding: '8px 16px',
19
+ backgroundColor: '#1a1a2e',
20
+ color: '#fff',
21
+ fontSize: '13px',
22
+ fontFamily: 'system-ui, -apple-system, sans-serif',
23
+ boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
24
+ }, children: [_jsx("span", { "aria-hidden": "true", style: {
25
+ display: 'inline-flex',
26
+ alignItems: 'center',
27
+ justifyContent: 'center',
28
+ width: '18px',
29
+ height: '18px',
30
+ borderRadius: '50%',
31
+ backgroundColor: '#f59e0b',
32
+ color: '#1a1a2e',
33
+ fontWeight: 700,
34
+ fontSize: '11px',
35
+ flexShrink: 0,
36
+ }, children: "P" }), _jsx("span", { style: { fontWeight: 500 }, children: "Release Preview" }), _jsx("span", { style: {
37
+ fontFamily: 'monospace',
38
+ opacity: 0.7,
39
+ fontSize: '12px',
40
+ }, title: releaseId, children: shortId }), _jsx("a", { href: "?leave_preview=1", style: {
41
+ marginLeft: 'auto',
42
+ padding: '4px 12px',
43
+ borderRadius: '6px',
44
+ backgroundColor: '#dc2626',
45
+ color: '#fff',
46
+ textDecoration: 'none',
47
+ fontSize: '12px',
48
+ fontWeight: 600,
49
+ whiteSpace: 'nowrap',
50
+ }, children: "Exit Preview" })] }));
51
+ }
@@ -0,0 +1,7 @@
1
+ export interface PreviewClaims {
2
+ releaseId?: string;
3
+ siteId?: string;
4
+ expiresAt?: string;
5
+ scope?: string;
6
+ }
7
+ export declare function validatePreviewToken(token: string, signKey: string): Promise<PreviewClaims | null>;
@@ -0,0 +1,37 @@
1
+ export async function validatePreviewToken(token, signKey) {
2
+ try {
3
+ const parts = token.split('.');
4
+ if (parts.length !== 3)
5
+ return null;
6
+ const signingInput = `${parts[0]}.${parts[1]}`;
7
+ const signature = base64URLToBytes(parts[2]);
8
+ const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(signKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
9
+ const expected = new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(signingInput)));
10
+ if (!constantTimeEqual(signature, expected))
11
+ return null;
12
+ const payload = new TextDecoder().decode(base64URLToBytes(parts[1]));
13
+ return JSON.parse(payload);
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ function base64URLToBytes(value) {
20
+ const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
21
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
22
+ const binary = atob(padded);
23
+ const bytes = new Uint8Array(binary.length);
24
+ for (let i = 0; i < binary.length; i++) {
25
+ bytes[i] = binary.charCodeAt(i);
26
+ }
27
+ return bytes;
28
+ }
29
+ function constantTimeEqual(left, right) {
30
+ if (left.length !== right.length)
31
+ return false;
32
+ let diff = 0;
33
+ for (let i = 0; i < left.length; i++) {
34
+ diff |= left[i] ^ right[i];
35
+ }
36
+ return diff === 0;
37
+ }
@@ -1,6 +1,6 @@
1
- import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError } from '../content/provider.js';
1
+ import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
2
2
  import type { RenderInput } from '../content/types.js';
3
- export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
3
+ export { FragmentNotFoundError, PageNotFoundError };
4
4
  export interface GradialFetchConfig {
5
5
  siteId?: string;
6
6
  bucket?: string;
@@ -29,7 +29,6 @@ export interface GradialSiteConfig {
29
29
  export declare function getPage<T = unknown>(route?: string, config?: GradialFetchConfig): Promise<T>;
30
30
  export declare function getSiteConfig<T extends GradialSiteConfig = GradialSiteConfig>(config?: GradialFetchConfig): Promise<T>;
31
31
  export declare function getFragment<T = unknown>(name: string, config?: GradialFetchConfig): Promise<T>;
32
- export declare function getLayout<T = unknown>(name: string, config?: GradialFetchConfig): Promise<T>;
33
32
  export declare function getRoutes(config?: GradialFetchConfig): Promise<string[]>;
34
33
  export declare function getRenderInput<TPage, TSite extends GradialSiteConfig = GradialSiteConfig>(route?: string, config?: GradialFetchConfig): Promise<GradialRenderInput<TPage, TSite>>;
35
34
  export declare function getPageRuntimeRenderInput(): Promise<RenderInput | null>;