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

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 +8 -10
  2. package/bin/aci +0 -0
  3. package/dist/astro/index.d.ts +2 -0
  4. package/dist/astro/index.js +4 -0
  5. package/dist/content/index.d.ts +1 -0
  6. package/dist/content/index.js +1 -0
  7. package/dist/content/provider.d.ts +19 -0
  8. package/dist/content/provider.js +7 -0
  9. package/dist/content/resolve-slots.d.ts +9 -0
  10. package/dist/content/resolve-slots.js +24 -0
  11. package/dist/content/types.d.ts +1 -1
  12. package/dist/content/validation.js +0 -1
  13. package/dist/define-component.js +0 -6
  14. package/dist/define-layout.d.ts +2 -0
  15. package/dist/define-layout.js +3 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/next/asset-route.d.ts +2 -0
  19. package/dist/next/asset-route.js +10 -2
  20. package/dist/next/config.js +1 -1
  21. package/dist/next/middleware.d.ts +6 -2
  22. package/dist/next/middleware.js +7 -2
  23. package/dist/next/page.d.ts +16 -15
  24. package/dist/next/page.js +27 -9
  25. package/dist/next/render.d.ts +5 -0
  26. package/dist/next/render.js +15 -0
  27. package/dist/next/root-layout.d.ts +4 -0
  28. package/dist/next/root-layout.js +5 -0
  29. package/dist/next/server.d.ts +3 -2
  30. package/dist/next/server.js +12 -2
  31. package/dist/providers/file.d.ts +2 -0
  32. package/dist/providers/file.js +61 -1
  33. package/dist/providers/s3.d.ts +1 -0
  34. package/dist/providers/s3.js +14 -1
  35. package/dist/registry.d.ts +15 -0
  36. package/dist/registry.js +10 -0
  37. package/dist/sveltekit/index.d.ts +2 -0
  38. package/dist/sveltekit/index.js +4 -0
  39. package/dist/testing/index.d.ts +3 -0
  40. package/dist/testing/index.js +15 -2
  41. package/dist/types/component.d.ts +0 -9
  42. package/dist/types/config.d.ts +0 -1
  43. package/dist/types/page.d.ts +1 -2
  44. package/dist/types/render-mode.d.ts +0 -1
  45. package/package.json +19 -13
  46. package/src/cli/compile-registry.mjs +132 -12
  47. package/src/types/component.ts +0 -10
  48. package/src/types/config.ts +0 -1
  49. package/src/types/page.ts +1 -2
  50. package/src/types/render-mode.ts +0 -8
  51. package/dist/content/cache.test.d.ts +0 -1
  52. package/dist/content/cache.test.js +0 -86
  53. package/dist/next/preview-banner.d.ts +0 -4
  54. package/dist/next/preview-banner.js +0 -51
  55. package/dist/next/preview.d.ts +0 -7
  56. package/dist/next/preview.js +0 -37
  57. package/src/cli/verify-renderer.mjs +0 -73
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
- npx tsx scripts/create-site.ts --framework astro --name my-site --dir ./my-site
13
+ pnpm exec tsx scripts/create-site.ts --framework astro --name my-site --dir ./my-site
14
14
  cd my-site
15
- npm run dev
15
+ pnpm 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
- npm install @gradial/aci zod@^4
32
- npm install -D typescript tsx
31
+ pnpm add @gradial/aci zod@^4
32
+ pnpm add -D typescript tsx
33
33
  ```
34
34
 
35
35
  Common imports:
@@ -78,7 +78,6 @@ export const heroContract = defineComponentContract({
78
78
  headline: z.string(),
79
79
  image: GradialImageSchema,
80
80
  }),
81
- renderModes: { canStatic: true, canSSR: true, canClientIsland: false },
82
81
  });
83
82
 
84
83
  export type HeroProps = z.infer<typeof heroContract.schema>;
@@ -107,7 +106,6 @@ const content = new FixtureContentProvider({
107
106
  $type: 'page',
108
107
  status: 'published',
109
108
  layout: 'marketing',
110
- renderMode: 'static',
111
109
  metadata: { title: 'Test Home' },
112
110
  regions: { main: [] }
113
111
  }
@@ -123,7 +121,7 @@ Use this in unit tests when you want the same provider API as
123
121
  Publish from this package directory:
124
122
 
125
123
  ```bash
126
- npm publish --access public
124
+ pnpm publish --access public
127
125
  ```
128
126
 
129
127
  The package includes a `prepack` script that builds `dist` before packing or
@@ -131,15 +129,15 @@ publishing.
131
129
 
132
130
  ## Verify the Published Shape Locally
133
131
 
134
- Before publishing, inspect the exact tarball npm will upload. This catches
132
+ Before publishing, inspect the exact tarball pnpm will upload. This catches
135
133
  package `files` mistakes where local source imports work from the repo but fail
136
- after npm filters the packed files.
134
+ after pnpm filters the packed files.
137
135
 
138
136
  From `packages/sdk`:
139
137
 
140
138
  ```bash
141
139
  tmpdir="$(mktemp -d /tmp/gradial-aci-pack.XXXXXX)"
142
- filename="$(npm pack --pack-destination "$tmpdir" | tail -n 1)"
140
+ filename="$(pnpm pack --pack-destination "$tmpdir" | tail -n 1)"
143
141
  tar -tzf "$tmpdir/$filename" | sort | rg '^(package/src/cli/|package/src/types/|package/dist/types/)'
144
142
  echo "tarball=$tmpdir/$filename"
145
143
  ```
package/bin/aci CHANGED
Binary file
@@ -33,6 +33,8 @@ 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';
36
38
  export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, type PreviewContext, type PreviewCookie, } from './preview.js';
37
39
  export interface GradialAstroOptions extends GradialContentWatchOptions {
38
40
  /** Path to compiled content root. Defaults to ACI_CONTENT_ROOT env or '.aci/compiled' */
@@ -32,6 +32,10 @@ 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
+ // ---------------------------------------------------------------------------
35
39
  // Preview support
36
40
  // ---------------------------------------------------------------------------
37
41
  export { getPreviewMode, createPreviewBannerHTML, createPreviewCookies, } from './preview.js';
@@ -2,3 +2,4 @@ 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,3 +2,4 @@ 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,17 +12,33 @@ 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
+ }
15
29
  export interface CompiledManifest {
16
30
  manifestVersion: string;
17
31
  buildDigest: string;
18
32
  siteConfigRef: string;
19
33
  routes: Record<string, RouteEntry>;
20
34
  fragments: Record<string, FragmentEntry>;
35
+ layouts?: Record<string, LayoutEntry>;
21
36
  }
22
37
  export interface ContentProvider {
23
38
  getSiteConfig<T = unknown>(): Promise<T>;
24
39
  getPage<T = unknown>(route: string): Promise<T>;
25
40
  getFragment<T = unknown>(id: string): Promise<T>;
41
+ getLayout<T = unknown>(name: string): Promise<T>;
26
42
  listRoutes(): Promise<RouteEntry[]>;
27
43
  }
28
44
  export declare class PageNotFoundError extends Error {
@@ -31,6 +47,9 @@ export declare class PageNotFoundError extends Error {
31
47
  export declare class FragmentNotFoundError extends Error {
32
48
  constructor(id: string, cause?: unknown);
33
49
  }
50
+ export declare class LayoutNotFoundError extends Error {
51
+ constructor(name: string, cause?: unknown);
52
+ }
34
53
  export interface RenderInputOptions {
35
54
  domain?: string;
36
55
  locale?: string;
@@ -16,6 +16,13 @@ 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
+ }
19
26
  import { getCachedSiteConfig } from './cache.js';
20
27
  export async function loadRenderInput(provider, route = '/', options = {}) {
21
28
  const normalized = normalizeRoute(route);
@@ -0,0 +1,9 @@
1
+ import type { LayoutEntry } from './provider.js';
2
+ import type { KernelPageBlock } from './types.js';
3
+ export interface FragmentData {
4
+ id: string;
5
+ component: string;
6
+ props?: Record<string, unknown>;
7
+ }
8
+ export type FragmentLoader = (id: string) => Promise<FragmentData>;
9
+ export declare function resolveSlots(layout: LayoutEntry, pageRegions: Record<string, KernelPageBlock[]>, getFragment: FragmentLoader): Promise<Record<string, KernelPageBlock[]>>;
@@ -0,0 +1,24 @@
1
+ export async function resolveSlots(layout, pageRegions, getFragment) {
2
+ const resolved = {};
3
+ for (const slot of layout.slots) {
4
+ const pageBlocks = pageRegions[slot.name];
5
+ if (pageBlocks && pageBlocks.length > 0) {
6
+ resolved[slot.name] = pageBlocks;
7
+ continue;
8
+ }
9
+ const defaults = layout.defaults?.[slot.name];
10
+ if (defaults?.length) {
11
+ const blocks = [];
12
+ for (const ref of defaults) {
13
+ const fragment = await getFragment(ref.fragmentId);
14
+ blocks.push({
15
+ id: fragment.id,
16
+ component: fragment.component,
17
+ props: fragment.props || {},
18
+ });
19
+ }
20
+ resolved[slot.name] = blocks;
21
+ }
22
+ }
23
+ return resolved;
24
+ }
@@ -55,7 +55,6 @@ export interface KernelPage {
55
55
  $type: 'page';
56
56
  status: string;
57
57
  layout: string;
58
- renderMode: string;
59
58
  metadata: KernelPageSeo;
60
59
  regions: Record<string, KernelPageBlock[]>;
61
60
  }
@@ -93,6 +92,7 @@ export interface RenderInput<TPage extends KernelPage = KernelPage, TSiteConfig
93
92
  siteConfig: TSiteConfig;
94
93
  page: TPage | null;
95
94
  fragments?: Record<string, unknown>;
95
+ layouts?: Record<string, unknown>;
96
96
  }
97
97
  export interface RenderOutput {
98
98
  route: string;
@@ -121,7 +121,6 @@ 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),
125
124
  metadata: z.looseObject({
126
125
  title: z.string().optional(),
127
126
  description: z.string().optional(),
@@ -1,12 +1,6 @@
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
- }
6
2
  return {
7
3
  ...definition,
8
- renderModes,
9
- render: renderModes,
10
4
  varyDimensions: definition.varyDimensions ?? definition.vary,
11
5
  vary: definition.vary ?? definition.varyDimensions,
12
6
  };
@@ -1,4 +1,6 @@
1
1
  import type { LayoutContract, LayoutDefinition, LayoutSlot } from './types/layout.js';
2
+ import type { FragmentRefNode } from './types/page.js';
2
3
  export declare function defineLayout(definition: LayoutDefinition): LayoutContract;
3
4
  export declare const defineLayoutContract: typeof defineLayout;
4
5
  export declare function slot(name: string, required?: boolean): LayoutSlot;
6
+ export declare function fragmentDefault(fragmentId: string): FragmentRefNode;
@@ -9,3 +9,6 @@ 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,4 +2,5 @@ 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';
5
6
  export * from './types/index.js';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ 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';
5
6
  export * from './types/index.js';
6
7
  // Note: content/ is NOT re-exported here because it imports node:path,
7
8
  // making it incompatible with client-side bundlers. Import server-only
@@ -5,5 +5,7 @@ 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";
8
10
  export declare function GET(_request: Request, { params }: AssetRouteContext): Promise<Response>;
9
11
  export {};
@@ -1,11 +1,19 @@
1
+ import { FileContentProvider } from '../providers/file.js';
1
2
  import { getReleaseAssetResponse } from './server.js';
3
+ const LOCAL_RELEASE_ID = 'local';
4
+ export const runtime = 'nodejs';
5
+ export const dynamic = 'force-dynamic';
2
6
  export async function GET(_request, { params }) {
3
7
  const resolvedParams = await params;
4
8
  const releaseId = resolvedParams.releaseId || '';
5
- const assetPath = (resolvedParams.path || []).join('/');
6
- if (!releaseId || !assetPath) {
9
+ const assetPath = '/' + (resolvedParams.path || []).join('/');
10
+ if (!releaseId || assetPath === '/') {
7
11
  return new Response('asset not found', { status: 404 });
8
12
  }
13
+ if (releaseId === LOCAL_RELEASE_ID) {
14
+ const provider = new FileContentProvider();
15
+ return provider.fetchRaw(assetPath);
16
+ }
9
17
  try {
10
18
  return await getReleaseAssetResponse(releaseId, assetPath);
11
19
  }
@@ -9,7 +9,7 @@ export function withGradialAci(nextConfig = {}, options = {}) {
9
9
  const gradialRewrites = [
10
10
  {
11
11
  source: `${assetPrefix}/:path*`,
12
- destination: `/api/aci-assets${assetPrefix}/:path*`,
12
+ destination: `/gradial/assets/local${assetPrefix}/:path*`,
13
13
  },
14
14
  ];
15
15
  if (!userRewrites)
@@ -1,7 +1,11 @@
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;
5
6
  deploymentId?: string;
6
7
  }
7
- export declare function createGradialMiddleware(config: GradialMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
8
+ export declare function createGradialMiddleware(config?: GradialMiddlewareConfig): (request: NextRequest) => Promise<NextResponse<unknown>>;
9
+ export declare const config: {
10
+ matcher: string[];
11
+ };
@@ -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,6 +38,11 @@ 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
+ };
41
46
  function gradialHeaders(headers) {
42
47
  const next = new Headers(headers);
43
48
  next.delete(RELEASE_HEADER);
@@ -59,7 +64,7 @@ async function resolvePreviewFromRequest(request, config) {
59
64
  console.warn('[gradial/mw] GRADIAL_PREVIEW_SIGN_KEY not set — preview tokens will be ignored');
60
65
  return null;
61
66
  }
62
- return await resolvePreviewContext({ token, siteId: config.siteId, signKey });
67
+ return await resolvePreviewContext({ token, siteId: config.siteId || '', signKey });
63
68
  }
64
69
  function cookieMap(cookies) {
65
70
  const map = new Map();
@@ -1,38 +1,39 @@
1
- import type { ComponentType } from 'react';
2
1
  import type { Metadata } from 'next';
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
- }
2
+ import { type KernelPage } from '../content/index.js';
3
+ import type { Registry } from '../registry.js';
12
4
  type NextPageProps = {
13
5
  params: Promise<{
14
6
  slug?: string[];
15
7
  }>;
16
8
  };
9
+ export type SlotPropsFn = (slotName: string, page: KernelPage) => Record<string, unknown> | undefined;
10
+ export interface CreatePageOptions {
11
+ slotProps?: SlotPropsFn;
12
+ }
17
13
  /**
18
14
  * Creates the catch-all route handler for a Next.js ACI site.
19
15
  *
20
16
  * Usage in `src/app/[[...slug]]/page.tsx`:
21
17
  *
22
18
  * ```tsx
23
- * import { createAciPage } from '@gradial/aci/next/page';
24
- * import { CmsPage } from '@/components/CmsPage';
19
+ * import { createPage } from '@gradial/aci/next/page';
20
+ * import { registry } from '@/cms/registry';
25
21
  *
26
- * const page = createAciPage({ CmsPage });
22
+ * const page = createPage(registry);
27
23
  * export const dynamic = page.dynamic;
28
24
  * export const generateMetadata = page.generateMetadata;
29
25
  * export default page.default;
30
26
  * ```
31
27
  *
32
28
  * The SDK owns param normalization, content loading, metadata generation,
33
- * and notFound handling. The site only provides its CmsPage component.
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.
34
35
  */
35
- export declare function createAciPage({ CmsPage }: CreateAciPageOptions): {
36
+ export declare function createPage(registry: Registry, options?: CreatePageOptions): {
36
37
  dynamic: "force-dynamic";
37
38
  generateMetadata: ({ params }: NextPageProps) => Promise<Metadata>;
38
39
  default: ({ params }: NextPageProps) => Promise<import("react").JSX.Element>;
package/dist/next/page.js CHANGED
@@ -1,26 +1,33 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { notFound } from 'next/navigation.js';
3
- import { PageNotFoundError, routeFromNextParams, getRenderInput, } from './server.js';
4
- import { routeMetadataForContent, } from '../content/index.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';
5
7
  /**
6
8
  * Creates the catch-all route handler for a Next.js ACI site.
7
9
  *
8
10
  * Usage in `src/app/[[...slug]]/page.tsx`:
9
11
  *
10
12
  * ```tsx
11
- * import { createAciPage } from '@gradial/aci/next/page';
12
- * import { CmsPage } from '@/components/CmsPage';
13
+ * import { createPage } from '@gradial/aci/next/page';
14
+ * import { registry } from '@/cms/registry';
13
15
  *
14
- * const page = createAciPage({ CmsPage });
16
+ * const page = createPage(registry);
15
17
  * export const dynamic = page.dynamic;
16
18
  * export const generateMetadata = page.generateMetadata;
17
19
  * export default page.default;
18
20
  * ```
19
21
  *
20
22
  * The SDK owns param normalization, content loading, metadata generation,
21
- * and notFound handling. The site only provides its CmsPage component.
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.
22
29
  */
23
- export function createAciPage({ CmsPage }) {
30
+ export function createPage(registry, options) {
24
31
  const dynamic = 'force-dynamic';
25
32
  async function generateMetadata({ params }) {
26
33
  const route = await routeFromNextParams(params);
@@ -43,7 +50,18 @@ export function createAciPage({ CmsPage }) {
43
50
  const input = await getRenderInput(route);
44
51
  if (!input.page || input.page.status !== 'published')
45
52
  notFound();
46
- return _jsx(CmsPage, { page: input.page, siteConfig: input.siteConfig });
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
+ }) }));
47
65
  }
48
66
  catch (error) {
49
67
  if (error instanceof PageNotFoundError)
@@ -0,0 +1,5 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { Registry } from '../registry.js';
3
+ import type { KernelPageBlock } from '../content/types.js';
4
+ export declare function renderBlock(block: KernelPageBlock, registry: Registry, extraProps?: Record<string, unknown>): ReactElement | null;
5
+ export declare function renderBlocks(blocks: readonly KernelPageBlock[], registry: Registry, extraProps?: Record<string, unknown>): (ReactElement | null)[];
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function renderBlock(block, registry, extraProps) {
3
+ const entry = registry.find((r) => r.name === block.component);
4
+ if (!entry) {
5
+ if (process.env.NODE_ENV !== 'production') {
6
+ return (_jsxs("div", { className: "mx-auto my-4 max-w-[var(--max-width-container)] rounded-card border-2 border-red-600 bg-red-50 px-5 py-4 font-mono text-sm leading-relaxed text-red-900", children: [_jsx("strong", { children: "Unknown block:" }), " ", _jsx("code", { children: block.component }), _jsx("br", {}), _jsxs("span", { className: "text-red-800 opacity-70", children: ["id: ", block.id] })] }, block.id));
7
+ }
8
+ return null;
9
+ }
10
+ const Component = entry.component;
11
+ return _jsx(Component, { ...block.props, ...extraProps }, block.id);
12
+ }
13
+ export function renderBlocks(blocks, registry, extraProps) {
14
+ return blocks.map((block) => renderBlock(block, registry, extraProps));
15
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function RootLayout({ children }: {
3
+ children: ReactNode;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DevRefresh } from './dev-refresh.js';
3
+ export function RootLayout({ children }) {
4
+ return (_jsx("html", { lang: "en", children: _jsxs("body", { children: [children, _jsx(DevRefresh, {})] }) }));
5
+ }
@@ -1,6 +1,6 @@
1
- import { FragmentNotFoundError, PageNotFoundError } from '../content/provider.js';
1
+ import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError } from '../content/provider.js';
2
2
  import type { RenderInput } from '../content/types.js';
3
- export { FragmentNotFoundError, PageNotFoundError };
3
+ export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
4
4
  export interface GradialFetchConfig {
5
5
  siteId?: string;
6
6
  bucket?: string;
@@ -29,6 +29,7 @@ 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>;
32
33
  export declare function getRoutes(config?: GradialFetchConfig): Promise<string[]>;
33
34
  export declare function getRenderInput<TPage, TSite extends GradialSiteConfig = GradialSiteConfig>(route?: string, config?: GradialFetchConfig): Promise<GradialRenderInput<TPage, TSite>>;
34
35
  export declare function getPageRuntimeRenderInput(): Promise<RenderInput | null>;
@@ -1,12 +1,12 @@
1
1
  import { headers } from 'next/headers.js';
2
- import { FragmentNotFoundError, PageNotFoundError, } from '../content/provider.js';
2
+ import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError, } from '../content/provider.js';
3
3
  import { getCachedManifest, getCachedSiteConfig } from '../content/cache.js';
4
4
  import { S3ContentProvider } from '../providers/s3.js';
5
5
  import { GRADIAL_RENDER_INPUT_HEADER, getPendingRenderInput } from '../runtime/page.js';
6
6
  import { getUncachedEdgeConfigValue } from './edge-config.js';
7
7
  const RELEASE_HEADER = 'x-gradial-release-id';
8
8
  const PREVIEW_HEADER = 'x-gradial-preview';
9
- export { FragmentNotFoundError, PageNotFoundError };
9
+ export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
10
10
  export async function getPage(route = '', config = {}) {
11
11
  const runtimeInput = await getPageRuntimeRenderInput();
12
12
  if (runtimeInput)
@@ -31,6 +31,16 @@ export async function getFragment(name, config = {}) {
31
31
  const provider = await resolveProviderWithFallback(config);
32
32
  return await provider.getFragment(name);
33
33
  }
34
+ export async function getLayout(name, config = {}) {
35
+ const runtimeInput = await getPageRuntimeRenderInput();
36
+ if (runtimeInput) {
37
+ const layouts = runtimeInput.layouts || {};
38
+ if (name in layouts)
39
+ return layouts[name];
40
+ }
41
+ const provider = await resolveProviderWithFallback(config);
42
+ return await provider.getLayout(name);
43
+ }
34
44
  export async function getRoutes(config = {}) {
35
45
  const provider = await resolveProviderWithFallback(config);
36
46
  const routes = await provider.listRoutes();
@@ -7,6 +7,8 @@ export declare class FileContentProvider implements ContentProvider {
7
7
  getSiteConfig<T = unknown>(): Promise<T>;
8
8
  getPage<T = unknown>(route: string): Promise<T>;
9
9
  getFragment<T = unknown>(id: string): Promise<T>;
10
+ getLayout<T = unknown>(name: string): Promise<T>;
10
11
  manifest(): Promise<CompiledManifest>;
12
+ fetchRaw(key: string): Promise<Response>;
11
13
  private readJSON;
12
14
  }