@olonjs/cli 3.0.88 → 3.0.90
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.
|
@@ -1641,7 +1641,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1641
1641
|
"@tiptap/extension-link": "^2.11.5",
|
|
1642
1642
|
"@tiptap/react": "^2.11.5",
|
|
1643
1643
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1644
|
-
"@olonjs/core": "^1.0.
|
|
1644
|
+
"@olonjs/core": "^1.0.78",
|
|
1645
1645
|
"clsx": "^2.1.1",
|
|
1646
1646
|
"lucide-react": "^0.474.0",
|
|
1647
1647
|
"react": "^19.0.0",
|
|
@@ -2612,6 +2612,9 @@ const TENANT_ID = 'alpha';
|
|
|
2612
2612
|
const filePages = getFilePages();
|
|
2613
2613
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
2614
2614
|
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2615
|
+
const ASSET_UPLOAD_MAX_RETRIES = 2;
|
|
2616
|
+
const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
|
|
2617
|
+
const ALLOWED_IMAGE_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif']);
|
|
2615
2618
|
|
|
2616
2619
|
interface CloudSaveUiState {
|
|
2617
2620
|
isOpen: boolean;
|
|
@@ -2918,14 +2921,39 @@ function App() {
|
|
|
2918
2921
|
[isCloudMode, CLOUD_API_URL]
|
|
2919
2922
|
);
|
|
2920
2923
|
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
+
const loadAssetsManifest = useCallback(async (): Promise<void> => {
|
|
2925
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
2926
|
+
const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
|
|
2927
|
+
for (const apiBase of apiBases) {
|
|
2928
|
+
try {
|
|
2929
|
+
const res = await fetch(`${apiBase}/assets/list?limit=200`, {
|
|
2930
|
+
method: 'GET',
|
|
2931
|
+
headers: {
|
|
2932
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
2933
|
+
},
|
|
2934
|
+
});
|
|
2935
|
+
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
2936
|
+
if (!res.ok) continue;
|
|
2937
|
+
const items = Array.isArray(body.items) ? body.items : [];
|
|
2938
|
+
setAssetsManifest(items);
|
|
2939
|
+
return;
|
|
2940
|
+
} catch {
|
|
2941
|
+
// try next candidate
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
setAssetsManifest([]);
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2924
2948
|
fetch('/api/list-assets')
|
|
2925
2949
|
.then((r) => (r.ok ? r.json() : []))
|
|
2926
2950
|
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
2927
2951
|
.catch(() => setAssetsManifest([]));
|
|
2928
|
-
}, []);
|
|
2952
|
+
}, [isCloudMode, CLOUD_API_URL, CLOUD_API_KEY, cloudApiCandidates]);
|
|
2953
|
+
|
|
2954
|
+
useEffect(() => {
|
|
2955
|
+
void loadAssetsManifest();
|
|
2956
|
+
}, [loadAssetsManifest]);
|
|
2929
2957
|
|
|
2930
2958
|
useEffect(() => {
|
|
2931
2959
|
return () => {
|
|
@@ -3278,14 +3306,60 @@ function App() {
|
|
|
3278
3306
|
assetsBaseUrl: '/assets',
|
|
3279
3307
|
assetsManifest,
|
|
3280
3308
|
async onAssetUpload(file: File): Promise<string> {
|
|
3281
|
-
// Note: Asset upload in Cloud Mode requires the R2 Bridge (Next Step in Roadmap)
|
|
3282
|
-
// For now, this works in Local Mode.
|
|
3283
3309
|
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
3310
|
+
if (!ALLOWED_IMAGE_MIME_TYPES.has(file.type)) {
|
|
3311
|
+
throw new Error('Unsupported image format. Allowed: jpeg, png, webp, gif, avif.');
|
|
3312
|
+
}
|
|
3284
3313
|
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
3285
|
-
|
|
3314
|
+
|
|
3315
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
3316
|
+
const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
|
|
3317
|
+
let lastError: Error | null = null;
|
|
3318
|
+
for (const apiBase of apiBases) {
|
|
3319
|
+
for (let attempt = 0; attempt <= ASSET_UPLOAD_MAX_RETRIES; attempt += 1) {
|
|
3320
|
+
try {
|
|
3321
|
+
const formData = new FormData();
|
|
3322
|
+
formData.append('file', file);
|
|
3323
|
+
formData.append('filename', file.name);
|
|
3324
|
+
const controller = new AbortController();
|
|
3325
|
+
const timeout = window.setTimeout(() => controller.abort(), ASSET_UPLOAD_TIMEOUT_MS);
|
|
3326
|
+
const res = await fetch(`${apiBase}/assets/upload`, {
|
|
3327
|
+
method: 'POST',
|
|
3328
|
+
headers: {
|
|
3329
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3330
|
+
'X-Correlation-Id': crypto.randomUUID(),
|
|
3331
|
+
},
|
|
3332
|
+
body: formData,
|
|
3333
|
+
signal: controller.signal,
|
|
3334
|
+
}).finally(() => window.clearTimeout(timeout));
|
|
3335
|
+
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string; code?: string };
|
|
3336
|
+
if (res.ok && typeof body.url === 'string') {
|
|
3337
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
3338
|
+
return body.url;
|
|
3339
|
+
}
|
|
3340
|
+
lastError = new Error(body.error || body.code || `Cloud upload failed: ${res.status}`);
|
|
3341
|
+
if (isRetryableStatus(res.status) && attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
3342
|
+
await sleep(backoffDelayMs(attempt));
|
|
3343
|
+
continue;
|
|
3344
|
+
}
|
|
3345
|
+
break;
|
|
3346
|
+
} catch (error: unknown) {
|
|
3347
|
+
const message = error instanceof Error ? error.message : 'Cloud upload failed.';
|
|
3348
|
+
lastError = new Error(message);
|
|
3349
|
+
if (attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
3350
|
+
await sleep(backoffDelayMs(attempt));
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
break;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
throw lastError ?? new Error('Cloud upload failed.');
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3286
3360
|
const base64 = await new Promise<string>((resolve, reject) => {
|
|
3287
3361
|
const reader = new FileReader();
|
|
3288
|
-
reader.onload
|
|
3362
|
+
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
3289
3363
|
reader.onerror = () => reject(reader.error);
|
|
3290
3364
|
reader.readAsDataURL(file);
|
|
3291
3365
|
});
|
|
@@ -3295,10 +3369,10 @@ function App() {
|
|
|
3295
3369
|
headers: { 'Content-Type': 'application/json' },
|
|
3296
3370
|
body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
|
|
3297
3371
|
});
|
|
3298
|
-
|
|
3299
3372
|
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
3300
3373
|
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
3301
3374
|
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
3375
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
3302
3376
|
return body.url;
|
|
3303
3377
|
},
|
|
3304
3378
|
},
|
|
@@ -7743,19 +7817,19 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
|
|
|
7743
7817
|
{
|
|
7744
7818
|
"main": [
|
|
7745
7819
|
{
|
|
7746
|
-
"label": "
|
|
7747
|
-
"href": "#
|
|
7820
|
+
"label": "The Problem",
|
|
7821
|
+
"href": "#problem"
|
|
7748
7822
|
},
|
|
7749
7823
|
{
|
|
7750
|
-
"label": "
|
|
7751
|
-
"href": "#
|
|
7824
|
+
"label": "Architecture",
|
|
7825
|
+
"href": "#architecture"
|
|
7752
7826
|
},
|
|
7753
7827
|
{
|
|
7754
|
-
"label": "
|
|
7755
|
-
"href": "#
|
|
7828
|
+
"label": "Why",
|
|
7829
|
+
"href": "#why"
|
|
7756
7830
|
},
|
|
7757
7831
|
{
|
|
7758
|
-
"label": "
|
|
7832
|
+
"label": "DX",
|
|
7759
7833
|
"href": "#devex"
|
|
7760
7834
|
}
|
|
7761
7835
|
]
|
|
@@ -7796,19 +7870,19 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
7796
7870
|
"badge": "v1.4.0",
|
|
7797
7871
|
"links": [
|
|
7798
7872
|
{
|
|
7799
|
-
"label": "
|
|
7800
|
-
"href": "#
|
|
7873
|
+
"label": "The Problem",
|
|
7874
|
+
"href": "#problem"
|
|
7801
7875
|
},
|
|
7802
7876
|
{
|
|
7803
|
-
"label": "
|
|
7804
|
-
"href": "#
|
|
7877
|
+
"label": "Architecture",
|
|
7878
|
+
"href": "#architecture"
|
|
7805
7879
|
},
|
|
7806
7880
|
{
|
|
7807
|
-
"label": "
|
|
7808
|
-
"href": "#
|
|
7881
|
+
"label": "Why",
|
|
7882
|
+
"href": "#why"
|
|
7809
7883
|
},
|
|
7810
7884
|
{
|
|
7811
|
-
"label": "
|
|
7885
|
+
"label": "DX",
|
|
7812
7886
|
"href": "#devex"
|
|
7813
7887
|
}
|
|
7814
7888
|
]
|
|
@@ -7917,8 +7991,18 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
7917
7991
|
"titleHighlight": "Agentic Web",
|
|
7918
7992
|
"description": "AI agents are becoming operational actors in commerce, marketing, and support. OlonJS introduces a deterministic machine contract for websites — so agents can reliably read and operate any site, without custom glue.",
|
|
7919
7993
|
"ctas": [
|
|
7920
|
-
{
|
|
7921
|
-
|
|
7994
|
+
{
|
|
7995
|
+
"id": "cta-1",
|
|
7996
|
+
"label": "Read the Spec",
|
|
7997
|
+
"href": "/docs",
|
|
7998
|
+
"variant": "primary"
|
|
7999
|
+
},
|
|
8000
|
+
{
|
|
8001
|
+
"id": "cta-2",
|
|
8002
|
+
"label": "View on GitHub",
|
|
8003
|
+
"href": "https://github.com/olonjs/npm-jpcore",
|
|
8004
|
+
"variant": "secondary"
|
|
8005
|
+
}
|
|
7922
8006
|
],
|
|
7923
8007
|
"metrics": []
|
|
7924
8008
|
},
|
|
@@ -7933,16 +8017,35 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
7933
8017
|
"problemTag": "The problem",
|
|
7934
8018
|
"problemTitle": "Websites aren't built for agents",
|
|
7935
8019
|
"problemItems": [
|
|
7936
|
-
{
|
|
7937
|
-
|
|
7938
|
-
|
|
8020
|
+
{
|
|
8021
|
+
"id": "pi-1",
|
|
8022
|
+
"text": "Agentic workflows are growing, but integration is mostly custom glue — rebuilt tenant by tenant"
|
|
8023
|
+
},
|
|
8024
|
+
{
|
|
8025
|
+
"id": "pi-2",
|
|
8026
|
+
"text": "Every site has a different content structure, routing assumptions, and edge cases"
|
|
8027
|
+
},
|
|
8028
|
+
{
|
|
8029
|
+
"id": "pi-3",
|
|
8030
|
+
"text": "HTML-heavy, CMS-fragmented, inconsistent across properties — slow, brittle, expensive"
|
|
8031
|
+
}
|
|
7939
8032
|
],
|
|
7940
8033
|
"solutionTag": "Our solution",
|
|
7941
8034
|
"solutionTitle": "A standard machine contract across tenants",
|
|
7942
8035
|
"solutionItems": [
|
|
7943
|
-
{
|
|
7944
|
-
|
|
7945
|
-
|
|
8036
|
+
{
|
|
8037
|
+
"id": "si-1",
|
|
8038
|
+
"text": "Predictable page endpoints for agents —",
|
|
8039
|
+
"code": "/{slug}.json"
|
|
8040
|
+
},
|
|
8041
|
+
{
|
|
8042
|
+
"id": "si-2",
|
|
8043
|
+
"text": "Typed, schema-driven content contracts — validated, versioned, auditable"
|
|
8044
|
+
},
|
|
8045
|
+
{
|
|
8046
|
+
"id": "si-3",
|
|
8047
|
+
"text": "Repeatable governance and deployment patterns across every tenant"
|
|
8048
|
+
}
|
|
7946
8049
|
]
|
|
7947
8050
|
},
|
|
7948
8051
|
"settings": {}
|
|
@@ -7956,15 +8059,47 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
7956
8059
|
"sectionTitle": "Built for enterprise scale",
|
|
7957
8060
|
"sectionLead": "Every layer is designed for determinism — from file system layout to component contracts to Studio UX.",
|
|
7958
8061
|
"cards": [
|
|
7959
|
-
{
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
8062
|
+
{
|
|
8063
|
+
"id": "fc-1",
|
|
8064
|
+
"emoji": "📐",
|
|
8065
|
+
"title": "Modular Type Registry",
|
|
8066
|
+
"description": "Core defines empty registries; tenants inject types via module augmentation. Full TypeScript safety, zero Core changes."
|
|
8067
|
+
},
|
|
8068
|
+
{
|
|
8069
|
+
"id": "fc-2",
|
|
8070
|
+
"emoji": "🧱",
|
|
8071
|
+
"title": "Tenant Block Protocol",
|
|
8072
|
+
"description": "Self-contained capsules (View + schema + types) enable automated ingestion and consistent editor generation."
|
|
8073
|
+
},
|
|
8074
|
+
{
|
|
8075
|
+
"id": "fc-3",
|
|
8076
|
+
"emoji": "⚙️",
|
|
8077
|
+
"title": "Deterministic CLI",
|
|
8078
|
+
"description": "@olonjs/cli projects new tenants from a canonical script — reproducible across every environment."
|
|
8079
|
+
},
|
|
8080
|
+
{
|
|
8081
|
+
"id": "fc-4",
|
|
8082
|
+
"emoji": "🎯",
|
|
8083
|
+
"title": "ICE Data Contract",
|
|
8084
|
+
"description": "Mandatory DOM attributes bind the Studio canvas to Inspector fields without coupling to tenant DOM structure."
|
|
8085
|
+
},
|
|
8086
|
+
{
|
|
8087
|
+
"id": "fc-5",
|
|
8088
|
+
"emoji": "📦",
|
|
8089
|
+
"title": "Base Schema Fragments",
|
|
8090
|
+
"description": "Shared BaseSectionData and BaseArrayItem enforce anchor IDs and stable React keys across all capsules."
|
|
8091
|
+
},
|
|
8092
|
+
{
|
|
8093
|
+
"id": "fc-6",
|
|
8094
|
+
"emoji": "🔗",
|
|
8095
|
+
"title": "Path-Based Selection",
|
|
8096
|
+
"description": "v1.4 strict path semantics eliminate nested array ambiguity. Studio selection is root-to-leaf, always deterministic."
|
|
8097
|
+
}
|
|
7965
8098
|
]
|
|
7966
8099
|
},
|
|
7967
|
-
"settings": {
|
|
8100
|
+
"settings": {
|
|
8101
|
+
"columns": 3
|
|
8102
|
+
}
|
|
7968
8103
|
},
|
|
7969
8104
|
{
|
|
7970
8105
|
"id": "why-now",
|
|
@@ -7975,10 +8110,26 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
7975
8110
|
"title": "Why this matters",
|
|
7976
8111
|
"titleAccent": "now",
|
|
7977
8112
|
"cards": [
|
|
7978
|
-
{
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
8113
|
+
{
|
|
8114
|
+
"id": "wc-1",
|
|
8115
|
+
"title": "Agentic commerce is live",
|
|
8116
|
+
"description": "Operational standards are missing. Without a contract layer, teams face high integration cost and low reliability."
|
|
8117
|
+
},
|
|
8118
|
+
{
|
|
8119
|
+
"id": "wc-2",
|
|
8120
|
+
"title": "Enterprises need governance",
|
|
8121
|
+
"description": "A contract layer you can audit, version, and scale — not a one-off adapter for every new agent workflow."
|
|
8122
|
+
},
|
|
8123
|
+
{
|
|
8124
|
+
"id": "wc-3",
|
|
8125
|
+
"title": "AI tooling is ready",
|
|
8126
|
+
"description": "Deterministic structure means AI can scaffold, validate, and evolve tenants with less prompt ambiguity."
|
|
8127
|
+
},
|
|
8128
|
+
{
|
|
8129
|
+
"id": "wc-4",
|
|
8130
|
+
"title": "Speed compounds",
|
|
8131
|
+
"description": "Teams that standardize now ship new experiences in hours while others rebuild integration logic repeatedly."
|
|
8132
|
+
}
|
|
7982
8133
|
]
|
|
7983
8134
|
},
|
|
7984
8135
|
"settings": {}
|
|
@@ -7987,19 +8138,40 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
7987
8138
|
"id": "dx-section",
|
|
7988
8139
|
"type": "devex",
|
|
7989
8140
|
"data": {
|
|
7990
|
-
"anchorId": "
|
|
8141
|
+
"anchorId": "devex",
|
|
7991
8142
|
"label": "Developer Velocity",
|
|
7992
8143
|
"title": "AI-native advantage,\nfrom day one",
|
|
7993
8144
|
"description": "OlonJS dramatically increases AI-assisted development speed. Because structure is deterministic, agents scaffold and evolve tenants faster — with lower regression risk.",
|
|
7994
8145
|
"features": [
|
|
7995
|
-
{
|
|
7996
|
-
|
|
7997
|
-
|
|
8146
|
+
{
|
|
8147
|
+
"id": "df-1",
|
|
8148
|
+
"text": "AI scaffolds and evolves tenants faster because structure is deterministic"
|
|
8149
|
+
},
|
|
8150
|
+
{
|
|
8151
|
+
"id": "df-2",
|
|
8152
|
+
"text": "Shared conventions reduce prompt ambiguity and implementation drift"
|
|
8153
|
+
},
|
|
8154
|
+
{
|
|
8155
|
+
"id": "df-3",
|
|
8156
|
+
"text": "Ship new tenant experiences in hours, not weeks"
|
|
8157
|
+
}
|
|
7998
8158
|
],
|
|
7999
8159
|
"stats": [
|
|
8000
|
-
{
|
|
8001
|
-
|
|
8002
|
-
|
|
8160
|
+
{
|
|
8161
|
+
"id": "ds-1",
|
|
8162
|
+
"value": "10×",
|
|
8163
|
+
"label": "Faster scaffolding"
|
|
8164
|
+
},
|
|
8165
|
+
{
|
|
8166
|
+
"id": "ds-2",
|
|
8167
|
+
"value": "∅",
|
|
8168
|
+
"label": "Glue per tenant"
|
|
8169
|
+
},
|
|
8170
|
+
{
|
|
8171
|
+
"id": "ds-3",
|
|
8172
|
+
"value": "100%",
|
|
8173
|
+
"label": "Type-safe contracts"
|
|
8174
|
+
}
|
|
8003
8175
|
]
|
|
8004
8176
|
},
|
|
8005
8177
|
"settings": {}
|
|
@@ -8013,15 +8185,24 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
8013
8185
|
"description": "Read the full specification or explore the source on GitHub. Zero dependencies to start — one JSON endpoint per page.",
|
|
8014
8186
|
"cliCommand": "npx @olonjs/cli@latest new tenant",
|
|
8015
8187
|
"ctas": [
|
|
8016
|
-
{
|
|
8017
|
-
|
|
8188
|
+
{
|
|
8189
|
+
"id": "cta-docs",
|
|
8190
|
+
"label": "Read the Specification",
|
|
8191
|
+
"href": "/docs",
|
|
8192
|
+
"variant": "primary"
|
|
8193
|
+
},
|
|
8194
|
+
{
|
|
8195
|
+
"id": "cta-gh",
|
|
8196
|
+
"label": "View on GitHub",
|
|
8197
|
+
"href": "https://github.com/olonjs/npm-jpcore",
|
|
8198
|
+
"variant": "secondary"
|
|
8199
|
+
}
|
|
8018
8200
|
]
|
|
8019
8201
|
},
|
|
8020
8202
|
"settings": {}
|
|
8021
8203
|
}
|
|
8022
8204
|
]
|
|
8023
8205
|
}
|
|
8024
|
-
|
|
8025
8206
|
END_OF_FILE_CONTENT
|
|
8026
8207
|
echo "Creating src/data/pages/post.json..."
|
|
8027
8208
|
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/post.json"
|
|
@@ -596,7 +596,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
596
596
|
"@tiptap/extension-link": "^2.11.5",
|
|
597
597
|
"@tiptap/react": "^2.11.5",
|
|
598
598
|
"@tiptap/starter-kit": "^2.11.5",
|
|
599
|
-
"@olonjs/core": "^1.0.
|
|
599
|
+
"@olonjs/core": "^1.0.78",
|
|
600
600
|
"clsx": "^2.1.1",
|
|
601
601
|
"lucide-react": "^0.474.0",
|
|
602
602
|
"react": "^19.0.0",
|
|
@@ -957,6 +957,9 @@ const TENANT_ID = 'santamamma'; // 🌿 SantaMamma Agriturismo
|
|
|
957
957
|
const filePages = getFilePages();
|
|
958
958
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
959
959
|
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
960
|
+
const ASSET_UPLOAD_MAX_RETRIES = 2;
|
|
961
|
+
const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
|
|
962
|
+
const ALLOWED_IMAGE_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif']);
|
|
960
963
|
|
|
961
964
|
interface CloudSaveUiState {
|
|
962
965
|
isOpen: boolean;
|
|
@@ -980,6 +983,30 @@ function stepProgress(doneSteps: StepId[]): number {
|
|
|
980
983
|
return Math.round((doneSteps.length / DEPLOY_STEPS.length) * 100);
|
|
981
984
|
}
|
|
982
985
|
|
|
986
|
+
function normalizeApiBase(raw: string): string {
|
|
987
|
+
return raw.trim().replace(/\/+$/, '');
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function buildUploadEndpoint(raw: string): string {
|
|
991
|
+
const base = normalizeApiBase(raw);
|
|
992
|
+
const withApi = /\/api\/v1$/i.test(base) ? base : `${base}/api/v1`;
|
|
993
|
+
return `${withApi}/assets/upload`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function isRetryableStatus(status: number): boolean {
|
|
997
|
+
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function backoffDelayMs(attempt: number): number {
|
|
1001
|
+
const base = 250 * Math.pow(2, attempt);
|
|
1002
|
+
const jitter = Math.floor(Math.random() * 120);
|
|
1003
|
+
return base + jitter;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function sleep(ms: number): Promise<void> {
|
|
1007
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1008
|
+
}
|
|
1009
|
+
|
|
983
1010
|
function App() {
|
|
984
1011
|
const [{ pages, siteConfig }] = useState(getInitialData);
|
|
985
1012
|
const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
|
|
@@ -988,12 +1015,36 @@ function App() {
|
|
|
988
1015
|
const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
|
|
989
1016
|
const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
|
|
990
1017
|
|
|
991
|
-
|
|
1018
|
+
const loadAssetsManifest = useCallback(async (): Promise<void> => {
|
|
1019
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
1020
|
+
try {
|
|
1021
|
+
const res = await fetch(`${buildUploadEndpoint(CLOUD_API_URL).replace(/\/upload$/, '/list')}?limit=200`, {
|
|
1022
|
+
method: 'GET',
|
|
1023
|
+
headers: {
|
|
1024
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
1025
|
+
},
|
|
1026
|
+
});
|
|
1027
|
+
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
1028
|
+
if (res.ok) {
|
|
1029
|
+
setAssetsManifest(Array.isArray(body.items) ? body.items : []);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
} catch {
|
|
1033
|
+
// fallback to empty
|
|
1034
|
+
}
|
|
1035
|
+
setAssetsManifest([]);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
992
1039
|
fetch('/api/list-assets')
|
|
993
1040
|
.then((r) => (r.ok ? r.json() : []))
|
|
994
1041
|
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
995
1042
|
.catch(() => setAssetsManifest([]));
|
|
996
|
-
}, []);
|
|
1043
|
+
}, [isCloudMode, CLOUD_API_URL, CLOUD_API_KEY]);
|
|
1044
|
+
|
|
1045
|
+
useEffect(() => {
|
|
1046
|
+
void loadAssetsManifest();
|
|
1047
|
+
}, [loadAssetsManifest]);
|
|
997
1048
|
|
|
998
1049
|
useEffect(() => {
|
|
999
1050
|
return () => { activeCloudSaveController.current?.abort(); };
|
|
@@ -1104,7 +1155,53 @@ function App() {
|
|
|
1104
1155
|
assetsManifest,
|
|
1105
1156
|
async onAssetUpload(file: File): Promise<string> {
|
|
1106
1157
|
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
1158
|
+
if (!ALLOWED_IMAGE_MIME_TYPES.has(file.type)) {
|
|
1159
|
+
throw new Error('Unsupported image format. Allowed: jpeg, png, webp, gif, avif.');
|
|
1160
|
+
}
|
|
1107
1161
|
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
1162
|
+
|
|
1163
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
1164
|
+
let lastError: Error | null = null;
|
|
1165
|
+
for (let attempt = 0; attempt <= ASSET_UPLOAD_MAX_RETRIES; attempt += 1) {
|
|
1166
|
+
try {
|
|
1167
|
+
const formData = new FormData();
|
|
1168
|
+
formData.append('file', file);
|
|
1169
|
+
formData.append('filename', file.name);
|
|
1170
|
+
const controller = new AbortController();
|
|
1171
|
+
const timeout = window.setTimeout(() => controller.abort(), ASSET_UPLOAD_TIMEOUT_MS);
|
|
1172
|
+
const res = await fetch(buildUploadEndpoint(CLOUD_API_URL), {
|
|
1173
|
+
method: 'POST',
|
|
1174
|
+
headers: {
|
|
1175
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
1176
|
+
'X-Correlation-Id': crypto.randomUUID(),
|
|
1177
|
+
},
|
|
1178
|
+
body: formData,
|
|
1179
|
+
signal: controller.signal,
|
|
1180
|
+
}).finally(() => window.clearTimeout(timeout));
|
|
1181
|
+
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string; code?: string };
|
|
1182
|
+
if (res.ok && typeof body.url === 'string') {
|
|
1183
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
1184
|
+
return body.url;
|
|
1185
|
+
}
|
|
1186
|
+
lastError = new Error(body.error || body.code || `Upload failed: ${res.status}`);
|
|
1187
|
+
if (isRetryableStatus(res.status) && attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
1188
|
+
await sleep(backoffDelayMs(attempt));
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
break;
|
|
1192
|
+
} catch (error: unknown) {
|
|
1193
|
+
const message = error instanceof Error ? error.message : 'Cloud upload failed.';
|
|
1194
|
+
lastError = new Error(message);
|
|
1195
|
+
if (attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
1196
|
+
await sleep(backoffDelayMs(attempt));
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
throw lastError ?? new Error('Cloud upload failed.');
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1108
1205
|
const base64 = await new Promise<string>((resolve, reject) => {
|
|
1109
1206
|
const reader = new FileReader();
|
|
1110
1207
|
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
@@ -1119,6 +1216,7 @@ function App() {
|
|
|
1119
1216
|
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
1120
1217
|
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
1121
1218
|
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
1219
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
1122
1220
|
return body.url;
|
|
1123
1221
|
},
|
|
1124
1222
|
},
|
|
@@ -1641,7 +1641,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1641
1641
|
"@tiptap/extension-link": "^2.11.5",
|
|
1642
1642
|
"@tiptap/react": "^2.11.5",
|
|
1643
1643
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1644
|
-
"@olonjs/core": "^1.0.
|
|
1644
|
+
"@olonjs/core": "^1.0.78",
|
|
1645
1645
|
"clsx": "^2.1.1",
|
|
1646
1646
|
"lucide-react": "^0.474.0",
|
|
1647
1647
|
"react": "^19.0.0",
|
|
@@ -2612,6 +2612,9 @@ const TENANT_ID = 'alpha';
|
|
|
2612
2612
|
const filePages = getFilePages();
|
|
2613
2613
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
2614
2614
|
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2615
|
+
const ASSET_UPLOAD_MAX_RETRIES = 2;
|
|
2616
|
+
const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
|
|
2617
|
+
const ALLOWED_IMAGE_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif']);
|
|
2615
2618
|
|
|
2616
2619
|
interface CloudSaveUiState {
|
|
2617
2620
|
isOpen: boolean;
|
|
@@ -2918,14 +2921,39 @@ function App() {
|
|
|
2918
2921
|
[isCloudMode, CLOUD_API_URL]
|
|
2919
2922
|
);
|
|
2920
2923
|
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
+
const loadAssetsManifest = useCallback(async (): Promise<void> => {
|
|
2925
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
2926
|
+
const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
|
|
2927
|
+
for (const apiBase of apiBases) {
|
|
2928
|
+
try {
|
|
2929
|
+
const res = await fetch(`${apiBase}/assets/list?limit=200`, {
|
|
2930
|
+
method: 'GET',
|
|
2931
|
+
headers: {
|
|
2932
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
2933
|
+
},
|
|
2934
|
+
});
|
|
2935
|
+
const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
|
|
2936
|
+
if (!res.ok) continue;
|
|
2937
|
+
const items = Array.isArray(body.items) ? body.items : [];
|
|
2938
|
+
setAssetsManifest(items);
|
|
2939
|
+
return;
|
|
2940
|
+
} catch {
|
|
2941
|
+
// try next candidate
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
setAssetsManifest([]);
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2924
2948
|
fetch('/api/list-assets')
|
|
2925
2949
|
.then((r) => (r.ok ? r.json() : []))
|
|
2926
2950
|
.then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
|
|
2927
2951
|
.catch(() => setAssetsManifest([]));
|
|
2928
|
-
}, []);
|
|
2952
|
+
}, [isCloudMode, CLOUD_API_URL, CLOUD_API_KEY, cloudApiCandidates]);
|
|
2953
|
+
|
|
2954
|
+
useEffect(() => {
|
|
2955
|
+
void loadAssetsManifest();
|
|
2956
|
+
}, [loadAssetsManifest]);
|
|
2929
2957
|
|
|
2930
2958
|
useEffect(() => {
|
|
2931
2959
|
return () => {
|
|
@@ -3278,14 +3306,60 @@ function App() {
|
|
|
3278
3306
|
assetsBaseUrl: '/assets',
|
|
3279
3307
|
assetsManifest,
|
|
3280
3308
|
async onAssetUpload(file: File): Promise<string> {
|
|
3281
|
-
// Note: Asset upload in Cloud Mode requires the R2 Bridge (Next Step in Roadmap)
|
|
3282
|
-
// For now, this works in Local Mode.
|
|
3283
3309
|
if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
|
|
3310
|
+
if (!ALLOWED_IMAGE_MIME_TYPES.has(file.type)) {
|
|
3311
|
+
throw new Error('Unsupported image format. Allowed: jpeg, png, webp, gif, avif.');
|
|
3312
|
+
}
|
|
3284
3313
|
if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
|
|
3285
|
-
|
|
3314
|
+
|
|
3315
|
+
if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
|
|
3316
|
+
const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
|
|
3317
|
+
let lastError: Error | null = null;
|
|
3318
|
+
for (const apiBase of apiBases) {
|
|
3319
|
+
for (let attempt = 0; attempt <= ASSET_UPLOAD_MAX_RETRIES; attempt += 1) {
|
|
3320
|
+
try {
|
|
3321
|
+
const formData = new FormData();
|
|
3322
|
+
formData.append('file', file);
|
|
3323
|
+
formData.append('filename', file.name);
|
|
3324
|
+
const controller = new AbortController();
|
|
3325
|
+
const timeout = window.setTimeout(() => controller.abort(), ASSET_UPLOAD_TIMEOUT_MS);
|
|
3326
|
+
const res = await fetch(`${apiBase}/assets/upload`, {
|
|
3327
|
+
method: 'POST',
|
|
3328
|
+
headers: {
|
|
3329
|
+
Authorization: `Bearer ${CLOUD_API_KEY}`,
|
|
3330
|
+
'X-Correlation-Id': crypto.randomUUID(),
|
|
3331
|
+
},
|
|
3332
|
+
body: formData,
|
|
3333
|
+
signal: controller.signal,
|
|
3334
|
+
}).finally(() => window.clearTimeout(timeout));
|
|
3335
|
+
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string; code?: string };
|
|
3336
|
+
if (res.ok && typeof body.url === 'string') {
|
|
3337
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
3338
|
+
return body.url;
|
|
3339
|
+
}
|
|
3340
|
+
lastError = new Error(body.error || body.code || `Cloud upload failed: ${res.status}`);
|
|
3341
|
+
if (isRetryableStatus(res.status) && attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
3342
|
+
await sleep(backoffDelayMs(attempt));
|
|
3343
|
+
continue;
|
|
3344
|
+
}
|
|
3345
|
+
break;
|
|
3346
|
+
} catch (error: unknown) {
|
|
3347
|
+
const message = error instanceof Error ? error.message : 'Cloud upload failed.';
|
|
3348
|
+
lastError = new Error(message);
|
|
3349
|
+
if (attempt < ASSET_UPLOAD_MAX_RETRIES) {
|
|
3350
|
+
await sleep(backoffDelayMs(attempt));
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
break;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
throw lastError ?? new Error('Cloud upload failed.');
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3286
3360
|
const base64 = await new Promise<string>((resolve, reject) => {
|
|
3287
3361
|
const reader = new FileReader();
|
|
3288
|
-
reader.onload
|
|
3362
|
+
reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
|
|
3289
3363
|
reader.onerror = () => reject(reader.error);
|
|
3290
3364
|
reader.readAsDataURL(file);
|
|
3291
3365
|
});
|
|
@@ -3295,10 +3369,10 @@ function App() {
|
|
|
3295
3369
|
headers: { 'Content-Type': 'application/json' },
|
|
3296
3370
|
body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
|
|
3297
3371
|
});
|
|
3298
|
-
|
|
3299
3372
|
const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
|
|
3300
3373
|
if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
|
|
3301
3374
|
if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
|
|
3375
|
+
await loadAssetsManifest().catch(() => undefined);
|
|
3302
3376
|
return body.url;
|
|
3303
3377
|
},
|
|
3304
3378
|
},
|
|
@@ -7743,19 +7817,19 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
|
|
|
7743
7817
|
{
|
|
7744
7818
|
"main": [
|
|
7745
7819
|
{
|
|
7746
|
-
"label": "
|
|
7747
|
-
"href": "#
|
|
7820
|
+
"label": "The Problem",
|
|
7821
|
+
"href": "#problem"
|
|
7748
7822
|
},
|
|
7749
7823
|
{
|
|
7750
|
-
"label": "
|
|
7751
|
-
"href": "#
|
|
7824
|
+
"label": "Architecture",
|
|
7825
|
+
"href": "#architecture"
|
|
7752
7826
|
},
|
|
7753
7827
|
{
|
|
7754
|
-
"label": "
|
|
7755
|
-
"href": "#
|
|
7828
|
+
"label": "Why",
|
|
7829
|
+
"href": "#why"
|
|
7756
7830
|
},
|
|
7757
7831
|
{
|
|
7758
|
-
"label": "
|
|
7832
|
+
"label": "DX",
|
|
7759
7833
|
"href": "#devex"
|
|
7760
7834
|
}
|
|
7761
7835
|
]
|
|
@@ -7796,19 +7870,19 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
7796
7870
|
"badge": "v1.4.0",
|
|
7797
7871
|
"links": [
|
|
7798
7872
|
{
|
|
7799
|
-
"label": "
|
|
7800
|
-
"href": "#
|
|
7873
|
+
"label": "The Problem",
|
|
7874
|
+
"href": "#problem"
|
|
7801
7875
|
},
|
|
7802
7876
|
{
|
|
7803
|
-
"label": "
|
|
7804
|
-
"href": "#
|
|
7877
|
+
"label": "Architecture",
|
|
7878
|
+
"href": "#architecture"
|
|
7805
7879
|
},
|
|
7806
7880
|
{
|
|
7807
|
-
"label": "
|
|
7808
|
-
"href": "#
|
|
7881
|
+
"label": "Why",
|
|
7882
|
+
"href": "#why"
|
|
7809
7883
|
},
|
|
7810
7884
|
{
|
|
7811
|
-
"label": "
|
|
7885
|
+
"label": "DX",
|
|
7812
7886
|
"href": "#devex"
|
|
7813
7887
|
}
|
|
7814
7888
|
]
|
|
@@ -7917,8 +7991,18 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
7917
7991
|
"titleHighlight": "Agentic Web",
|
|
7918
7992
|
"description": "AI agents are becoming operational actors in commerce, marketing, and support. OlonJS introduces a deterministic machine contract for websites — so agents can reliably read and operate any site, without custom glue.",
|
|
7919
7993
|
"ctas": [
|
|
7920
|
-
{
|
|
7921
|
-
|
|
7994
|
+
{
|
|
7995
|
+
"id": "cta-1",
|
|
7996
|
+
"label": "Read the Spec",
|
|
7997
|
+
"href": "/docs",
|
|
7998
|
+
"variant": "primary"
|
|
7999
|
+
},
|
|
8000
|
+
{
|
|
8001
|
+
"id": "cta-2",
|
|
8002
|
+
"label": "View on GitHub",
|
|
8003
|
+
"href": "https://github.com/olonjs/npm-jpcore",
|
|
8004
|
+
"variant": "secondary"
|
|
8005
|
+
}
|
|
7922
8006
|
],
|
|
7923
8007
|
"metrics": []
|
|
7924
8008
|
},
|
|
@@ -7933,16 +8017,35 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
7933
8017
|
"problemTag": "The problem",
|
|
7934
8018
|
"problemTitle": "Websites aren't built for agents",
|
|
7935
8019
|
"problemItems": [
|
|
7936
|
-
{
|
|
7937
|
-
|
|
7938
|
-
|
|
8020
|
+
{
|
|
8021
|
+
"id": "pi-1",
|
|
8022
|
+
"text": "Agentic workflows are growing, but integration is mostly custom glue — rebuilt tenant by tenant"
|
|
8023
|
+
},
|
|
8024
|
+
{
|
|
8025
|
+
"id": "pi-2",
|
|
8026
|
+
"text": "Every site has a different content structure, routing assumptions, and edge cases"
|
|
8027
|
+
},
|
|
8028
|
+
{
|
|
8029
|
+
"id": "pi-3",
|
|
8030
|
+
"text": "HTML-heavy, CMS-fragmented, inconsistent across properties — slow, brittle, expensive"
|
|
8031
|
+
}
|
|
7939
8032
|
],
|
|
7940
8033
|
"solutionTag": "Our solution",
|
|
7941
8034
|
"solutionTitle": "A standard machine contract across tenants",
|
|
7942
8035
|
"solutionItems": [
|
|
7943
|
-
{
|
|
7944
|
-
|
|
7945
|
-
|
|
8036
|
+
{
|
|
8037
|
+
"id": "si-1",
|
|
8038
|
+
"text": "Predictable page endpoints for agents —",
|
|
8039
|
+
"code": "/{slug}.json"
|
|
8040
|
+
},
|
|
8041
|
+
{
|
|
8042
|
+
"id": "si-2",
|
|
8043
|
+
"text": "Typed, schema-driven content contracts — validated, versioned, auditable"
|
|
8044
|
+
},
|
|
8045
|
+
{
|
|
8046
|
+
"id": "si-3",
|
|
8047
|
+
"text": "Repeatable governance and deployment patterns across every tenant"
|
|
8048
|
+
}
|
|
7946
8049
|
]
|
|
7947
8050
|
},
|
|
7948
8051
|
"settings": {}
|
|
@@ -7956,15 +8059,47 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
7956
8059
|
"sectionTitle": "Built for enterprise scale",
|
|
7957
8060
|
"sectionLead": "Every layer is designed for determinism — from file system layout to component contracts to Studio UX.",
|
|
7958
8061
|
"cards": [
|
|
7959
|
-
{
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
8062
|
+
{
|
|
8063
|
+
"id": "fc-1",
|
|
8064
|
+
"emoji": "📐",
|
|
8065
|
+
"title": "Modular Type Registry",
|
|
8066
|
+
"description": "Core defines empty registries; tenants inject types via module augmentation. Full TypeScript safety, zero Core changes."
|
|
8067
|
+
},
|
|
8068
|
+
{
|
|
8069
|
+
"id": "fc-2",
|
|
8070
|
+
"emoji": "🧱",
|
|
8071
|
+
"title": "Tenant Block Protocol",
|
|
8072
|
+
"description": "Self-contained capsules (View + schema + types) enable automated ingestion and consistent editor generation."
|
|
8073
|
+
},
|
|
8074
|
+
{
|
|
8075
|
+
"id": "fc-3",
|
|
8076
|
+
"emoji": "⚙️",
|
|
8077
|
+
"title": "Deterministic CLI",
|
|
8078
|
+
"description": "@olonjs/cli projects new tenants from a canonical script — reproducible across every environment."
|
|
8079
|
+
},
|
|
8080
|
+
{
|
|
8081
|
+
"id": "fc-4",
|
|
8082
|
+
"emoji": "🎯",
|
|
8083
|
+
"title": "ICE Data Contract",
|
|
8084
|
+
"description": "Mandatory DOM attributes bind the Studio canvas to Inspector fields without coupling to tenant DOM structure."
|
|
8085
|
+
},
|
|
8086
|
+
{
|
|
8087
|
+
"id": "fc-5",
|
|
8088
|
+
"emoji": "📦",
|
|
8089
|
+
"title": "Base Schema Fragments",
|
|
8090
|
+
"description": "Shared BaseSectionData and BaseArrayItem enforce anchor IDs and stable React keys across all capsules."
|
|
8091
|
+
},
|
|
8092
|
+
{
|
|
8093
|
+
"id": "fc-6",
|
|
8094
|
+
"emoji": "🔗",
|
|
8095
|
+
"title": "Path-Based Selection",
|
|
8096
|
+
"description": "v1.4 strict path semantics eliminate nested array ambiguity. Studio selection is root-to-leaf, always deterministic."
|
|
8097
|
+
}
|
|
7965
8098
|
]
|
|
7966
8099
|
},
|
|
7967
|
-
"settings": {
|
|
8100
|
+
"settings": {
|
|
8101
|
+
"columns": 3
|
|
8102
|
+
}
|
|
7968
8103
|
},
|
|
7969
8104
|
{
|
|
7970
8105
|
"id": "why-now",
|
|
@@ -7975,10 +8110,26 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
7975
8110
|
"title": "Why this matters",
|
|
7976
8111
|
"titleAccent": "now",
|
|
7977
8112
|
"cards": [
|
|
7978
|
-
{
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
8113
|
+
{
|
|
8114
|
+
"id": "wc-1",
|
|
8115
|
+
"title": "Agentic commerce is live",
|
|
8116
|
+
"description": "Operational standards are missing. Without a contract layer, teams face high integration cost and low reliability."
|
|
8117
|
+
},
|
|
8118
|
+
{
|
|
8119
|
+
"id": "wc-2",
|
|
8120
|
+
"title": "Enterprises need governance",
|
|
8121
|
+
"description": "A contract layer you can audit, version, and scale — not a one-off adapter for every new agent workflow."
|
|
8122
|
+
},
|
|
8123
|
+
{
|
|
8124
|
+
"id": "wc-3",
|
|
8125
|
+
"title": "AI tooling is ready",
|
|
8126
|
+
"description": "Deterministic structure means AI can scaffold, validate, and evolve tenants with less prompt ambiguity."
|
|
8127
|
+
},
|
|
8128
|
+
{
|
|
8129
|
+
"id": "wc-4",
|
|
8130
|
+
"title": "Speed compounds",
|
|
8131
|
+
"description": "Teams that standardize now ship new experiences in hours while others rebuild integration logic repeatedly."
|
|
8132
|
+
}
|
|
7982
8133
|
]
|
|
7983
8134
|
},
|
|
7984
8135
|
"settings": {}
|
|
@@ -7987,19 +8138,40 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
7987
8138
|
"id": "dx-section",
|
|
7988
8139
|
"type": "devex",
|
|
7989
8140
|
"data": {
|
|
7990
|
-
"anchorId": "
|
|
8141
|
+
"anchorId": "devex",
|
|
7991
8142
|
"label": "Developer Velocity",
|
|
7992
8143
|
"title": "AI-native advantage,\nfrom day one",
|
|
7993
8144
|
"description": "OlonJS dramatically increases AI-assisted development speed. Because structure is deterministic, agents scaffold and evolve tenants faster — with lower regression risk.",
|
|
7994
8145
|
"features": [
|
|
7995
|
-
{
|
|
7996
|
-
|
|
7997
|
-
|
|
8146
|
+
{
|
|
8147
|
+
"id": "df-1",
|
|
8148
|
+
"text": "AI scaffolds and evolves tenants faster because structure is deterministic"
|
|
8149
|
+
},
|
|
8150
|
+
{
|
|
8151
|
+
"id": "df-2",
|
|
8152
|
+
"text": "Shared conventions reduce prompt ambiguity and implementation drift"
|
|
8153
|
+
},
|
|
8154
|
+
{
|
|
8155
|
+
"id": "df-3",
|
|
8156
|
+
"text": "Ship new tenant experiences in hours, not weeks"
|
|
8157
|
+
}
|
|
7998
8158
|
],
|
|
7999
8159
|
"stats": [
|
|
8000
|
-
{
|
|
8001
|
-
|
|
8002
|
-
|
|
8160
|
+
{
|
|
8161
|
+
"id": "ds-1",
|
|
8162
|
+
"value": "10×",
|
|
8163
|
+
"label": "Faster scaffolding"
|
|
8164
|
+
},
|
|
8165
|
+
{
|
|
8166
|
+
"id": "ds-2",
|
|
8167
|
+
"value": "∅",
|
|
8168
|
+
"label": "Glue per tenant"
|
|
8169
|
+
},
|
|
8170
|
+
{
|
|
8171
|
+
"id": "ds-3",
|
|
8172
|
+
"value": "100%",
|
|
8173
|
+
"label": "Type-safe contracts"
|
|
8174
|
+
}
|
|
8003
8175
|
]
|
|
8004
8176
|
},
|
|
8005
8177
|
"settings": {}
|
|
@@ -8013,15 +8185,24 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
8013
8185
|
"description": "Read the full specification or explore the source on GitHub. Zero dependencies to start — one JSON endpoint per page.",
|
|
8014
8186
|
"cliCommand": "npx @olonjs/cli@latest new tenant",
|
|
8015
8187
|
"ctas": [
|
|
8016
|
-
{
|
|
8017
|
-
|
|
8188
|
+
{
|
|
8189
|
+
"id": "cta-docs",
|
|
8190
|
+
"label": "Read the Specification",
|
|
8191
|
+
"href": "/docs",
|
|
8192
|
+
"variant": "primary"
|
|
8193
|
+
},
|
|
8194
|
+
{
|
|
8195
|
+
"id": "cta-gh",
|
|
8196
|
+
"label": "View on GitHub",
|
|
8197
|
+
"href": "https://github.com/olonjs/npm-jpcore",
|
|
8198
|
+
"variant": "secondary"
|
|
8199
|
+
}
|
|
8018
8200
|
]
|
|
8019
8201
|
},
|
|
8020
8202
|
"settings": {}
|
|
8021
8203
|
}
|
|
8022
8204
|
]
|
|
8023
8205
|
}
|
|
8024
|
-
|
|
8025
8206
|
END_OF_FILE_CONTENT
|
|
8026
8207
|
echo "Creating src/data/pages/post.json..."
|
|
8027
8208
|
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/post.json"
|