@gradial/aci 0.1.52-rc.4 → 0.1.52-rc.5
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.
- package/README.md +4 -4
- package/dist/cli/compile-registry.d.mts +5 -1
- package/dist/cli/compile-registry.mjs +25 -10
- package/dist/cli/validate-content.d.mts +31 -1
- package/dist/cli/validate-content.mjs +50 -12
- package/dist/content/provider.d.ts +0 -1
- package/dist/content/resolve-slots.d.ts +9 -2
- package/dist/content/resolve-slots.js +5 -3
- package/dist/content/types.d.ts +2 -0
- package/dist/next/error-seat-payload.js +1 -1
- package/dist/providers/s3.js +4 -11
- package/dist/types/block-ref.d.ts +2 -0
- package/dist/types/block-ref.js +1 -0
- package/package.json +7 -17
- package/src/cli/compile-registry.mjs +24 -10
- package/src/cli/validate-content.mjs +52 -13
- package/src/types/block-ref.test.ts +45 -0
- package/src/types/block-ref.ts +2 -0
- package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
- package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
- package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
- package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
- package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
- package/templates/common/.content/pages/home/_index.json +10 -0
- package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
- package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
- package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
- package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agentic Content Infrastructure SDK
|
|
2
2
|
|
|
3
|
-
`@gradial/aci` `0.1.52-rc.
|
|
3
|
+
`@gradial/aci` `0.1.52-rc.5` is a pre-1.0 public-surface reset for the ACI
|
|
4
4
|
client SDK. Breaking changes from the old `0.1.x` accidental surface are
|
|
5
5
|
intentional: the root import is framework-neutral, framework runtime behavior is
|
|
6
6
|
behind explicit subpaths, and the first required customer path is React/Next.
|
|
@@ -8,7 +8,7 @@ behind explicit subpaths, and the first required customer path is React/Next.
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npm install @gradial/aci@0.1.52-rc.
|
|
11
|
+
npm install @gradial/aci@0.1.52-rc.5 zod@^4
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Root Import
|
|
@@ -78,13 +78,13 @@ host bundles select and serve immutable release artifacts.
|
|
|
78
78
|
The first RC scaffold path is Next:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
npx @gradial/aci@0.1.52-rc.
|
|
81
|
+
npx @gradial/aci@0.1.52-rc.5 init my-site --template next-starter
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
List available templates:
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
npx @gradial/aci@0.1.52-rc.
|
|
87
|
+
npx @gradial/aci@0.1.52-rc.5 init --list
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
## Personalization: The One-Tag Install
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
2
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
import { ImageSchema } from '../types/image.ts';
|
|
7
8
|
import { VideoSchema } from '../types/video.ts';
|
|
@@ -18,14 +19,16 @@ try {
|
|
|
18
19
|
globalThis.React = React;
|
|
19
20
|
}
|
|
20
21
|
catch { /* react not available — component files without JSX will still work */ }
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
if (isMainModule()) {
|
|
23
|
+
const args = parseArgs(process.argv.slice(2));
|
|
24
|
+
try {
|
|
25
|
+
const result = await compileRegistry(args);
|
|
26
|
+
console.log(JSON.stringify({ success: true, ...result, errors: [] }, null, 2));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
}
|
|
29
32
|
}
|
|
30
33
|
async function compileRegistry(options) {
|
|
31
34
|
const required = ['components', 'layouts', 'output'];
|
|
@@ -440,9 +443,10 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
|
|
|
440
443
|
const defs = {
|
|
441
444
|
baseBlock: {
|
|
442
445
|
type: 'object',
|
|
443
|
-
required: ['id', 'component', 'props'],
|
|
446
|
+
required: ['$type', 'id', 'component', 'props'],
|
|
444
447
|
additionalProperties: true,
|
|
445
448
|
properties: {
|
|
449
|
+
$type: { const: 'component' },
|
|
446
450
|
id: { type: 'string', minLength: 1 },
|
|
447
451
|
component: { type: 'string' },
|
|
448
452
|
props: { type: 'object' },
|
|
@@ -757,3 +761,14 @@ function parseArgs(argv) {
|
|
|
757
761
|
}
|
|
758
762
|
return out;
|
|
759
763
|
}
|
|
764
|
+
function isMainModule() {
|
|
765
|
+
if (!process.argv[1])
|
|
766
|
+
return false;
|
|
767
|
+
try {
|
|
768
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
export { compileRegistry, generatePageSchema };
|
|
@@ -1,2 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export {
|
|
2
|
+
export function validateContent(options: any): Promise<{
|
|
3
|
+
warnings?: any[] | undefined;
|
|
4
|
+
success: boolean;
|
|
5
|
+
pagesValidated: number;
|
|
6
|
+
blocksValidated: number;
|
|
7
|
+
errors: ({
|
|
8
|
+
page: string;
|
|
9
|
+
message: string;
|
|
10
|
+
fragment?: undefined;
|
|
11
|
+
component?: undefined;
|
|
12
|
+
path?: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
fragment: string;
|
|
15
|
+
message: string;
|
|
16
|
+
page?: undefined;
|
|
17
|
+
component?: undefined;
|
|
18
|
+
path?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
fragment: string;
|
|
21
|
+
component: any;
|
|
22
|
+
message: string;
|
|
23
|
+
page?: undefined;
|
|
24
|
+
path?: undefined;
|
|
25
|
+
} | {
|
|
26
|
+
fragment: string;
|
|
27
|
+
component: any;
|
|
28
|
+
path: any;
|
|
29
|
+
message: any;
|
|
30
|
+
page?: undefined;
|
|
31
|
+
})[];
|
|
32
|
+
}>;
|
|
@@ -19,21 +19,25 @@
|
|
|
19
19
|
* Uses the JSON schemas generated by compile-registry — portable, no React/CSS
|
|
20
20
|
* imports needed, same schemas usable by Go backend or other tools.
|
|
21
21
|
*/
|
|
22
|
+
import { realpathSync } from 'node:fs';
|
|
22
23
|
import { readdir, readFile } from 'node:fs/promises';
|
|
23
24
|
import path from 'node:path';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
if (isMainModule()) {
|
|
27
|
+
const args = parseArgs(process.argv.slice(2));
|
|
28
|
+
try {
|
|
29
|
+
const result = await validateContent(args);
|
|
30
|
+
console.log(JSON.stringify(result, null, 2));
|
|
31
|
+
if (!result.success)
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error(JSON.stringify({
|
|
36
|
+
success: false,
|
|
37
|
+
errors: [{ message: String(error?.message ?? error) }],
|
|
38
|
+
}, null, 2));
|
|
29
39
|
process.exitCode = 1;
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
console.error(JSON.stringify({
|
|
33
|
-
success: false,
|
|
34
|
-
errors: [{ message: String(error?.message ?? error) }],
|
|
35
|
-
}, null, 2));
|
|
36
|
-
process.exitCode = 1;
|
|
40
|
+
}
|
|
37
41
|
}
|
|
38
42
|
// ---------------------------------------------------------------------------
|
|
39
43
|
// Main
|
|
@@ -86,6 +90,22 @@ async function validateContent(options) {
|
|
|
86
90
|
const blocks = collectBlocks(pageData);
|
|
87
91
|
for (const block of blocks) {
|
|
88
92
|
blocksValidated++;
|
|
93
|
+
if (!block || typeof block !== 'object') {
|
|
94
|
+
collector.push({
|
|
95
|
+
page: relativePath,
|
|
96
|
+
blockId: '<unknown>',
|
|
97
|
+
message: 'page region entry must be a component block',
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (block.$type !== 'component') {
|
|
102
|
+
collector.push({
|
|
103
|
+
page: relativePath,
|
|
104
|
+
blockId: block.id ?? '<unknown>',
|
|
105
|
+
path: '$type',
|
|
106
|
+
message: 'block "$type" must be "component"',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
89
109
|
if (!block.component) {
|
|
90
110
|
collector.push({
|
|
91
111
|
page: relativePath,
|
|
@@ -348,6 +368,13 @@ function validateNestedSlots(parentBlock, componentMap, schemaMap, path = 'props
|
|
|
348
368
|
issues.push({ path: nestedPath, message: `${parentBlock.component}: block ref child must be a block` });
|
|
349
369
|
continue;
|
|
350
370
|
}
|
|
371
|
+
if (nestedBlock.$type !== 'component') {
|
|
372
|
+
issues.push({
|
|
373
|
+
path: `${nestedPath}.$type`,
|
|
374
|
+
message: `${parentBlock.component}: block ref child "$type" must be "component"`,
|
|
375
|
+
});
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
351
378
|
if (!componentMap.has(nestedBlock.component)) {
|
|
352
379
|
issues.push({ path: `${nestedPath}.component`, message: `${parentBlock.component}: unknown nested component "${nestedBlock.component}"` });
|
|
353
380
|
continue;
|
|
@@ -424,3 +451,14 @@ function parseArgs(argv) {
|
|
|
424
451
|
}
|
|
425
452
|
return out;
|
|
426
453
|
}
|
|
454
|
+
function isMainModule() {
|
|
455
|
+
if (!process.argv[1])
|
|
456
|
+
return false;
|
|
457
|
+
try {
|
|
458
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
export { validateContent };
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { LayoutEntry } from './provider.js';
|
|
2
2
|
import type { KernelPageBlock } from './types.js';
|
|
3
|
-
export interface
|
|
4
|
-
|
|
3
|
+
export interface FragmentBlock {
|
|
4
|
+
$type: 'component';
|
|
5
5
|
component: string;
|
|
6
6
|
props?: Record<string, unknown>;
|
|
7
7
|
}
|
|
8
|
+
export interface FragmentData {
|
|
9
|
+
fragmentId?: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
block?: FragmentBlock;
|
|
12
|
+
component?: string;
|
|
13
|
+
props?: Record<string, unknown>;
|
|
14
|
+
}
|
|
8
15
|
export type FragmentLoader = (id: string) => Promise<FragmentData>;
|
|
9
16
|
export declare function resolveSlots(layout: LayoutEntry, pageRegions: Record<string, KernelPageBlock[]>, getFragment: FragmentLoader): Promise<Record<string, KernelPageBlock[]>>;
|
|
@@ -11,10 +11,12 @@ export async function resolveSlots(layout, pageRegions, getFragment) {
|
|
|
11
11
|
const blocks = [];
|
|
12
12
|
for (const ref of defaults) {
|
|
13
13
|
const fragment = await getFragment(ref.fragmentId);
|
|
14
|
+
const block = fragment.block ?? { component: fragment.component ?? '', props: fragment.props };
|
|
14
15
|
blocks.push({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
$type: 'component',
|
|
17
|
+
id: fragment.id ?? fragment.fragmentId ?? ref.fragmentId,
|
|
18
|
+
component: block.component,
|
|
19
|
+
props: block.props || {},
|
|
18
20
|
});
|
|
19
21
|
}
|
|
20
22
|
resolved[slot.name] = blocks;
|
package/dist/content/types.d.ts
CHANGED
|
@@ -42,11 +42,13 @@ export interface KernelPageSeo {
|
|
|
42
42
|
canonical?: string;
|
|
43
43
|
}
|
|
44
44
|
export interface KernelPageBlock {
|
|
45
|
+
$type: 'component';
|
|
45
46
|
id: string;
|
|
46
47
|
component: string;
|
|
47
48
|
props: Record<string, unknown>;
|
|
48
49
|
}
|
|
49
50
|
export interface KernelAtomicBlock {
|
|
51
|
+
$type: 'component';
|
|
50
52
|
id: string;
|
|
51
53
|
component: string;
|
|
52
54
|
props: Record<string, unknown>;
|
|
@@ -104,7 +104,7 @@ export function buildErrorSeatPayload(contentRoot, posture) {
|
|
|
104
104
|
if (!fragmentRef)
|
|
105
105
|
continue;
|
|
106
106
|
const fragment = readJSON(root, fragmentRef);
|
|
107
|
-
blocks.push({ id: fragment.id, component: fragment.component, props: fragment.props || {} });
|
|
107
|
+
blocks.push({ $type: 'component', id: fragment.id, component: fragment.component, props: fragment.props || {} });
|
|
108
108
|
}
|
|
109
109
|
if (blocks.length)
|
|
110
110
|
slots[slot.name] = blocks;
|
package/dist/providers/s3.js
CHANGED
|
@@ -78,13 +78,12 @@ export class S3ContentProvider {
|
|
|
78
78
|
}
|
|
79
79
|
async getLayout(name) {
|
|
80
80
|
const manifest = await this.manifest();
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
81
|
+
const layouts = manifest.layouts;
|
|
82
|
+
if (!layouts || !layouts[name]) {
|
|
83
83
|
throw new LayoutNotFoundError(name);
|
|
84
84
|
}
|
|
85
|
-
const ref = entry.payloadRef;
|
|
86
85
|
try {
|
|
87
|
-
return await this.getJSON(
|
|
86
|
+
return await this.getJSON(`layouts/${name}.json`);
|
|
88
87
|
}
|
|
89
88
|
catch (error) {
|
|
90
89
|
throw new LayoutNotFoundError(name, error);
|
|
@@ -200,13 +199,7 @@ export class S3ContentProvider {
|
|
|
200
199
|
}
|
|
201
200
|
let page;
|
|
202
201
|
try {
|
|
203
|
-
|
|
204
|
-
if (parsed?.schemaVersion === 'gradial.ssr-page.v1' && parsed.page !== undefined) {
|
|
205
|
-
page = parsed.page;
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
page = parsed;
|
|
209
|
-
}
|
|
202
|
+
page = JSON.parse(source);
|
|
210
203
|
}
|
|
211
204
|
catch (cause) {
|
|
212
205
|
throw pageIntegrityError(`invalid JSON for ${tuple.contentRef}`, cause);
|
|
@@ -5,6 +5,7 @@ export interface BlockRefMeta {
|
|
|
5
5
|
multiple: boolean;
|
|
6
6
|
}
|
|
7
7
|
declare const blockRefBase: z.ZodObject<{
|
|
8
|
+
$type: z.ZodLiteral<"component">;
|
|
8
9
|
id: z.ZodString;
|
|
9
10
|
component: z.ZodString;
|
|
10
11
|
props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
@@ -12,6 +13,7 @@ declare const blockRefBase: z.ZodObject<{
|
|
|
12
13
|
type InferContractProps<T> = T extends ComponentContract<string, infer Schema> ? Schema extends z.ZodType<infer P> ? P : Record<string, unknown> : Record<string, unknown>;
|
|
13
14
|
type InferContractId<T> = T extends ComponentContract<infer Id, unknown> ? Id : never;
|
|
14
15
|
export type BlockRefEntry<T extends ComponentContract> = {
|
|
16
|
+
$type: 'component';
|
|
15
17
|
id: string;
|
|
16
18
|
component: InferContractId<T>;
|
|
17
19
|
props: InferContractProps<T>;
|
package/dist/types/block-ref.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.52-rc.
|
|
3
|
+
"version": "0.1.52-rc.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/gradial/bare-metal-go.git",
|
|
@@ -76,16 +76,6 @@
|
|
|
76
76
|
"import": "./dist/next/error-seat.js",
|
|
77
77
|
"default": "./dist/next/error-seat.js"
|
|
78
78
|
},
|
|
79
|
-
"./next/middleware": {
|
|
80
|
-
"types": "./dist/next/middleware.d.ts",
|
|
81
|
-
"import": "./dist/next/middleware.js",
|
|
82
|
-
"default": "./dist/next/middleware.js"
|
|
83
|
-
},
|
|
84
|
-
"./next/asset-route": {
|
|
85
|
-
"types": "./dist/next/asset-route.d.ts",
|
|
86
|
-
"import": "./dist/next/asset-route.js",
|
|
87
|
-
"default": "./dist/next/asset-route.js"
|
|
88
|
-
},
|
|
89
79
|
"./next/server": {
|
|
90
80
|
"types": "./dist/next/server.d.ts",
|
|
91
81
|
"import": "./dist/next/server.js",
|
|
@@ -156,11 +146,11 @@
|
|
|
156
146
|
"zod": "^4.0.0"
|
|
157
147
|
},
|
|
158
148
|
"optionalDependencies": {
|
|
159
|
-
"@gradial/aci-cli-darwin-arm64": "0.1.52-rc.
|
|
160
|
-
"@gradial/aci-cli-darwin-x64": "0.1.52-rc.
|
|
161
|
-
"@gradial/aci-cli-linux-arm64": "0.1.52-rc.
|
|
162
|
-
"@gradial/aci-cli-linux-x64": "0.1.52-rc.
|
|
163
|
-
"@gradial/aci-cli-win32-arm64": "0.1.52-rc.
|
|
164
|
-
"@gradial/aci-cli-win32-x64": "0.1.52-rc.
|
|
149
|
+
"@gradial/aci-cli-darwin-arm64": "0.1.52-rc.5",
|
|
150
|
+
"@gradial/aci-cli-darwin-x64": "0.1.52-rc.5",
|
|
151
|
+
"@gradial/aci-cli-linux-arm64": "0.1.52-rc.5",
|
|
152
|
+
"@gradial/aci-cli-linux-x64": "0.1.52-rc.5",
|
|
153
|
+
"@gradial/aci-cli-win32-arm64": "0.1.52-rc.5",
|
|
154
|
+
"@gradial/aci-cli-win32-x64": "0.1.52-rc.5"
|
|
165
155
|
}
|
|
166
156
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
2
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
import { ImageSchema } from '../types/image.ts';
|
|
7
8
|
import { VideoSchema } from '../types/video.ts';
|
|
@@ -19,14 +20,15 @@ try {
|
|
|
19
20
|
if (!globalThis.React) globalThis.React = React;
|
|
20
21
|
} catch { /* react not available — component files without JSX will still work */ }
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} catch (error) {
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
if (isMainModule()) {
|
|
24
|
+
const args = parseArgs(process.argv.slice(2));
|
|
25
|
+
try {
|
|
26
|
+
const result = await compileRegistry(args);
|
|
27
|
+
console.log(JSON.stringify({ success: true, ...result, errors: [] }, null, 2));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
}
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
async function compileRegistry(options) {
|
|
@@ -430,9 +432,10 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
|
|
|
430
432
|
const defs = {
|
|
431
433
|
baseBlock: {
|
|
432
434
|
type: 'object',
|
|
433
|
-
required: ['id', 'component', 'props'],
|
|
435
|
+
required: ['$type', 'id', 'component', 'props'],
|
|
434
436
|
additionalProperties: true,
|
|
435
437
|
properties: {
|
|
438
|
+
$type: { const: 'component' },
|
|
436
439
|
id: { type: 'string', minLength: 1 },
|
|
437
440
|
component: { type: 'string' },
|
|
438
441
|
props: { type: 'object' },
|
|
@@ -741,3 +744,14 @@ function parseArgs(argv) {
|
|
|
741
744
|
}
|
|
742
745
|
return out;
|
|
743
746
|
}
|
|
747
|
+
|
|
748
|
+
function isMainModule() {
|
|
749
|
+
if (!process.argv[1]) return false;
|
|
750
|
+
try {
|
|
751
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
|
|
752
|
+
} catch {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export { compileRegistry, generatePageSchema };
|
|
@@ -19,21 +19,24 @@
|
|
|
19
19
|
* Uses the JSON schemas generated by compile-registry — portable, no React/CSS
|
|
20
20
|
* imports needed, same schemas usable by Go backend or other tools.
|
|
21
21
|
*/
|
|
22
|
+
import { realpathSync } from 'node:fs';
|
|
22
23
|
import { readdir, readFile } from 'node:fs/promises';
|
|
23
24
|
import path from 'node:path';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
if (isMainModule()) {
|
|
28
|
+
const args = parseArgs(process.argv.slice(2));
|
|
29
|
+
try {
|
|
30
|
+
const result = await validateContent(args);
|
|
31
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32
|
+
if (!result.success) process.exitCode = 1;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(JSON.stringify({
|
|
35
|
+
success: false,
|
|
36
|
+
errors: [{ message: String(error?.message ?? error) }],
|
|
37
|
+
}, null, 2));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// ---------------------------------------------------------------------------
|
|
@@ -95,6 +98,24 @@ async function validateContent(options) {
|
|
|
95
98
|
for (const block of blocks) {
|
|
96
99
|
blocksValidated++;
|
|
97
100
|
|
|
101
|
+
if (!block || typeof block !== 'object') {
|
|
102
|
+
collector.push({
|
|
103
|
+
page: relativePath,
|
|
104
|
+
blockId: '<unknown>',
|
|
105
|
+
message: 'page region entry must be a component block',
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (block.$type !== 'component') {
|
|
111
|
+
collector.push({
|
|
112
|
+
page: relativePath,
|
|
113
|
+
blockId: block.id ?? '<unknown>',
|
|
114
|
+
path: '$type',
|
|
115
|
+
message: 'block "$type" must be "component"',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
98
119
|
if (!block.component) {
|
|
99
120
|
collector.push({
|
|
100
121
|
page: relativePath,
|
|
@@ -380,6 +401,13 @@ function validateNestedSlots(parentBlock, componentMap, schemaMap, path = 'props
|
|
|
380
401
|
issues.push({ path: nestedPath, message: `${parentBlock.component}: block ref child must be a block` });
|
|
381
402
|
continue;
|
|
382
403
|
}
|
|
404
|
+
if (nestedBlock.$type !== 'component') {
|
|
405
|
+
issues.push({
|
|
406
|
+
path: `${nestedPath}.$type`,
|
|
407
|
+
message: `${parentBlock.component}: block ref child "$type" must be "component"`,
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
383
411
|
|
|
384
412
|
if (!componentMap.has(nestedBlock.component)) {
|
|
385
413
|
issues.push({ path: `${nestedPath}.component`, message: `${parentBlock.component}: unknown nested component "${nestedBlock.component}"` });
|
|
@@ -450,3 +478,14 @@ function parseArgs(argv) {
|
|
|
450
478
|
}
|
|
451
479
|
return out;
|
|
452
480
|
}
|
|
481
|
+
|
|
482
|
+
function isMainModule() {
|
|
483
|
+
if (!process.argv[1]) return false;
|
|
484
|
+
try {
|
|
485
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
|
|
486
|
+
} catch {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export { validateContent };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
import { compileSchema } from '../compiler/zod-to-jsonschema.js';
|
|
6
|
+
import { defineComponentContract } from '../define-component.js';
|
|
7
|
+
import { blockRefArray } from './block-ref.js';
|
|
8
|
+
|
|
9
|
+
const childContract = defineComponentContract({
|
|
10
|
+
id: 'child',
|
|
11
|
+
props: z.object({
|
|
12
|
+
label: z.string(),
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('block references require and accept the component discriminator', () => {
|
|
17
|
+
const schema = blockRefArray([childContract]);
|
|
18
|
+
const typed = {
|
|
19
|
+
$type: 'component',
|
|
20
|
+
id: 'child-1',
|
|
21
|
+
component: 'child',
|
|
22
|
+
props: { label: 'Hello' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
assert.equal(schema.safeParse([typed]).success, true);
|
|
26
|
+
assert.equal(schema.safeParse([{
|
|
27
|
+
id: typed.id,
|
|
28
|
+
component: typed.component,
|
|
29
|
+
props: typed.props,
|
|
30
|
+
}]).success, false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('compiled block-reference schema exposes the component discriminator', () => {
|
|
34
|
+
const compiled = compileSchema(blockRefArray([childContract])) as {
|
|
35
|
+
items?: {
|
|
36
|
+
properties?: Record<string, { const?: string }>;
|
|
37
|
+
required?: string[];
|
|
38
|
+
additionalProperties?: boolean;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
assert.equal(compiled.items?.properties?.$type?.const, 'component');
|
|
43
|
+
assert.equal(compiled.items?.required?.includes('$type'), true);
|
|
44
|
+
assert.equal(compiled.items?.additionalProperties, false);
|
|
45
|
+
});
|
package/src/types/block-ref.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface BlockRefMeta {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const blockRefBase = z.object({
|
|
12
|
+
$type: z.literal('component'),
|
|
12
13
|
id: z.string().min(1),
|
|
13
14
|
component: z.string().min(1),
|
|
14
15
|
props: z.record(z.string(), z.unknown()),
|
|
@@ -21,6 +22,7 @@ type InferContractProps<T> = T extends ComponentContract<string, infer Schema>
|
|
|
21
22
|
type InferContractId<T> = T extends ComponentContract<infer Id, unknown> ? Id : never;
|
|
22
23
|
|
|
23
24
|
export type BlockRefEntry<T extends ComponentContract> = {
|
|
25
|
+
$type: 'component';
|
|
24
26
|
id: string;
|
|
25
27
|
component: InferContractId<T>;
|
|
26
28
|
props: InferContractProps<T>;
|
|
@@ -13,17 +13,18 @@ export const atomicBlockStoryFixtures: AtomicBlockStoryFixture[] = [
|
|
|
13
13
|
storybookId: 'core-atomic-blocks-button--variants',
|
|
14
14
|
title: 'Button / All Variants',
|
|
15
15
|
block: {
|
|
16
|
+
$type: 'component',
|
|
16
17
|
id: 'fixture-button-variants',
|
|
17
18
|
component: 'container',
|
|
18
19
|
props: {
|
|
19
20
|
theme: 'auto',
|
|
20
21
|
blocks: [
|
|
21
|
-
{ id: 'btn-primary', component: 'button', props: { label: 'Primary button', href: '#', variant: 'primary' } },
|
|
22
|
-
{ id: 'btn-secondary', component: 'button', props: { label: 'Secondary button', href: '#', variant: 'secondary' } },
|
|
23
|
-
{ id: 'btn-tertiary', component: 'button', props: { label: 'Tertiary button', href: '#', variant: 'tertiary' } },
|
|
24
|
-
{ id: 'btn-icon-before', component: 'button', props: { label: 'Icon before', href: '#', variant: 'primary', icon: 'arrow-right', iconPosition: 'before' } },
|
|
25
|
-
{ id: 'btn-icon-after', component: 'button', props: { label: 'Icon after', href: '#', variant: 'secondary', icon: 'arrow-right', iconPosition: 'after' } },
|
|
26
|
-
{ id: 'btn-new-window', component: 'button', props: { label: 'Opens in new tab', href: 'https://example.com', variant: 'primary', newWindow: true, icon: 'arrow-right', iconPosition: 'after' } }
|
|
22
|
+
{ $type: 'component', id: 'btn-primary', component: 'button', props: { label: 'Primary button', href: '#', variant: 'primary' } },
|
|
23
|
+
{ $type: 'component', id: 'btn-secondary', component: 'button', props: { label: 'Secondary button', href: '#', variant: 'secondary' } },
|
|
24
|
+
{ $type: 'component', id: 'btn-tertiary', component: 'button', props: { label: 'Tertiary button', href: '#', variant: 'tertiary' } },
|
|
25
|
+
{ $type: 'component', id: 'btn-icon-before', component: 'button', props: { label: 'Icon before', href: '#', variant: 'primary', icon: 'arrow-right', iconPosition: 'before' } },
|
|
26
|
+
{ $type: 'component', id: 'btn-icon-after', component: 'button', props: { label: 'Icon after', href: '#', variant: 'secondary', icon: 'arrow-right', iconPosition: 'after' } },
|
|
27
|
+
{ $type: 'component', id: 'btn-new-window', component: 'button', props: { label: 'Opens in new tab', href: 'https://example.com', variant: 'primary', newWindow: true, icon: 'arrow-right', iconPosition: 'after' } }
|
|
27
28
|
]
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -14,14 +14,15 @@ export const compositionStoryFixtures: CompositionStoryFixture[] = [
|
|
|
14
14
|
storybookId: 'composition-container--buttons',
|
|
15
15
|
title: 'Container / Buttons',
|
|
16
16
|
block: {
|
|
17
|
+
$type: 'component',
|
|
17
18
|
id: 'fixture-container-buttons',
|
|
18
19
|
component: 'container',
|
|
19
20
|
props: {
|
|
20
21
|
theme: 'auto',
|
|
21
22
|
className: 'space-y-4',
|
|
22
23
|
blocks: [
|
|
23
|
-
{ id: 'btn-1', component: 'button', props: { label: 'Primary action', href: '#', variant: 'primary', icon: 'arrow_forward', iconPosition: 'after' } },
|
|
24
|
-
{ id: 'btn-2', component: 'button', props: { label: 'Secondary action', href: '#', variant: 'secondary' } }
|
|
24
|
+
{ $type: 'component', id: 'btn-1', component: 'button', props: { label: 'Primary action', href: '#', variant: 'primary', icon: 'arrow_forward', iconPosition: 'after' } },
|
|
25
|
+
{ $type: 'component', id: 'btn-2', component: 'button', props: { label: 'Secondary action', href: '#', variant: 'secondary' } }
|
|
25
26
|
]
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -14,6 +14,7 @@ export const sectionStoryFixtures: SectionStoryFixture[] = [
|
|
|
14
14
|
storybookId: 'sections-home-hero--default',
|
|
15
15
|
title: 'Home Hero',
|
|
16
16
|
block: {
|
|
17
|
+
$type: 'component',
|
|
17
18
|
id: 'fixture-home-hero',
|
|
18
19
|
component: 'home_hero',
|
|
19
20
|
props: {
|
|
@@ -22,7 +23,7 @@ export const sectionStoryFixtures: SectionStoryFixture[] = [
|
|
|
22
23
|
description: 'One content model, three renderers. Build once in Next.js, Astro, and SvelteKit—then prove parity with shared fixtures.',
|
|
23
24
|
ctaLabel: 'Get started',
|
|
24
25
|
ctaHref: '/docs',
|
|
25
|
-
|
|
26
|
+
image: {
|
|
26
27
|
$type: 'gradial.image',
|
|
27
28
|
assetId: 'fixture_seattle',
|
|
28
29
|
versionId: 'v0001',
|
|
@@ -98,6 +98,7 @@ The `home` slug is special-cased to route `/`. All other slugs map directly.
|
|
|
98
98
|
|
|
99
99
|
```json
|
|
100
100
|
{
|
|
101
|
+
"$type": "component",
|
|
101
102
|
"id": "hero-section",
|
|
102
103
|
"component": "home_hero",
|
|
103
104
|
"props": {
|
|
@@ -109,6 +110,7 @@ The `home` slug is special-cased to route `/`. All other slugs map directly.
|
|
|
109
110
|
```
|
|
110
111
|
|
|
111
112
|
- `component` must match a contract `id` exactly.
|
|
113
|
+
- Every embedded block must declare `"$type": "component"`.
|
|
112
114
|
- `props` must validate against the contract's Zod schema.
|
|
113
115
|
- `id` is unique within the page.
|
|
114
116
|
|
|
@@ -141,12 +143,14 @@ export const containerContract = defineComponentContract({
|
|
|
141
143
|
**Content JSON:**
|
|
142
144
|
```json
|
|
143
145
|
{
|
|
146
|
+
"$type": "component",
|
|
144
147
|
"id": "cta-section",
|
|
145
148
|
"component": "container",
|
|
146
149
|
"props": {
|
|
147
150
|
"className": "space-y-4",
|
|
148
151
|
"content": [
|
|
149
152
|
{
|
|
153
|
+
"$type": "component",
|
|
150
154
|
"id": "primary-cta",
|
|
151
155
|
"component": "button",
|
|
152
156
|
"props": {
|
|
@@ -156,6 +160,7 @@ export const containerContract = defineComponentContract({
|
|
|
156
160
|
}
|
|
157
161
|
},
|
|
158
162
|
{
|
|
163
|
+
"$type": "component",
|
|
159
164
|
"id": "secondary-cta",
|
|
160
165
|
"component": "button",
|
|
161
166
|
"props": {
|
|
@@ -182,6 +187,7 @@ The `button` component is a common atomic block:
|
|
|
182
187
|
|
|
183
188
|
```json
|
|
184
189
|
{
|
|
190
|
+
"$type": "component",
|
|
185
191
|
"id": "cta-button",
|
|
186
192
|
"component": "button",
|
|
187
193
|
"props": {
|
|
@@ -482,6 +488,7 @@ framework.
|
|
|
482
488
|
```json
|
|
483
489
|
// .content/pages/home/_index.json (inside regions.main)
|
|
484
490
|
{
|
|
491
|
+
"$type": "component",
|
|
485
492
|
"id": "features",
|
|
486
493
|
"component": "feature_showcase",
|
|
487
494
|
"props": {
|
|
@@ -570,9 +577,10 @@ export default [
|
|
|
570
577
|
];
|
|
571
578
|
```
|
|
572
579
|
|
|
573
|
-
Fragment content lives in `.content/fragments/{id}/_index.json` and
|
|
574
|
-
|
|
575
|
-
Footer) are registered in the registry
|
|
580
|
+
Fragment content lives in `.content/fragments/{id}/_index.json` and retains
|
|
581
|
+
the resource envelope `"$type": "fragment"` while declaring `component` +
|
|
582
|
+
`props`. Chrome components (Header, Footer) are registered in the registry
|
|
583
|
+
alongside page components.
|
|
576
584
|
|
|
577
585
|
The `layout` field in page JSON is **required** — there is no implicit
|
|
578
586
|
default. Every page must declare which layout it uses.
|
|
@@ -113,12 +113,14 @@ type RenderChildren<T extends readonly BlockRef[]> = (blocks: T) => Promise<Reac
|
|
|
113
113
|
|
|
114
114
|
```json
|
|
115
115
|
{
|
|
116
|
+
"$type": "component",
|
|
116
117
|
"id": "cta-section",
|
|
117
118
|
"component": "container",
|
|
118
119
|
"props": {
|
|
119
120
|
"theme": "dark",
|
|
120
121
|
"blocks": [
|
|
121
122
|
{
|
|
123
|
+
"$type": "component",
|
|
122
124
|
"id": "primary-cta",
|
|
123
125
|
"component": "callout",
|
|
124
126
|
"props": {
|
|
@@ -127,6 +129,7 @@ type RenderChildren<T extends readonly BlockRef[]> = (blocks: T) => Promise<Reac
|
|
|
127
129
|
}
|
|
128
130
|
},
|
|
129
131
|
{
|
|
132
|
+
"$type": "component",
|
|
130
133
|
"id": "heading",
|
|
131
134
|
"component": "heading",
|
|
132
135
|
"props": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"regions": {
|
|
12
12
|
"main": [
|
|
13
13
|
{
|
|
14
|
+
"$type": "component",
|
|
14
15
|
"id": "hero",
|
|
15
16
|
"component": "home_hero",
|
|
16
17
|
"props": {
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
}
|
|
28
29
|
},
|
|
29
30
|
{
|
|
31
|
+
"$type": "component",
|
|
30
32
|
"id": "why-meridian",
|
|
31
33
|
"component": "feature_grid",
|
|
32
34
|
"props": {
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
"subtitle": "Engineered for the edges of the world.",
|
|
35
37
|
"features": [
|
|
36
38
|
{
|
|
39
|
+
"$type": "component",
|
|
37
40
|
"id": "global-coverage",
|
|
38
41
|
"component": "feature_card",
|
|
39
42
|
"props": {
|
|
@@ -43,6 +46,7 @@
|
|
|
43
46
|
}
|
|
44
47
|
},
|
|
45
48
|
{
|
|
49
|
+
"$type": "component",
|
|
46
50
|
"id": "low-latency",
|
|
47
51
|
"component": "feature_card",
|
|
48
52
|
"props": {
|
|
@@ -52,6 +56,7 @@
|
|
|
52
56
|
}
|
|
53
57
|
},
|
|
54
58
|
{
|
|
59
|
+
"$type": "component",
|
|
55
60
|
"id": "easy-setup",
|
|
56
61
|
"component": "feature_card",
|
|
57
62
|
"props": {
|
|
@@ -64,6 +69,7 @@
|
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
71
|
{
|
|
72
|
+
"$type": "component",
|
|
67
73
|
"id": "personalization-choice",
|
|
68
74
|
"component": "personalization_choice",
|
|
69
75
|
"props": {
|
|
@@ -75,6 +81,7 @@
|
|
|
75
81
|
}
|
|
76
82
|
},
|
|
77
83
|
{
|
|
84
|
+
"$type": "component",
|
|
78
85
|
"id": "stats",
|
|
79
86
|
"component": "stats_bar",
|
|
80
87
|
"props": {
|
|
@@ -87,6 +94,7 @@
|
|
|
87
94
|
}
|
|
88
95
|
},
|
|
89
96
|
{
|
|
97
|
+
"$type": "component",
|
|
90
98
|
"id": "cta",
|
|
91
99
|
"component": "container",
|
|
92
100
|
"props": {
|
|
@@ -94,6 +102,7 @@
|
|
|
94
102
|
"className": "items-center justify-center text-center",
|
|
95
103
|
"content": [
|
|
96
104
|
{
|
|
105
|
+
"$type": "component",
|
|
97
106
|
"id": "cta-primary",
|
|
98
107
|
"component": "button",
|
|
99
108
|
"props": {
|
|
@@ -105,6 +114,7 @@
|
|
|
105
114
|
}
|
|
106
115
|
},
|
|
107
116
|
{
|
|
117
|
+
"$type": "component",
|
|
108
118
|
"id": "cta-secondary",
|
|
109
119
|
"component": "button",
|
|
110
120
|
"props": {
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$type": "fragment",
|
|
3
3
|
"fragmentId": "navbar",
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
4
|
+
"block": {
|
|
5
|
+
"$type": "component",
|
|
6
|
+
"component": "site-navigation",
|
|
7
|
+
"props": {
|
|
8
|
+
"brandHref": "/",
|
|
9
|
+
"brandLabel": "Meridian",
|
|
10
|
+
"brandLogo": {
|
|
11
|
+
"src": "/assets/meridian-logo.svg",
|
|
12
|
+
"alt": "Meridian"
|
|
13
|
+
},
|
|
14
|
+
"navigation": [
|
|
15
|
+
{ "id": "home", "label": "Home", "href": "/" },
|
|
16
|
+
{ "id": "plans", "label": "Plans", "href": "/plans/" },
|
|
17
|
+
{ "id": "devices", "label": "Devices", "href": "/devices/" },
|
|
18
|
+
{ "id": "business", "label": "Business", "href": "/business/" },
|
|
19
|
+
{ "id": "blog", "label": "Blog", "href": "/blog/" },
|
|
20
|
+
{ "id": "about", "label": "About", "href": "/about/" }
|
|
21
|
+
],
|
|
22
|
+
"utilities": [
|
|
23
|
+
{ "id": "sign-in", "label": "Sign in", "href": "/sign-in/", "variant": "ghost" },
|
|
24
|
+
{ "id": "contact-sales", "label": "Contact sales", "href": "/contact-sales/", "variant": "primary" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
}
|
|
@@ -1,62 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$type": "fragment",
|
|
3
3
|
"fragmentId": "site-footer",
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
]
|
|
4
|
+
"block": {
|
|
5
|
+
"$type": "component",
|
|
6
|
+
"component": "site-footer",
|
|
7
|
+
"props": {
|
|
8
|
+
"brandHref": "/",
|
|
9
|
+
"brandLabel": "Meridian",
|
|
10
|
+
"brandLogo": {
|
|
11
|
+
"src": "/assets/meridian-logo.svg",
|
|
12
|
+
"alt": "Meridian"
|
|
21
13
|
},
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
14
|
+
"columns": [
|
|
15
|
+
{
|
|
16
|
+
"id": "products",
|
|
17
|
+
"title": "Products",
|
|
18
|
+
"links": [
|
|
19
|
+
{ "id": "plans", "label": "Plans", "href": "/plans/" },
|
|
20
|
+
{ "id": "devices", "label": "Devices", "href": "/devices/" },
|
|
21
|
+
{ "id": "coverage", "label": "Coverage", "href": "/coverage/" }
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "solutions",
|
|
26
|
+
"title": "Solutions",
|
|
27
|
+
"links": [
|
|
28
|
+
{ "id": "business", "label": "Business", "href": "/business/" },
|
|
29
|
+
{ "id": "maritime", "label": "Maritime", "href": "/business/maritime/" },
|
|
30
|
+
{ "id": "aerospace", "label": "Aerospace", "href": "/business/aerospace/" }
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "resources",
|
|
35
|
+
"title": "Resources",
|
|
36
|
+
"links": [
|
|
37
|
+
{ "id": "blog", "label": "Blog", "href": "/blog/" },
|
|
38
|
+
{ "id": "documentation", "label": "Documentation", "href": "/docs/" },
|
|
39
|
+
{ "id": "support", "label": "Support", "href": "/support/" }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "company",
|
|
44
|
+
"title": "Company",
|
|
45
|
+
"links": [
|
|
46
|
+
{ "id": "about", "label": "About us", "href": "/about/" },
|
|
47
|
+
{ "id": "careers", "label": "Careers", "href": "/careers/" },
|
|
48
|
+
{ "id": "contact", "label": "Contact", "href": "/contact/" }
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"social": [
|
|
53
|
+
{ "id": "x", "label": "X", "href": "https://x.com/meridian", "icon": "X" },
|
|
54
|
+
{ "id": "linkedin", "label": "LinkedIn", "href": "https://www.linkedin.com/company/meridian", "icon": "in" },
|
|
55
|
+
{ "id": "youtube", "label": "YouTube", "href": "https://www.youtube.com/meridian", "icon": "YT" }
|
|
56
|
+
],
|
|
57
|
+
"legalLinks": [
|
|
58
|
+
{ "id": "terms", "label": "Terms of Service", "href": "/terms/" },
|
|
59
|
+
{ "id": "privacy", "label": "Privacy Policy", "href": "/privacy/" },
|
|
60
|
+
{ "id": "security", "label": "Security", "href": "/security/" }
|
|
61
|
+
],
|
|
62
|
+
"copyright": "Copyright (c) 2026 Meridian Satellite Networks, Inc. All rights reserved."
|
|
63
|
+
}
|
|
61
64
|
}
|
|
62
65
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"regions": {
|
|
12
12
|
"main": [
|
|
13
13
|
{
|
|
14
|
+
"$type": "component",
|
|
14
15
|
"id": "about-hero",
|
|
15
16
|
"component": "home_hero",
|
|
16
17
|
"props": {
|
|
@@ -27,12 +28,14 @@
|
|
|
27
28
|
}
|
|
28
29
|
},
|
|
29
30
|
{
|
|
31
|
+
"$type": "component",
|
|
30
32
|
"id": "about-actions",
|
|
31
33
|
"component": "container",
|
|
32
34
|
"props": {
|
|
33
35
|
"className": "items-center justify-center text-center",
|
|
34
36
|
"content": [
|
|
35
37
|
{
|
|
38
|
+
"$type": "component",
|
|
36
39
|
"id": "home-link",
|
|
37
40
|
"component": "button",
|
|
38
41
|
"props": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"regions": {
|
|
12
12
|
"main": [
|
|
13
13
|
{
|
|
14
|
+
"$type": "component",
|
|
14
15
|
"id": "hero",
|
|
15
16
|
"component": "home_hero",
|
|
16
17
|
"props": {
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
}
|
|
28
29
|
},
|
|
29
30
|
{
|
|
31
|
+
"$type": "component",
|
|
30
32
|
"id": "why-meridian",
|
|
31
33
|
"component": "feature_grid",
|
|
32
34
|
"props": {
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
"subtitle": "Engineered for the edges of the world.",
|
|
35
37
|
"features": [
|
|
36
38
|
{
|
|
39
|
+
"$type": "component",
|
|
37
40
|
"id": "global-coverage",
|
|
38
41
|
"component": "feature_card",
|
|
39
42
|
"props": {
|
|
@@ -43,6 +46,7 @@
|
|
|
43
46
|
}
|
|
44
47
|
},
|
|
45
48
|
{
|
|
49
|
+
"$type": "component",
|
|
46
50
|
"id": "low-latency",
|
|
47
51
|
"component": "feature_card",
|
|
48
52
|
"props": {
|
|
@@ -52,6 +56,7 @@
|
|
|
52
56
|
}
|
|
53
57
|
},
|
|
54
58
|
{
|
|
59
|
+
"$type": "component",
|
|
55
60
|
"id": "easy-setup",
|
|
56
61
|
"component": "feature_card",
|
|
57
62
|
"props": {
|
|
@@ -64,6 +69,7 @@
|
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
71
|
{
|
|
72
|
+
"$type": "component",
|
|
67
73
|
"id": "stats",
|
|
68
74
|
"component": "stats_bar",
|
|
69
75
|
"props": {
|
|
@@ -92,6 +98,7 @@
|
|
|
92
98
|
}
|
|
93
99
|
},
|
|
94
100
|
{
|
|
101
|
+
"$type": "component",
|
|
95
102
|
"id": "cta",
|
|
96
103
|
"component": "container",
|
|
97
104
|
"props": {
|
|
@@ -99,6 +106,7 @@
|
|
|
99
106
|
"className": "items-center justify-center text-center",
|
|
100
107
|
"content": [
|
|
101
108
|
{
|
|
109
|
+
"$type": "component",
|
|
102
110
|
"id": "cta-primary",
|
|
103
111
|
"component": "button",
|
|
104
112
|
"props": {
|
|
@@ -110,6 +118,7 @@
|
|
|
110
118
|
}
|
|
111
119
|
},
|
|
112
120
|
{
|
|
121
|
+
"$type": "component",
|
|
113
122
|
"id": "cta-secondary",
|
|
114
123
|
"component": "button",
|
|
115
124
|
"props": {
|