@gradial/aci 0.1.39 → 0.1.40

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 (31) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/consent-banner/index.d.ts +33 -0
  3. package/dist/consent-banner/index.js +99 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/react/VideoPlayer.d.ts +2 -2
  7. package/dist/react/index.d.ts +2 -2
  8. package/package.json +26 -15
  9. package/src/astro/ConsentBanner.astro +62 -0
  10. package/src/cli/validate-content.mjs +0 -0
  11. package/src/consent-banner/styles.css +110 -0
  12. package/templates/astro/template/.aci.yaml +28 -0
  13. package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
  14. package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
  15. package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
  16. package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
  17. package/templates/common/.content/config/robots.txt +2 -0
  18. package/templates/common/.content/config/site.json +197 -0
  19. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +9 -0
  20. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +14 -0
  21. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +23 -0
  22. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +37 -0
  23. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +9 -0
  24. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +14 -0
  25. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +23 -0
  26. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +37 -0
  27. package/templates/common/.content/fragments/navbar/_index.json +25 -0
  28. package/templates/common/.content/fragments/site-footer/_index.json +62 -0
  29. package/templates/common/.content/pages/about/_index.json +51 -0
  30. package/templates/nextjs/template/.aci.yaml +26 -0
  31. package/templates/nextjs/template/src/middleware.ts +7 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## 0.1.40 - 2026-07-21
4
+
5
+ ### Added
6
+
7
+ - First-party consent banner component (`mountConsentBanner`) for sites without
8
+ an external CMP. Checks for the `aci_consent` cookie, renders a banner with
9
+ accept/reject buttons, and calls `applyConsent()` on user decision.
10
+ - `ConsentBanner.astro` wrapper component for Astro sites, importable from
11
+ `@gradial/aci/astro/ConsentBanner`.
12
+ - Base CSS styles for the consent banner in `@gradial/aci/consent-banner/styles.css`.
13
+ Sites override with their own CSS.
14
+
15
+ ## 0.1.34 - 2026-07-20
16
+
17
+ ### Fixed
18
+
19
+ - Preserve layout-default fragment references during `aci content compile`, so
20
+ shared site chrome such as navigation and footers is not silently omitted.
21
+ - Reject malformed or dangling layout-default fragment references before local
22
+ compilation or managed publishing can produce or stage invalid artifacts.
23
+
24
+ After upgrading, delete any `.aci/compiled` directory produced by an older
25
+ compiler and rerun `aci content compile`. Existing compiled output is derived
26
+ state and is not repaired in place.
@@ -0,0 +1,33 @@
1
+ export interface ConsentBannerOptions {
2
+ /** Heading text. Default: "Privacy" */
3
+ heading?: string;
4
+ /** Body text explaining what the user is consenting to. */
5
+ message?: string;
6
+ /** Label for the accept button. Default: "Accept" */
7
+ acceptLabel?: string;
8
+ /** Label for the reject button. Default: "Reject" */
9
+ rejectLabel?: string;
10
+ /** Label for a link to the privacy policy. Optional. */
11
+ privacyPolicyLabel?: string;
12
+ /** URL to the privacy policy. Optional. */
13
+ privacyPolicyHref?: string;
14
+ /** Source identifier written to the cookie envelope. Default: "banner" */
15
+ source?: string;
16
+ /** Callback fired after a consent decision is written. */
17
+ onDecision?: (granted: boolean) => void;
18
+ }
19
+ /**
20
+ * hasConsentCookie returns true when the aci_consent cookie exists (the user
21
+ * has already made a decision, regardless of whether they granted or denied).
22
+ */
23
+ export declare function hasConsentCookie(doc: {
24
+ cookie: string;
25
+ }): boolean;
26
+ /**
27
+ * mountConsentBanner checks for an existing consent cookie. If none exists,
28
+ * it injects a banner into the document body and wires up accept/reject
29
+ * buttons. If a cookie already exists, it does nothing (idempotent).
30
+ *
31
+ * Returns a cleanup function that removes the banner and event listeners.
32
+ */
33
+ export declare function mountConsentBanner(doc: Document, opts?: ConsentBannerOptions): () => void;
@@ -0,0 +1,99 @@
1
+ // First-party consent banner (design/04, preset="manual"): a self-contained
2
+ // client-side component for sites that don't have an external CMP (c15t,
3
+ // OneTrust, etc.). Checks for the aci_consent cookie, shows a banner if
4
+ // absent, and calls applyConsent() on user decision.
5
+ //
6
+ // The banner is framework-agnostic — it renders plain DOM and is driven by
7
+ // CSS classes so each site can style it. The Astro wrapper component
8
+ // (packages/sdk/src/astro/ConsentBanner.astro) imports and invokes this,
9
+ // then applies site-specific styles.
10
+ import { applyConsent, readConsentCookie, PERSONALIZATION_PURPOSE, } from "../collect/consent/envelope.js";
11
+ const DEFAULTS = {
12
+ heading: "Privacy",
13
+ message: "We use cookies to personalize content and improve your experience. You can accept or reject personalization.",
14
+ acceptLabel: "Accept",
15
+ rejectLabel: "Reject",
16
+ source: "banner",
17
+ };
18
+ const BANNER_ID = "aci-consent-banner";
19
+ const BANNER_ATTR = "data-aci-consent-banner";
20
+ const BANNER_HIDDEN = "aci-consent-banner--hidden";
21
+ /**
22
+ * hasConsentCookie returns true when the aci_consent cookie exists (the user
23
+ * has already made a decision, regardless of whether they granted or denied).
24
+ */
25
+ export function hasConsentCookie(doc) {
26
+ return readConsentCookie(doc) !== undefined;
27
+ }
28
+ /**
29
+ * mountConsentBanner checks for an existing consent cookie. If none exists,
30
+ * it injects a banner into the document body and wires up accept/reject
31
+ * buttons. If a cookie already exists, it does nothing (idempotent).
32
+ *
33
+ * Returns a cleanup function that removes the banner and event listeners.
34
+ */
35
+ export function mountConsentBanner(doc, opts = {}) {
36
+ if (hasConsentCookie(doc))
37
+ return () => { };
38
+ if (doc.getElementById(BANNER_ID))
39
+ return () => { };
40
+ const config = { ...DEFAULTS, ...opts };
41
+ const banner = createBannerElement(config, doc);
42
+ doc.body.appendChild(banner);
43
+ const cleanup = () => {
44
+ banner.remove();
45
+ };
46
+ const decide = (granted) => {
47
+ const purposes = granted ? [PERSONALIZATION_PURPOSE] : [];
48
+ applyConsent(purposes, config.source, { document: doc });
49
+ banner.classList.add(BANNER_HIDDEN);
50
+ setTimeout(cleanup, 300);
51
+ opts.onDecision?.(granted);
52
+ };
53
+ banner.querySelector("[data-aci-consent-accept]")?.addEventListener("click", () => decide(true));
54
+ banner.querySelector("[data-aci-consent-reject]")?.addEventListener("click", () => decide(false));
55
+ return cleanup;
56
+ }
57
+ function createBannerElement(config, doc) {
58
+ const banner = doc.createElement("div");
59
+ banner.id = BANNER_ID;
60
+ banner.setAttribute(BANNER_ATTR, "");
61
+ banner.className = BANNER_ID;
62
+ const content = doc.createElement("div");
63
+ content.className = `${BANNER_ID}__content`;
64
+ const text = doc.createElement("div");
65
+ text.className = `${BANNER_ID}__text`;
66
+ const heading = doc.createElement("p");
67
+ heading.className = `${BANNER_ID}__heading`;
68
+ heading.textContent = config.heading;
69
+ text.appendChild(heading);
70
+ const message = doc.createElement("p");
71
+ message.className = `${BANNER_ID}__message`;
72
+ message.textContent = config.message;
73
+ text.appendChild(message);
74
+ if (config.privacyPolicyHref && config.privacyPolicyLabel) {
75
+ const link = doc.createElement("a");
76
+ link.className = `${BANNER_ID}__privacy-link`;
77
+ link.href = config.privacyPolicyHref;
78
+ link.textContent = config.privacyPolicyLabel;
79
+ text.appendChild(link);
80
+ }
81
+ content.appendChild(text);
82
+ const actions = doc.createElement("div");
83
+ actions.className = `${BANNER_ID}__actions`;
84
+ const rejectBtn = doc.createElement("button");
85
+ rejectBtn.type = "button";
86
+ rejectBtn.className = `${BANNER_ID}__btn ${BANNER_ID}__btn--reject`;
87
+ rejectBtn.setAttribute("data-aci-consent-reject", "");
88
+ rejectBtn.textContent = config.rejectLabel;
89
+ actions.appendChild(rejectBtn);
90
+ const acceptBtn = doc.createElement("button");
91
+ acceptBtn.type = "button";
92
+ acceptBtn.className = `${BANNER_ID}__btn ${BANNER_ID}__btn--accept`;
93
+ acceptBtn.setAttribute("data-aci-consent-accept", "");
94
+ acceptBtn.textContent = config.acceptLabel;
95
+ actions.appendChild(acceptBtn);
96
+ content.appendChild(actions);
97
+ banner.appendChild(content);
98
+ return banner;
99
+ }
package/dist/index.d.ts CHANGED
@@ -15,3 +15,4 @@ export { AssetSchema, isImage, isAsset, } from './types/media.js';
15
15
  export * from './audience-data-models.js';
16
16
  export * from './define-data-model.js';
17
17
  export * from './types/index.js';
18
+ export { mountConsentBanner, hasConsentCookie, type ConsentBannerOptions, } from './consent-banner/index.js';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { AssetSchema, isImage, isAsset, } from './types/media.js';
9
9
  export * from './audience-data-models.js';
10
10
  export * from './define-data-model.js';
11
11
  export * from './types/index.js';
12
+ export { mountConsentBanner, hasConsentCookie, } from './consent-banner/index.js';
12
13
  // Note: content/ is NOT re-exported here because it imports node:path,
13
14
  // making it incompatible with client-side bundlers. Import server-only
14
15
  // content utilities from '@gradial/aci/content' explicitly.
@@ -16,10 +16,10 @@ export declare function VideoPlayer({ video, ...attrs }: VideoPlayerProps): impo
16
16
  title: string;
17
17
  rel?: string | undefined | undefined;
18
18
  content?: string | undefined | undefined;
19
- dir?: string | undefined | undefined;
20
- hidden?: boolean | undefined | undefined;
21
19
  slot?: string | undefined | undefined;
22
20
  style?: import("react").CSSProperties | undefined;
21
+ dir?: string | undefined | undefined;
22
+ hidden?: boolean | undefined | undefined;
23
23
  children?: import("react").ReactNode;
24
24
  className?: string | undefined | undefined;
25
25
  crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
@@ -25,10 +25,10 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
25
25
  rel?: string | undefined | undefined;
26
26
  content?: string | undefined | undefined;
27
27
  referrerPolicy?: import("react").HTMLAttributeReferrerPolicy | undefined;
28
- dir?: string | undefined | undefined;
29
- hidden?: boolean | undefined | undefined;
30
28
  slot?: string | undefined | undefined;
31
29
  style?: import("react").CSSProperties | undefined;
30
+ dir?: string | undefined | undefined;
31
+ hidden?: boolean | undefined | undefined;
32
32
  children?: ReactNode;
33
33
  className?: string | undefined | undefined;
34
34
  crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -9,9 +9,12 @@
9
9
  "bin/aci.js",
10
10
  "src/cli/*.mjs",
11
11
  "src/types/*.ts",
12
+ "src/astro/*.astro",
13
+ "src/consent-banner/*.css",
12
14
  "templates/common",
13
15
  "templates/nextjs",
14
16
  "templates/astro",
17
+ "CHANGELOG.md",
15
18
  "README.md"
16
19
  ],
17
20
  "bin": {
@@ -83,11 +86,26 @@
83
86
  "import": "./dist/astro/index.js",
84
87
  "default": "./dist/astro/index.js"
85
88
  },
89
+ "./astro/ConsentBanner": {
90
+ "import": "./src/astro/ConsentBanner.astro"
91
+ },
92
+ "./consent-banner": {
93
+ "types": "./dist/consent-banner/index.d.ts",
94
+ "import": "./dist/consent-banner/index.js"
95
+ },
86
96
  "./testing": {
87
97
  "types": "./dist/testing/conformance/index.d.ts",
88
98
  "import": "./dist/testing/conformance/index.js"
89
99
  }
90
100
  },
101
+ "scripts": {
102
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
103
+ "test": "node --import tsx --test \"src/**/*.test.ts\"",
104
+ "build:cli": "cd ../.. && ./scripts/build-cli.sh",
105
+ "generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
106
+ "compile-registry": "tsx src/cli/compile-registry.mjs",
107
+ "prepack": "npm run generate-starters && npm run build && node scripts/ensure-bin-modes.mjs"
108
+ },
91
109
  "dependencies": {
92
110
  "markdown-it": "^14.1.0",
93
111
  "sanitize-html": "^2.13.1",
@@ -127,18 +145,11 @@
127
145
  "zod": "^4.0.0"
128
146
  },
129
147
  "optionalDependencies": {
130
- "@gradial/aci-cli-darwin-arm64": "0.1.39",
131
- "@gradial/aci-cli-darwin-x64": "0.1.39",
132
- "@gradial/aci-cli-linux-arm64": "0.1.39",
133
- "@gradial/aci-cli-linux-x64": "0.1.39",
134
- "@gradial/aci-cli-win32-arm64": "0.1.39",
135
- "@gradial/aci-cli-win32-x64": "0.1.39"
136
- },
137
- "scripts": {
138
- "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
139
- "test": "node --import tsx --test \"src/**/*.test.ts\"",
140
- "build:cli": "cd ../.. && ./scripts/build-cli.sh",
141
- "generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
142
- "compile-registry": "tsx src/cli/compile-registry.mjs"
148
+ "@gradial/aci-cli-darwin-arm64": "0.1.40",
149
+ "@gradial/aci-cli-darwin-x64": "0.1.40",
150
+ "@gradial/aci-cli-linux-arm64": "0.1.40",
151
+ "@gradial/aci-cli-linux-x64": "0.1.40",
152
+ "@gradial/aci-cli-win32-arm64": "0.1.40",
153
+ "@gradial/aci-cli-win32-x64": "0.1.40"
143
154
  }
144
- }
155
+ }
@@ -0,0 +1,62 @@
1
+ ---
2
+ // ConsentBanner.astro — Astro wrapper around the SDK's first-party consent
3
+ // banner. Import this into your layout and pass site-specific text/styles.
4
+ //
5
+ // Usage:
6
+ // import ConsentBanner from '@gradial/aci/astro/ConsentBanner.astro';
7
+ // <ConsentBanner
8
+ // heading="Cookie Preferences"
9
+ // message="We use cookies to personalize your experience."
10
+ // acceptLabel="Accept all"
11
+ // rejectLabel="Reject"
12
+ // privacyPolicyHref="/privacy"
13
+ // privacyPolicyLabel="Privacy Policy"
14
+ // />
15
+
16
+ export interface Props {
17
+ heading?: string;
18
+ message?: string;
19
+ acceptLabel?: string;
20
+ rejectLabel?: string;
21
+ privacyPolicyLabel?: string;
22
+ privacyPolicyHref?: string;
23
+ source?: string;
24
+ }
25
+
26
+ const {
27
+ heading,
28
+ message,
29
+ acceptLabel,
30
+ rejectLabel,
31
+ privacyPolicyLabel,
32
+ privacyPolicyHref,
33
+ source,
34
+ } = Astro.props as Props;
35
+
36
+ const opts = JSON.stringify({
37
+ heading,
38
+ message,
39
+ acceptLabel,
40
+ rejectLabel,
41
+ privacyPolicyLabel,
42
+ privacyPolicyHref,
43
+ source,
44
+ });
45
+ ---
46
+
47
+ <div
48
+ id="aci-consent-banner-mount"
49
+ data-aci-consent-banner-mount
50
+ data-opts={opts}
51
+ >
52
+ </div>
53
+
54
+ <script>
55
+ import { mountConsentBanner } from '../consent-banner/index.js';
56
+
57
+ const mount = document.getElementById('aci-consent-banner-mount');
58
+ if (mount) {
59
+ const opts = JSON.parse(mount.getAttribute('data-opts') || '{}');
60
+ mountConsentBanner(document, opts);
61
+ }
62
+ </script>
File without changes
@@ -0,0 +1,110 @@
1
+ /* Base consent banner styles — sites override with their own CSS. */
2
+ /* The banner is injected into document.body by mountConsentBanner(). */
3
+
4
+ #aci-consent-banner {
5
+ position: fixed;
6
+ bottom: 0;
7
+ left: 0;
8
+ right: 0;
9
+ z-index: 9999;
10
+ background: #1a1a2e;
11
+ color: #fff;
12
+ font-family: system-ui, -apple-system, sans-serif;
13
+ font-size: 14px;
14
+ line-height: 1.5;
15
+ transition: opacity 0.3s ease, transform 0.3s ease;
16
+ opacity: 1;
17
+ transform: translateY(0);
18
+ }
19
+
20
+ #aci-consent-banner.aci-consent-banner--hidden {
21
+ opacity: 0;
22
+ transform: translateY(100%);
23
+ pointer-events: none;
24
+ }
25
+
26
+ #aci-consent-banner .aci-consent-banner__content {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ gap: 16px;
31
+ max-width: 1200px;
32
+ margin: 0 auto;
33
+ padding: 16px 24px;
34
+ }
35
+
36
+ #aci-consent-banner .aci-consent-banner__text {
37
+ flex: 1;
38
+ min-width: 0;
39
+ }
40
+
41
+ #aci-consent-banner .aci-consent-banner__heading {
42
+ margin: 0 0 4px 0;
43
+ font-size: 16px;
44
+ font-weight: 600;
45
+ }
46
+
47
+ #aci-consent-banner .aci-consent-banner__message {
48
+ margin: 0;
49
+ font-size: 13px;
50
+ opacity: 0.85;
51
+ }
52
+
53
+ #aci-consent-banner .aci-consent-banner__privacy-link {
54
+ margin-left: 8px;
55
+ color: #8ecae6;
56
+ text-decoration: underline;
57
+ font-size: 13px;
58
+ }
59
+
60
+ #aci-consent-banner .aci-consent-banner__actions {
61
+ display: flex;
62
+ gap: 8px;
63
+ flex-shrink: 0;
64
+ }
65
+
66
+ #aci-consent-banner .aci-consent-banner__btn {
67
+ padding: 8px 20px;
68
+ border: none;
69
+ border-radius: 6px;
70
+ font-size: 14px;
71
+ font-weight: 500;
72
+ cursor: pointer;
73
+ transition: background-color 0.15s ease;
74
+ }
75
+
76
+ #aci-consent-banner .aci-consent-banner__btn--accept {
77
+ background: #2196f3;
78
+ color: #fff;
79
+ }
80
+
81
+ #aci-consent-banner .aci-consent-banner__btn--accept:hover {
82
+ background: #1976d2;
83
+ }
84
+
85
+ #aci-consent-banner .aci-consent-banner__btn--reject {
86
+ background: transparent;
87
+ color: #fff;
88
+ border: 1px solid rgba(255, 255, 255, 0.3);
89
+ }
90
+
91
+ #aci-consent-banner .aci-consent-banner__btn--reject:hover {
92
+ background: rgba(255, 255, 255, 0.1);
93
+ }
94
+
95
+ @media (max-width: 640px) {
96
+ #aci-consent-banner .aci-consent-banner__content {
97
+ flex-direction: column;
98
+ align-items: stretch;
99
+ gap: 12px;
100
+ }
101
+
102
+ #aci-consent-banner .aci-consent-banner__actions {
103
+ flex-direction: column-reverse;
104
+ }
105
+
106
+ #aci-consent-banner .aci-consent-banner__btn {
107
+ width: 100%;
108
+ text-align: center;
109
+ }
110
+ }
@@ -0,0 +1,28 @@
1
+ version: "1"
2
+ siteId: "site_astro"
3
+ domain: "meridian.local"
4
+ framework: astro
5
+
6
+ source:
7
+ root: "./"
8
+ outDir: "dist"
9
+ publicDir: "public"
10
+
11
+ componentRegistry: ./src/cms/contracts/components/index.ts
12
+ layoutRegistry: ./src/cms/contracts/layouts/index.ts
13
+
14
+ capabilities:
15
+ staticRender: true
16
+ ssr: true
17
+ ssrIslands: true
18
+ clientIslands: true
19
+ fragmentRender: true
20
+
21
+ routes:
22
+ cmsManaged: "/[...slug]"
23
+ frameworkOwned:
24
+ - "/api/*"
25
+ - "/_astro/*"
26
+ - "/storybook/*"
27
+
28
+ rendererProtocol: stdio-json
@@ -0,0 +1,2 @@
1
+ User-agent: *
2
+ Allow: /
@@ -0,0 +1,197 @@
1
+ {
2
+ "$type": "site",
3
+ "id": "meridian",
4
+ "status": "published",
5
+ "title": "Meridian",
6
+ "domain": "www.meridian.local",
7
+ "defaultLocale": "en-us",
8
+ "locales": ["en-us"],
9
+ "overlayResolution": ["locale", "audience"],
10
+ "brandHref": "/",
11
+ "brandLogo": {
12
+ "src": "/assets/meridian-logo.svg",
13
+ "alt": "Meridian"
14
+ },
15
+ "seo": {
16
+ "title": "Meridian",
17
+ "description": "Global satellite internet from anywhere on Earth.",
18
+ "siteName": "Meridian"
19
+ },
20
+ "personalization": {
21
+ "collect": true,
22
+ "consentPreset": "c15t",
23
+ "consentCategories": "measurement,marketing"
24
+ },
25
+ "navigation": [
26
+ {
27
+ "id": "home",
28
+ "label": "Home",
29
+ "href": "/"
30
+ },
31
+ {
32
+ "id": "plans",
33
+ "label": "Plans",
34
+ "href": "/plans/"
35
+ },
36
+ {
37
+ "id": "devices",
38
+ "label": "Devices",
39
+ "href": "/devices/"
40
+ },
41
+ {
42
+ "id": "business",
43
+ "label": "Business",
44
+ "href": "/business/"
45
+ },
46
+ {
47
+ "id": "blog",
48
+ "label": "Blog",
49
+ "href": "/blog/"
50
+ },
51
+ {
52
+ "id": "about",
53
+ "label": "About",
54
+ "href": "/about/"
55
+ }
56
+ ],
57
+ "utilities": [
58
+ {
59
+ "id": "sign-in",
60
+ "label": "Sign in",
61
+ "href": "/sign-in/",
62
+ "variant": "ghost"
63
+ },
64
+ {
65
+ "id": "contact-sales",
66
+ "label": "Contact sales",
67
+ "href": "/contact-sales/",
68
+ "variant": "primary"
69
+ }
70
+ ],
71
+ "footer": {
72
+ "columns": [
73
+ {
74
+ "id": "products",
75
+ "title": "Products",
76
+ "links": [
77
+ {
78
+ "id": "plans",
79
+ "label": "Plans",
80
+ "href": "/plans/"
81
+ },
82
+ {
83
+ "id": "devices",
84
+ "label": "Devices",
85
+ "href": "/devices/"
86
+ },
87
+ {
88
+ "id": "coverage",
89
+ "label": "Coverage",
90
+ "href": "/coverage/"
91
+ }
92
+ ]
93
+ },
94
+ {
95
+ "id": "solutions",
96
+ "title": "Solutions",
97
+ "links": [
98
+ {
99
+ "id": "business",
100
+ "label": "Business",
101
+ "href": "/business/"
102
+ },
103
+ {
104
+ "id": "maritime",
105
+ "label": "Maritime",
106
+ "href": "/business/maritime/"
107
+ },
108
+ {
109
+ "id": "aerospace",
110
+ "label": "Aerospace",
111
+ "href": "/business/aerospace/"
112
+ }
113
+ ]
114
+ },
115
+ {
116
+ "id": "resources",
117
+ "title": "Resources",
118
+ "links": [
119
+ {
120
+ "id": "blog",
121
+ "label": "Blog",
122
+ "href": "/blog/"
123
+ },
124
+ {
125
+ "id": "documentation",
126
+ "label": "Documentation",
127
+ "href": "/docs/"
128
+ },
129
+ {
130
+ "id": "support",
131
+ "label": "Support",
132
+ "href": "/support/"
133
+ }
134
+ ]
135
+ },
136
+ {
137
+ "id": "company",
138
+ "title": "Company",
139
+ "links": [
140
+ {
141
+ "id": "about",
142
+ "label": "About us",
143
+ "href": "/about/"
144
+ },
145
+ {
146
+ "id": "careers",
147
+ "label": "Careers",
148
+ "href": "/careers/"
149
+ },
150
+ {
151
+ "id": "contact",
152
+ "label": "Contact",
153
+ "href": "/contact/"
154
+ }
155
+ ]
156
+ }
157
+ ],
158
+ "social": [
159
+ {
160
+ "id": "x",
161
+ "label": "X",
162
+ "href": "https://x.com/meridian",
163
+ "icon": "X"
164
+ },
165
+ {
166
+ "id": "linkedin",
167
+ "label": "LinkedIn",
168
+ "href": "https://www.linkedin.com/company/meridian",
169
+ "icon": "in"
170
+ },
171
+ {
172
+ "id": "youtube",
173
+ "label": "YouTube",
174
+ "href": "https://www.youtube.com/meridian",
175
+ "icon": "YT"
176
+ }
177
+ ],
178
+ "legalLinks": [
179
+ {
180
+ "id": "terms",
181
+ "label": "Terms of Service",
182
+ "href": "/terms/"
183
+ },
184
+ {
185
+ "id": "privacy",
186
+ "label": "Privacy Policy",
187
+ "href": "/privacy/"
188
+ },
189
+ {
190
+ "id": "security",
191
+ "label": "Security",
192
+ "href": "/security/"
193
+ }
194
+ ],
195
+ "copyright": "Copyright (c) 2026 Meridian Satellite Networks, Inc. All rights reserved."
196
+ }
197
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$type": "dam.asset",
3
+ "schemaVersion": 1,
4
+ "assetId": "ast_site_about_hero",
5
+ "createdAt": "2026-06-01T00:00:00Z",
6
+ "createdBy": "local-seed",
7
+ "updatedAt": "2026-06-01T00:00:00Z",
8
+ "updatedBy": "local-seed"
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$type": "dam.editorial",
3
+ "schemaVersion": 1,
4
+ "title": "Meridian about hero",
5
+ "description": "Seeded hero image for the Meridian about page.",
6
+ "altText": "Satellite ground station at twilight",
7
+ "tags": ["meridian", "hero", "ground-station", "satellite"],
8
+ "properties": {
9
+ "source": "unsplash",
10
+ "sourceUrl": "https://images.unsplash.com/photo-1516849841032-87cbac4d88f7?w=1920&q=80"
11
+ },
12
+ "customFields": {},
13
+ "notes": "Downloaded for the local DAM bridge harness."
14
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$type": "dam.governance",
3
+ "schemaVersion": 1,
4
+ "asset": {
5
+ "status": "active",
6
+ "legalHold": false,
7
+ "retention": {
8
+ "policy": "local-seed"
9
+ },
10
+ "publicationRestriction": "none"
11
+ },
12
+ "versions": {
13
+ "v0001": {
14
+ "status": "approved",
15
+ "approvedAt": "2026-06-01T00:00:00Z",
16
+ "approvedBy": "local-seed",
17
+ "license": {
18
+ "kind": "unsplash-local-fixture"
19
+ },
20
+ "publicationRestriction": "none"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "$type": "dam.assetVersion",
3
+ "schemaVersion": 1,
4
+ "versionId": "v0001",
5
+ "blob": {
6
+ "sha256": "fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3",
7
+ "key": ".gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3",
8
+ "byteSize": 14255,
9
+ "mime": "image/jpeg",
10
+ "originalFilename": "about-hero.jpg",
11
+ "checksumSource": "server-computed"
12
+ },
13
+ "technical": {
14
+ "dimensions": {
15
+ "width": 1200,
16
+ "height": 600
17
+ },
18
+ "duration": null,
19
+ "exif": {},
20
+ "iptc": {},
21
+ "xmp": {},
22
+ "video": null,
23
+ "audio": null,
24
+ "document": null,
25
+ "analysis": {
26
+ "saliency": null,
27
+ "faces": [],
28
+ "objects": [],
29
+ "segments": [],
30
+ "textRegions": [],
31
+ "logoRegions": [],
32
+ "safeAreas": []
33
+ }
34
+ },
35
+ "createdAt": "2026-06-01T00:00:00Z",
36
+ "createdBy": "local-seed"
37
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$type": "dam.asset",
3
+ "schemaVersion": 1,
4
+ "assetId": "ast_site_home_hero",
5
+ "createdAt": "2026-06-01T00:00:00Z",
6
+ "createdBy": "local-seed",
7
+ "updatedAt": "2026-06-01T00:00:00Z",
8
+ "updatedBy": "local-seed"
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$type": "dam.editorial",
3
+ "schemaVersion": 1,
4
+ "title": "Meridian home hero",
5
+ "description": "Seeded hero image for the Meridian home page.",
6
+ "altText": "Earth from space with satellite constellation",
7
+ "tags": ["meridian", "hero", "space", "earth"],
8
+ "properties": {
9
+ "source": "unsplash",
10
+ "sourceUrl": "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1920&q=80"
11
+ },
12
+ "customFields": {},
13
+ "notes": "Downloaded for the local DAM bridge harness."
14
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$type": "dam.governance",
3
+ "schemaVersion": 1,
4
+ "asset": {
5
+ "status": "active",
6
+ "legalHold": false,
7
+ "retention": {
8
+ "policy": "local-seed"
9
+ },
10
+ "publicationRestriction": "none"
11
+ },
12
+ "versions": {
13
+ "v0001": {
14
+ "status": "approved",
15
+ "approvedAt": "2026-06-01T00:00:00Z",
16
+ "approvedBy": "local-seed",
17
+ "license": {
18
+ "kind": "unsplash-local-fixture"
19
+ },
20
+ "publicationRestriction": "none"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "$type": "dam.assetVersion",
3
+ "schemaVersion": 1,
4
+ "versionId": "v0001",
5
+ "blob": {
6
+ "sha256": "a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a",
7
+ "key": ".gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a",
8
+ "byteSize": 15601,
9
+ "mime": "image/jpeg",
10
+ "originalFilename": "home-hero.jpg",
11
+ "checksumSource": "server-computed"
12
+ },
13
+ "technical": {
14
+ "dimensions": {
15
+ "width": 1200,
16
+ "height": 600
17
+ },
18
+ "duration": null,
19
+ "exif": {},
20
+ "iptc": {},
21
+ "xmp": {},
22
+ "video": null,
23
+ "audio": null,
24
+ "document": null,
25
+ "analysis": {
26
+ "saliency": null,
27
+ "faces": [],
28
+ "objects": [],
29
+ "segments": [],
30
+ "textRegions": [],
31
+ "logoRegions": [],
32
+ "safeAreas": []
33
+ }
34
+ },
35
+ "createdAt": "2026-06-01T00:00:00Z",
36
+ "createdBy": "local-seed"
37
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$type": "fragment",
3
+ "fragmentId": "navbar",
4
+ "component": "site-navigation",
5
+ "props": {
6
+ "brandHref": "/",
7
+ "brandLabel": "Meridian",
8
+ "brandLogo": {
9
+ "src": "/assets/meridian-logo.svg",
10
+ "alt": "Meridian"
11
+ },
12
+ "navigation": [
13
+ { "id": "home", "label": "Home", "href": "/" },
14
+ { "id": "plans", "label": "Plans", "href": "/plans/" },
15
+ { "id": "devices", "label": "Devices", "href": "/devices/" },
16
+ { "id": "business", "label": "Business", "href": "/business/" },
17
+ { "id": "blog", "label": "Blog", "href": "/blog/" },
18
+ { "id": "about", "label": "About", "href": "/about/" }
19
+ ],
20
+ "utilities": [
21
+ { "id": "sign-in", "label": "Sign in", "href": "/sign-in/", "variant": "ghost" },
22
+ { "id": "contact-sales", "label": "Contact sales", "href": "/contact-sales/", "variant": "primary" }
23
+ ]
24
+ }
25
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "$type": "fragment",
3
+ "fragmentId": "site-footer",
4
+ "component": "site-footer",
5
+ "props": {
6
+ "brandHref": "/",
7
+ "brandLabel": "Meridian",
8
+ "brandLogo": {
9
+ "src": "/assets/meridian-logo.svg",
10
+ "alt": "Meridian"
11
+ },
12
+ "columns": [
13
+ {
14
+ "id": "products",
15
+ "title": "Products",
16
+ "links": [
17
+ { "id": "plans", "label": "Plans", "href": "/plans/" },
18
+ { "id": "devices", "label": "Devices", "href": "/devices/" },
19
+ { "id": "coverage", "label": "Coverage", "href": "/coverage/" }
20
+ ]
21
+ },
22
+ {
23
+ "id": "solutions",
24
+ "title": "Solutions",
25
+ "links": [
26
+ { "id": "business", "label": "Business", "href": "/business/" },
27
+ { "id": "maritime", "label": "Maritime", "href": "/business/maritime/" },
28
+ { "id": "aerospace", "label": "Aerospace", "href": "/business/aerospace/" }
29
+ ]
30
+ },
31
+ {
32
+ "id": "resources",
33
+ "title": "Resources",
34
+ "links": [
35
+ { "id": "blog", "label": "Blog", "href": "/blog/" },
36
+ { "id": "documentation", "label": "Documentation", "href": "/docs/" },
37
+ { "id": "support", "label": "Support", "href": "/support/" }
38
+ ]
39
+ },
40
+ {
41
+ "id": "company",
42
+ "title": "Company",
43
+ "links": [
44
+ { "id": "about", "label": "About us", "href": "/about/" },
45
+ { "id": "careers", "label": "Careers", "href": "/careers/" },
46
+ { "id": "contact", "label": "Contact", "href": "/contact/" }
47
+ ]
48
+ }
49
+ ],
50
+ "social": [
51
+ { "id": "x", "label": "X", "href": "https://x.com/meridian", "icon": "X" },
52
+ { "id": "linkedin", "label": "LinkedIn", "href": "https://www.linkedin.com/company/meridian", "icon": "in" },
53
+ { "id": "youtube", "label": "YouTube", "href": "https://www.youtube.com/meridian", "icon": "YT" }
54
+ ],
55
+ "legalLinks": [
56
+ { "id": "terms", "label": "Terms of Service", "href": "/terms/" },
57
+ { "id": "privacy", "label": "Privacy Policy", "href": "/privacy/" },
58
+ { "id": "security", "label": "Security", "href": "/security/" }
59
+ ],
60
+ "copyright": "Copyright (c) 2026 Meridian Satellite Networks, Inc. All rights reserved."
61
+ }
62
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "$type": "page",
3
+ "id": "meridian-about",
4
+ "status": "published",
5
+ "layout": "default",
6
+ "metadata": {
7
+ "title": "About Meridian",
8
+ "description": "Meridian is building the next generation of satellite internet to connect the unconnected.",
9
+ "canonical": "https://www.meridian.local/about"
10
+ },
11
+ "regions": {
12
+ "main": [
13
+ {
14
+ "id": "about-hero",
15
+ "component": "home_hero",
16
+ "props": {
17
+ "eyebrow": "About us",
18
+ "headline": "Connecting the edges of the world.",
19
+ "description": "Meridian designs, builds, and operates low-orbit satellites and ground terminals that bring reliable broadband to places traditional networks cannot reach.",
20
+ "ctaLabel": "Back to home",
21
+ "ctaHref": "/",
22
+ "image": {
23
+ "$type": "dam.assetRef",
24
+ "assetId": "ast_site_about_hero",
25
+ "alt": "Meridian network coverage map with soft gradient"
26
+ }
27
+ }
28
+ },
29
+ {
30
+ "id": "about-actions",
31
+ "component": "container",
32
+ "props": {
33
+ "className": "items-center justify-center text-center",
34
+ "content": [
35
+ {
36
+ "id": "home-link",
37
+ "component": "button",
38
+ "props": {
39
+ "label": "Return home",
40
+ "href": "/",
41
+ "variant": "primary",
42
+ "icon": "arrow_forward",
43
+ "iconPosition": "after"
44
+ }
45
+ }
46
+ ]
47
+ }
48
+ }
49
+ ]
50
+ }
51
+ }
@@ -0,0 +1,26 @@
1
+ version: "1"
2
+ siteId: "site_nextjs"
3
+ domain: "meridian.local"
4
+ framework: next
5
+
6
+ source:
7
+ root: "./"
8
+
9
+ componentRegistry: ./src/cms/contracts/components/index.ts
10
+ layoutRegistry: ./src/cms/contracts/layouts/index.ts
11
+
12
+ capabilities:
13
+ staticRender: true
14
+ ssr: true
15
+ ssrIslands: true
16
+ clientIslands: true
17
+ fragmentRender: true
18
+
19
+ routes:
20
+ cmsManaged: "/[...slug]"
21
+ frameworkOwned:
22
+ - "/api/*"
23
+ - "/_next/*"
24
+ - "/.gradial-dam/*"
25
+
26
+ rendererProtocol: stdio-json
@@ -0,0 +1,7 @@
1
+ import { createGradialMiddleware } from '@gradial/aci/next/middleware';
2
+
3
+ export default createGradialMiddleware();
4
+
5
+ export const config = {
6
+ matcher: ['/((?!_next(?:/|$)|api(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)'],
7
+ };