@gradial/aci 0.1.46 → 0.1.48

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 (61) hide show
  1. package/README.md +4 -4
  2. package/dist/astro/error-page.d.ts +56 -0
  3. package/dist/astro/error-page.js +79 -0
  4. package/dist/astro/index.d.ts +4 -0
  5. package/dist/astro/index.js +9 -0
  6. package/dist/cli/emit-error-pages.d.mts +39 -0
  7. package/dist/cli/emit-error-pages.mjs +402 -0
  8. package/dist/collect/aci.js +1 -1
  9. package/dist/collect/aci.js.sha256 +1 -1
  10. package/dist/collect/webvitals.d.ts +3 -0
  11. package/dist/collect/webvitals.js +21 -11
  12. package/dist/content/default-error-page-html.d.ts +2 -0
  13. package/dist/content/default-error-page-html.js +6 -0
  14. package/dist/content/default-error-page.d.ts +23 -0
  15. package/dist/content/default-error-page.js +34 -0
  16. package/dist/content/error-page-kind.d.ts +57 -0
  17. package/dist/content/error-page-kind.js +106 -0
  18. package/dist/content/error-pages.d.ts +88 -0
  19. package/dist/content/error-pages.js +120 -0
  20. package/dist/content/index.d.ts +2 -0
  21. package/dist/content/index.js +2 -0
  22. package/dist/content/types.d.ts +12 -0
  23. package/dist/edge/personalization-core.js +6 -2
  24. package/dist/next/config.js +17 -0
  25. package/dist/next/error-page-instrumentation.d.ts +23 -0
  26. package/dist/next/error-page-instrumentation.js +36 -0
  27. package/dist/next/error-page-marker.d.ts +11 -0
  28. package/dist/next/error-page-marker.js +20 -0
  29. package/dist/next/error-pages.d.ts +52 -0
  30. package/dist/next/error-pages.js +94 -0
  31. package/dist/next/error-seat-payload.d.ts +33 -0
  32. package/dist/next/error-seat-payload.js +143 -0
  33. package/dist/next/error-seat.d.ts +57 -0
  34. package/dist/next/error-seat.js +83 -0
  35. package/dist/next/index.d.ts +4 -0
  36. package/dist/next/index.js +8 -0
  37. package/package.json +14 -8
  38. package/src/cli/emit-error-pages.mjs +424 -0
  39. package/templates/astro/template/package.json.tmpl +1 -1
  40. package/templates/astro/template/src/aci/error-page-instrumentation.mjs +33 -0
  41. package/templates/astro/template/src/pages/404.astro +54 -0
  42. package/templates/astro/template/src/pages/500.astro +53 -0
  43. package/templates/astro/template/src/pages/[...slug].astro +15 -2
  44. package/templates/common/.agents/skills/authoring/SKILL.md +71 -0
  45. package/templates/common/.content/config/robots.txt +4 -0
  46. package/templates/common/.content/config/site.json.tmpl +4 -0
  47. package/templates/common/.content/pages/404/_index.json +31 -0
  48. package/templates/common/.content/pages/error/_index.json +31 -0
  49. package/templates/common/.content/schemas/site.schema.json +9 -0
  50. package/templates/common/starter-content/.content/config/robots.txt +2 -0
  51. package/templates/common/starter-content/.content/config/site.json +4 -0
  52. package/templates/common/starter-content/.content/pages/404/_index.json +31 -0
  53. package/templates/common/starter-content/.content/pages/about/_index.json +3 -6
  54. package/templates/common/starter-content/.content/pages/error/_index.json +31 -0
  55. package/templates/common/starter-content/.content/pages/home/_index.json +10 -19
  56. package/templates/common/starter-content/.content/schemas/site.schema.json +9 -0
  57. package/templates/nextjs/template/package.json.tmpl +1 -1
  58. package/templates/nextjs/template/src/aci/error-page-instrumentation.d.mts +4 -0
  59. package/templates/nextjs/template/src/aci/error-page-instrumentation.mjs +38 -0
  60. package/templates/nextjs/template/src/app/error.tsx +18 -0
  61. package/templates/nextjs/template/src/app/not-found.tsx +13 -0
@@ -0,0 +1,33 @@
1
+ // Error-page analytics policy — THIS FILE IS YOURS (customer-owned;
2
+ // code-organization R-14 / authorable-error-pages D-15). The ACI SDK owns
3
+ // the delivery mechanism (when this runs, serialization, the
4
+ // data-aci-error-page marker); this file owns WHAT is pushed.
5
+ //
6
+ // Contract:
7
+ // - pushErrorPageView MUST stay self-contained: browser globals only — no
8
+ // imports, no closed-over identifiers. The Astro seats and the 500.html
9
+ // emitter execute it STRINGIFIED via toString(), so anything outside the
10
+ // function body is lost.
11
+ // - Emptying the body to a no-op disables error-page analytics on every
12
+ // surface (404.astro, 500.astro, emitted static copies) — the sanctioned
13
+ // alternative to GTM-container-side suppression.
14
+ // - Plain .mjs on purpose: it is consumed verbatim, never transpiled.
15
+
16
+ /**
17
+ * Pushes one `page_view` event per error-surface view (D-14 default
18
+ * payload). `page_path` is the visitor's requested path, so 404 reports
19
+ * double as a broken-link inventory.
20
+ *
21
+ * @param {'not_found' | 'error'} pageType
22
+ */
23
+ export function pushErrorPageView(pageType) {
24
+ window.dataLayer = window.dataLayer || [];
25
+ window.dataLayer.push({
26
+ event: 'page_view',
27
+ page_type: pageType,
28
+ page_path: window.location.pathname,
29
+ page_location: window.location.href,
30
+ page_referrer: document.referrer || '(direct)',
31
+ page_title: document.title,
32
+ });
33
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ // Astro's native notFound error seat (authorable-error-pages REQ-7).
3
+ // Renders the page bound by site.json#errorPages.notFound through the normal
4
+ // SiteLayout path; static builds emit dist/404.html, which doubles as the
5
+ // direct /404/ route and the error surface (D-11 — the instrumentation on a
6
+ // direct visit is the accepted consequence). If the bound page cannot be
7
+ // loaded, the SDK degrades to its shipped default fragment — this seat never
8
+ // throws.
9
+ import { getErrorPageData, createRenderChildren } from '@gradial/aci/astro';
10
+ import { registry } from '../cms/registry';
11
+ import SiteLayout from '../layouts/SiteLayout.astro';
12
+ import { pushErrorPageView } from '../aci/error-page-instrumentation.mjs';
13
+
14
+ const errorPage = await getErrorPageData(Astro, 'notFound', { pushErrorPageView });
15
+ const data = errorPage.data;
16
+ const renderChildren = createRenderChildren(registry);
17
+ ---
18
+
19
+ <SiteLayout
20
+ title={errorPage.meta.title}
21
+ description={errorPage.meta.description}
22
+ canonical={errorPage.meta.canonical}
23
+ siteName={errorPage.meta.siteName}
24
+ >
25
+ {data ? (
26
+ data.layout.slots.map((slot) => {
27
+ const blocks = data.slots[slot.name];
28
+ if (!blocks?.length) return null;
29
+ const rendered = renderChildren(blocks);
30
+ if (slot.name === 'main') {
31
+ return (
32
+ <main id="content-main">
33
+ {rendered.map((child) => {
34
+ const Comp = child.Component as any;
35
+ return Comp ? <Comp {...child.props} /> : null;
36
+ })}
37
+ </main>
38
+ );
39
+ }
40
+ return rendered.map((child) => {
41
+ const Comp = child.Component as any;
42
+ return Comp ? <Comp {...child.props} /> : null;
43
+ });
44
+ })
45
+ ) : (
46
+ <Fragment set:html={errorPage.fallbackHtml} />
47
+ )}
48
+ <!-- D-14 error-surface analytics: ONE page_view push with the visitor's
49
+ requested path. The push body is site-owned (D-15): the SDK serializes
50
+ src/aci/error-page-instrumentation.mjs into this tag, injected like
51
+ SiteLayout's collectTag; inline scripts execute here — Astro serves
52
+ real HTML documents (unlike the Next flight-rendered seat). -->
53
+ <Fragment set:html={errorPage.instrumentationTag} />
54
+ </SiteLayout>
@@ -0,0 +1,53 @@
1
+ ---
2
+ // Astro's native catchAll (500) error seat (authorable-error-pages REQ-7).
3
+ // Renders the page bound by site.json#errorPages.catchAll through the normal
4
+ // SiteLayout path. Static builds emit dist/500.html for the hosting layer;
5
+ // server output serves this seat when a route throws during on-demand
6
+ // rendering. If the bound page cannot be loaded, the SDK degrades to its
7
+ // shipped default fragment — this seat never throws.
8
+ import { getErrorPageData, createRenderChildren } from '@gradial/aci/astro';
9
+ import { registry } from '../cms/registry';
10
+ import SiteLayout from '../layouts/SiteLayout.astro';
11
+ import { pushErrorPageView } from '../aci/error-page-instrumentation.mjs';
12
+
13
+ const errorPage = await getErrorPageData(Astro, 'catchAll', { pushErrorPageView });
14
+ const data = errorPage.data;
15
+ const renderChildren = createRenderChildren(registry);
16
+ ---
17
+
18
+ <SiteLayout
19
+ title={errorPage.meta.title}
20
+ description={errorPage.meta.description}
21
+ canonical={errorPage.meta.canonical}
22
+ siteName={errorPage.meta.siteName}
23
+ >
24
+ {data ? (
25
+ data.layout.slots.map((slot) => {
26
+ const blocks = data.slots[slot.name];
27
+ if (!blocks?.length) return null;
28
+ const rendered = renderChildren(blocks);
29
+ if (slot.name === 'main') {
30
+ return (
31
+ <main id="content-main">
32
+ {rendered.map((child) => {
33
+ const Comp = child.Component as any;
34
+ return Comp ? <Comp {...child.props} /> : null;
35
+ })}
36
+ </main>
37
+ );
38
+ }
39
+ return rendered.map((child) => {
40
+ const Comp = child.Component as any;
41
+ return Comp ? <Comp {...child.props} /> : null;
42
+ });
43
+ })
44
+ ) : (
45
+ <Fragment set:html={errorPage.fallbackHtml} />
46
+ )}
47
+ <!-- D-14 error-surface analytics: ONE page_view push with the visitor's
48
+ requested path. The push body is site-owned (D-15): the SDK serializes
49
+ src/aci/error-page-instrumentation.mjs into this tag, injected like
50
+ SiteLayout's collectTag; inline scripts execute here — Astro serves
51
+ real HTML documents (unlike the Next flight-rendered seat). -->
52
+ <Fragment set:html={errorPage.instrumentationTag} />
53
+ </SiteLayout>
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { getGradialStaticPaths, getPageData, createRenderChildren } from '@gradial/aci/astro';
2
+ import { getGradialStaticPaths, getPageData, createRenderChildren, PageNotFoundError, type PageData } from '@gradial/aci/astro';
3
3
  import { registry } from '../cms/registry';
4
4
  import SiteLayout from '../layouts/SiteLayout.astro';
5
5
 
@@ -7,7 +7,20 @@ export const getStaticPaths = getGradialStaticPaths;
7
7
 
8
8
  // SDK owns route normalization, content loading, metadata, layout/slot
9
9
  // resolution, and collect-tag emission. No FileContentProvider imports.
10
- const data = await getPageData(Astro);
10
+ //
11
+ // Server output: unknown routes raise PageNotFoundError here; rewrite to the
12
+ // 404 seat so misses answer HTTP 404, not 500 (authorable-error-pages D-10).
13
+ // src/pages/404.astro is load-bearing for this rewrite — deleting it turns
14
+ // every miss into a 500.
15
+ // Static output never takes this branch — getStaticPaths only emits known
16
+ // routes.
17
+ let data: PageData;
18
+ try {
19
+ data = await getPageData(Astro);
20
+ } catch (error) {
21
+ if (error instanceof PageNotFoundError) return Astro.rewrite('/404/');
22
+ throw error;
23
+ }
11
24
  const renderChildren = createRenderChildren(registry);
12
25
  ---
13
26
 
@@ -238,6 +238,77 @@ Global elements (navigation, footer, branding, SEO) live in
238
238
  }
239
239
  ```
240
240
 
241
+ ### Error Pages
242
+
243
+ Sites bind their error pages in `site.json` via `errorPages`:
244
+
245
+ ```json
246
+ {
247
+ "errorPages": {
248
+ "notFound": "/404/",
249
+ "catchAll": "/error/"
250
+ }
251
+ }
252
+ ```
253
+
254
+ - Each binding is a route path resolving to an **ordinary content page**
255
+ (`.content/pages/404/_index.json`, `.content/pages/error/_index.json`) —
256
+ authored with normal layouts and components like any other page.
257
+ `notFound` serves unknown routes with HTTP 404; `catchAll` serves
258
+ application errors (500-class).
259
+ - Error pages stay out of discovery: set `metadata.sitemap.visible: false`
260
+ on both pages, and the template `robots.txt` disallows both routes.
261
+ - If `errorPages` is absent, the build warns loudly and ships a
262
+ branded-neutral default page (warn-then-enforce migration: absent
263
+ bindings will eventually fail the build, so author them). Present but
264
+ broken bindings — malformed paths, no matching page, unpublished page —
265
+ fail the build immediately.
266
+ - Error pages are base-audience only: no personalization, audience, or
267
+ experiment overlays (validation rejects them), and collect/assignment
268
+ markup is stripped from materialized copies.
269
+ - Analytics: error-surface renders push one `page_view` event to
270
+ `window.dataLayer` with `page_type` (`not_found` | `error`),
271
+ `page_path` (the visitor's requested path), `page_location`,
272
+ `page_referrer`, and `page_title`. The push body is site-owned
273
+ (`src/aci/error-page-instrumentation.mjs` — see the GTM/GA4 section
274
+ below); the SDK wrappers handle delivery.
275
+
276
+ ### Error-page analytics (GTM/GA4)
277
+
278
+ The push body is YOURS: it lives in the site-owned
279
+ `src/aci/error-page-instrumentation.mjs`, whose `pushErrorPageView`
280
+ function every error surface (Next seats, Astro seats, the emitted static
281
+ `500.html`) executes. The scaffolded default pushes one event to
282
+ `window.dataLayer`:
283
+
284
+ ```js
285
+ { event: 'page_view', page_type: 'not_found' | 'error',
286
+ page_path, page_location, page_referrer, page_title }
287
+ ```
288
+
289
+ - **Customize** — edit the function in place. It must stay self-contained
290
+ (browser globals only, no imports, no closed-over identifiers): some
291
+ surfaces execute it stringified.
292
+ - **Disable** — make the function body a no-op. That turns off error-page
293
+ analytics on every surface, and no GTM-side suppression is needed.
294
+
295
+ Container-side dedup is only needed when you KEEP the default push
296
+ alongside automatic GA4 page views: because the event name is `page_view`,
297
+ either disable `send_page_view` and drive all page views from the
298
+ dataLayer, or add a trigger exception on the config tag for error
299
+ surfaces. The stable suppression signal is the `data-aci-error-page`
300
+ attribute (values `not_found` | `error`; the attribute is SDK-owned and
301
+ stable regardless of what your push does): present on the instrumentation
302
+ script tag (Astro, parse-time) and on a server-rendered marker element
303
+ (Next, in the DOM before container load), so container-load triggers can
304
+ gate on it in both frameworks. If you leave automatic page views on and
305
+ also keep the default push, error pages will double-count — that is a
306
+ container configuration choice, not an SDK default.
307
+
308
+ Direct visits to `/404/` and `/error/` do not carry error context and do
309
+ not push the event (Astro static output is the one exception: its
310
+ `404.html` doubles as the direct route).
311
+
241
312
  ### ID Conventions
242
313
 
243
314
  - Every block must have a unique `id` within the page.
@@ -0,0 +1,4 @@
1
+ User-agent: *
2
+ Allow: /
3
+ Disallow: /404/
4
+ Disallow: /error/
@@ -6,6 +6,10 @@
6
6
  "domain": "localhost",
7
7
  "defaultLocale": "en-us",
8
8
  "locales": ["en-us"],
9
+ "errorPages": {
10
+ "notFound": "/404/",
11
+ "catchAll": "/error/"
12
+ },
9
13
  "overlayResolution": ["locale"],
10
14
  "brandHref": "/",
11
15
  "seo": {
@@ -0,0 +1,31 @@
1
+ {
2
+ "$type": "page",
3
+ "id": "meridian-not-found",
4
+ "status": "published",
5
+ "layout": "default",
6
+ "metadata": {
7
+ "title": "Page not found — Meridian",
8
+ "description": "The page you are looking for does not exist.",
9
+ "sitemap": { "visible": false }
10
+ },
11
+ "regions": {
12
+ "main": [
13
+ {
14
+ "id": "not-found-hero",
15
+ "component": "home_hero",
16
+ "props": {
17
+ "eyebrow": "404",
18
+ "headline": "This page drifted out of orbit.",
19
+ "description": "The page you are looking for does not exist or has moved. Check the address, or head back to the homepage.",
20
+ "ctaLabel": "Back to home",
21
+ "ctaHref": "/",
22
+ "image": {
23
+ "$type": "dam.assetRef",
24
+ "assetId": "ast_site_home_hero",
25
+ "alt": "Light abstract background with satellite constellation"
26
+ }
27
+ }
28
+ }
29
+ ]
30
+ }
31
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$type": "page",
3
+ "id": "meridian-error",
4
+ "status": "published",
5
+ "layout": "default",
6
+ "metadata": {
7
+ "title": "Something went wrong — Meridian",
8
+ "description": "An unexpected error occurred while serving this page.",
9
+ "sitemap": { "visible": false }
10
+ },
11
+ "regions": {
12
+ "main": [
13
+ {
14
+ "id": "error-hero",
15
+ "component": "home_hero",
16
+ "props": {
17
+ "eyebrow": "Temporary interruption",
18
+ "headline": "Something went wrong on our end.",
19
+ "description": "An unexpected error occurred while serving this page. Please try again in a moment, or head back to the homepage.",
20
+ "ctaLabel": "Back to home",
21
+ "ctaHref": "/",
22
+ "image": {
23
+ "$type": "dam.assetRef",
24
+ "assetId": "ast_site_home_hero",
25
+ "alt": "Light abstract background with satellite constellation"
26
+ }
27
+ }
28
+ }
29
+ ]
30
+ }
31
+ }
@@ -7,6 +7,15 @@
7
7
  "additionalProperties": true,
8
8
  "properties": {
9
9
  "$type": { "const": "site" },
10
+ "errorPages": {
11
+ "type": "object",
12
+ "required": ["notFound", "catchAll"],
13
+ "additionalProperties": false,
14
+ "properties": {
15
+ "notFound": { "type": "string", "pattern": "^/(?:[A-Za-z0-9._~-]+/)+$" },
16
+ "catchAll": { "type": "string", "pattern": "^/(?:[A-Za-z0-9._~-]+/)+$" }
17
+ }
18
+ },
10
19
  "id": { "type": "string", "minLength": 1 },
11
20
  "status": { "type": "string", "minLength": 1 },
12
21
  "title": { "type": "string", "minLength": 1 },
@@ -1,2 +1,4 @@
1
1
  User-agent: *
2
2
  Allow: /
3
+ Disallow: /404/
4
+ Disallow: /error/
@@ -6,6 +6,10 @@
6
6
  "domain": "www.meridian.local",
7
7
  "defaultLocale": "en-us",
8
8
  "locales": ["en-us"],
9
+ "errorPages": {
10
+ "notFound": "/404/",
11
+ "catchAll": "/error/"
12
+ },
9
13
  "overlayResolution": ["locale", "audience"],
10
14
  "brandHref": "/",
11
15
  "brandLogo": {
@@ -0,0 +1,31 @@
1
+ {
2
+ "$type": "page",
3
+ "id": "meridian-not-found",
4
+ "status": "published",
5
+ "layout": "default",
6
+ "metadata": {
7
+ "title": "Page not found — Meridian",
8
+ "description": "The page you are looking for does not exist.",
9
+ "sitemap": { "visible": false }
10
+ },
11
+ "regions": {
12
+ "main": [
13
+ {
14
+ "id": "not-found-hero",
15
+ "component": "home_hero",
16
+ "props": {
17
+ "eyebrow": "404",
18
+ "headline": "This page drifted out of orbit.",
19
+ "description": "The page you are looking for does not exist or has moved. Check the address, or head back to the homepage.",
20
+ "ctaLabel": "Back to home",
21
+ "ctaHref": "/",
22
+ "image": {
23
+ "$type": "dam.assetRef",
24
+ "assetId": "ast_site_home_hero",
25
+ "alt": "Light abstract background with satellite constellation"
26
+ }
27
+ }
28
+ }
29
+ ]
30
+ }
31
+ }
@@ -24,8 +24,7 @@
24
24
  "assetId": "ast_site_about_hero",
25
25
  "alt": "Meridian network coverage map with soft gradient"
26
26
  }
27
- },
28
- "$type": "component"
27
+ }
29
28
  },
30
29
  {
31
30
  "id": "about-actions",
@@ -42,12 +41,10 @@
42
41
  "variant": "primary",
43
42
  "icon": "arrow_forward",
44
43
  "iconPosition": "after"
45
- },
46
- "$type": "component"
44
+ }
47
45
  }
48
46
  ]
49
- },
50
- "$type": "component"
47
+ }
51
48
  }
52
49
  ]
53
50
  }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$type": "page",
3
+ "id": "meridian-error",
4
+ "status": "published",
5
+ "layout": "default",
6
+ "metadata": {
7
+ "title": "Something went wrong — Meridian",
8
+ "description": "An unexpected error occurred while serving this page.",
9
+ "sitemap": { "visible": false }
10
+ },
11
+ "regions": {
12
+ "main": [
13
+ {
14
+ "id": "error-hero",
15
+ "component": "home_hero",
16
+ "props": {
17
+ "eyebrow": "Temporary interruption",
18
+ "headline": "Something went wrong on our end.",
19
+ "description": "An unexpected error occurred while serving this page. Please try again in a moment, or head back to the homepage.",
20
+ "ctaLabel": "Back to home",
21
+ "ctaHref": "/",
22
+ "image": {
23
+ "$type": "dam.assetRef",
24
+ "assetId": "ast_site_home_hero",
25
+ "alt": "Light abstract background with satellite constellation"
26
+ }
27
+ }
28
+ }
29
+ ]
30
+ }
31
+ }
@@ -4,7 +4,7 @@
4
4
  "status": "published",
5
5
  "layout": "default",
6
6
  "metadata": {
7
- "title": "Meridian — Global Satellite Internet",
7
+ "title": "Meridian \u2014 Global Satellite Internet",
8
8
  "description": "Connect from anywhere on Earth. Low-latency satellite internet for homes, businesses, and beyond.",
9
9
  "canonical": "https://www.meridian.local/"
10
10
  },
@@ -24,8 +24,7 @@
24
24
  "assetId": "ast_site_home_hero",
25
25
  "alt": "Light abstract hero background with satellite constellation"
26
26
  }
27
- },
28
- "$type": "component"
27
+ }
29
28
  },
30
29
  {
31
30
  "id": "why-meridian",
@@ -41,8 +40,7 @@
41
40
  "icon": "globe",
42
41
  "title": "Global coverage",
43
42
  "description": "A constellation of low-orbit satellites keeps you connected across continents, oceans, and remote terrain."
44
- },
45
- "$type": "component"
43
+ }
46
44
  },
47
45
  {
48
46
  "id": "low-latency",
@@ -51,8 +49,7 @@
51
49
  "icon": "speed",
52
50
  "title": "Low latency",
53
51
  "description": "Sub-50ms latency means video calls, gaming, and real-time control feel local, even in the middle of nowhere."
54
- },
55
- "$type": "component"
52
+ }
56
53
  },
57
54
  {
58
55
  "id": "easy-setup",
@@ -61,12 +58,10 @@
61
58
  "icon": "setup",
62
59
  "title": "Easy setup",
63
60
  "description": "A compact terminal and a phone app. Point at the sky, power on, and get online in minutes."
64
- },
65
- "$type": "component"
61
+ }
66
62
  }
67
63
  ]
68
- },
69
- "$type": "component"
64
+ }
70
65
  },
71
66
  {
72
67
  "id": "stats",
@@ -94,8 +89,7 @@
94
89
  "suffix": ""
95
90
  }
96
91
  ]
97
- },
98
- "$type": "component"
92
+ }
99
93
  },
100
94
  {
101
95
  "id": "cta",
@@ -113,8 +107,7 @@
113
107
  "variant": "primary",
114
108
  "icon": "arrow_forward",
115
109
  "iconPosition": "after"
116
- },
117
- "$type": "component"
110
+ }
118
111
  },
119
112
  {
120
113
  "id": "cta-secondary",
@@ -125,12 +118,10 @@
125
118
  "variant": "outline",
126
119
  "icon": "arrow_forward",
127
120
  "iconPosition": "after"
128
- },
129
- "$type": "component"
121
+ }
130
122
  }
131
123
  ]
132
- },
133
- "$type": "component"
124
+ }
134
125
  }
135
126
  ]
136
127
  }
@@ -7,6 +7,15 @@
7
7
  "additionalProperties": true,
8
8
  "properties": {
9
9
  "$type": { "const": "site" },
10
+ "errorPages": {
11
+ "type": "object",
12
+ "required": ["notFound", "catchAll"],
13
+ "additionalProperties": false,
14
+ "properties": {
15
+ "notFound": { "type": "string", "pattern": "^/(?:[A-Za-z0-9._~-]+/)+$" },
16
+ "catchAll": { "type": "string", "pattern": "^/(?:[A-Za-z0-9._~-]+/)+$" }
17
+ }
18
+ },
10
19
  "id": { "type": "string", "minLength": 1 },
11
20
  "status": { "type": "string", "minLength": 1 },
12
21
  "title": { "type": "string", "minLength": 1 },
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "ACI_CONTENT_PROVIDER=file next dev -H 0.0.0.0",
8
- "build": "next build",
8
+ "build": "node ./node_modules/@gradial/aci/src/cli/emit-error-pages.mjs prepare && next build && node ./node_modules/@gradial/aci/src/cli/emit-error-pages.mjs emit",
9
9
  "preview": "next start -H 0.0.0.0",
10
10
  "typecheck": "tsc --noEmit",
11
11
  "test": "node --import tsx --test tests/**/*.test.ts",
@@ -0,0 +1,4 @@
1
+ // Type surface for the customer-owned policy module beside this file
2
+ // (authorable-error-pages D-15). Edit the .mjs; this declaration only needs
3
+ // touching if you change the function's signature.
4
+ export declare function pushErrorPageView(pageType: 'not_found' | 'error'): void;
@@ -0,0 +1,38 @@
1
+ // Error-page analytics policy — THIS FILE IS YOURS (customer-owned;
2
+ // code-organization R-14 / authorable-error-pages D-15). The ACI SDK owns
3
+ // the delivery mechanism (when this runs, serialization, the
4
+ // data-aci-error-page marker); this file owns WHAT is pushed.
5
+ //
6
+ // Contract:
7
+ // - pushErrorPageView MUST stay self-contained: browser globals only — no
8
+ // imports, no closed-over identifiers. The Astro seats and the 500.html
9
+ // emitter execute it STRINGIFIED via toString(), so anything outside the
10
+ // function body is lost.
11
+ // - Emptying the body to a no-op disables error-page analytics on every
12
+ // surface (Next seats, Astro seats, 500.html) — the sanctioned
13
+ // alternative to GTM-container-side suppression.
14
+ // - Plain .mjs on purpose: it is consumed verbatim, never transpiled.
15
+ //
16
+ // 'use client' (Next only, inert elsewhere): the not-found seat is a server
17
+ // component and can only hand a function to the client-side instrumentation
18
+ // component as a client reference.
19
+ 'use client';
20
+
21
+ /**
22
+ * Pushes one `page_view` event per error-surface view (D-14 default
23
+ * payload). `page_path` is the visitor's requested path, so 404 reports
24
+ * double as a broken-link inventory.
25
+ *
26
+ * @param {'not_found' | 'error'} pageType
27
+ */
28
+ export function pushErrorPageView(pageType) {
29
+ window.dataLayer = window.dataLayer || [];
30
+ window.dataLayer.push({
31
+ event: 'page_view',
32
+ page_type: pageType,
33
+ page_path: window.location.pathname,
34
+ page_location: window.location.href,
35
+ page_referrer: document.referrer || '(direct)',
36
+ page_title: document.title,
37
+ });
38
+ }
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+
3
+ import { ErrorSeat } from '@gradial/aci/next/error-seat';
4
+ import { registry } from '@/cms/registry';
5
+ import { pushErrorPageView } from '../aci/error-page-instrumentation.mjs';
6
+
7
+ // Next's app-error seat (client-only by contract). ErrorSeat renders the
8
+ // page bound by site.json#errorPages.catchAll from content serialized into
9
+ // the bundle at build time — nothing is fetched while the app is failing.
10
+ // The analytics push comes from the site-owned policy module (D-15):
11
+ // edit src/aci/error-page-instrumentation.mjs to customize or disable it.
12
+ //
13
+ // Coverage: render errors below the root layout. NOT covered here: root
14
+ // layout errors (global-error), middleware failures, and platform 5xx —
15
+ // those are served the static public/500.html emitted by the build.
16
+ export default function AppError({ error }: { error: Error & { digest?: string } }) {
17
+ return <ErrorSeat registry={registry} error={error} pushErrorPageView={pushErrorPageView} />;
18
+ }
@@ -0,0 +1,13 @@
1
+ import { createNotFound } from '@gradial/aci/next';
2
+ import { registry } from '@/cms/registry';
3
+ import { pushErrorPageView } from '../aci/error-page-instrumentation.mjs';
4
+
5
+ // Next's native not-found seat: the SDK catch-all invokes notFound() on
6
+ // missing content, and this renders the page bound by
7
+ // site.json#errorPages.notFound with a real HTTP 404. If that page is itself
8
+ // missing at request time, the SDK falls back to its shipped default inline —
9
+ // this seat never recurses and never throws.
10
+ //
11
+ // The analytics push comes from the site-owned policy module (D-15):
12
+ // edit src/aci/error-page-instrumentation.mjs to customize or disable it.
13
+ export default createNotFound(registry, { pushErrorPageView });