@olonjs/cli 3.0.143 → 3.0.145
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/assets/src_tenant_alpha.sh +1041 -53
- package/assets/templates/alpha/src_tenant.sh +1041 -53
- package/package.json +1 -1
|
@@ -1817,7 +1817,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1817
1817
|
"@tiptap/extension-link": "^2.11.5",
|
|
1818
1818
|
"@tiptap/react": "^2.11.5",
|
|
1819
1819
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1820
|
-
"@olonjs/core": "^1.1.
|
|
1820
|
+
"@olonjs/core": "^1.1.11",
|
|
1821
1821
|
"class-variance-authority": "^0.7.1",
|
|
1822
1822
|
"clsx": "^2.1.1",
|
|
1823
1823
|
"lucide-react": "^0.474.0",
|
|
@@ -2131,7 +2131,7 @@ import { build } from 'vite';
|
|
|
2131
2131
|
import path from 'path';
|
|
2132
2132
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
2133
2133
|
import fs from 'fs/promises';
|
|
2134
|
-
import { webmcp } from '@olonjs/core';
|
|
2134
|
+
import { resolvePageMatchFromRegistry, resolvePublicPageDocument, webmcp } from '@olonjs/core';
|
|
2135
2135
|
|
|
2136
2136
|
const {
|
|
2137
2137
|
buildPageContract,
|
|
@@ -2144,8 +2144,10 @@ const {
|
|
|
2144
2144
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
2145
2145
|
const root = path.resolve(__dirname, '..');
|
|
2146
2146
|
const pagesDir = path.resolve(root, 'src/data/pages');
|
|
2147
|
+
const collectionsDir = path.resolve(root, 'src/data/collections');
|
|
2147
2148
|
const publicDir = path.resolve(root, 'public');
|
|
2148
2149
|
const distDir = path.resolve(root, 'dist');
|
|
2150
|
+
const distSsrDir = path.resolve(root, 'dist-ssr');
|
|
2149
2151
|
|
|
2150
2152
|
async function writeTargets(relativePath, content) {
|
|
2151
2153
|
const targets = [
|
|
@@ -2163,6 +2165,15 @@ async function writeJsonTargets(relativePath, value) {
|
|
|
2163
2165
|
await writeTargets(relativePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
2164
2166
|
}
|
|
2165
2167
|
|
|
2168
|
+
// Schemas go to dist-ssr ONLY: committed (dist-ssr is not gitignored) so provisioning can read
|
|
2169
|
+
// them from the repo, but NOT served as static files — so /schemas/* falls through to the
|
|
2170
|
+
// blob rewrite, and provision/hotSave own the served contract.
|
|
2171
|
+
async function writeSsrJson(relativePath, value) {
|
|
2172
|
+
const target = path.resolve(distSsrDir, relativePath);
|
|
2173
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
2174
|
+
await fs.writeFile(target, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2166
2177
|
function escapeHtmlAttribute(value) {
|
|
2167
2178
|
return String(value).replace(/&/g, '&').replace(/"/g, '"');
|
|
2168
2179
|
}
|
|
@@ -2174,6 +2185,42 @@ function toCanonicalSlug(relativeJsonPath) {
|
|
|
2174
2185
|
return slug;
|
|
2175
2186
|
}
|
|
2176
2187
|
|
|
2188
|
+
async function readJsonFile(filePath) {
|
|
2189
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
2190
|
+
return JSON.parse(raw);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
async function expandCollectionTarget(slug, pageFilePath) {
|
|
2194
|
+
if (!/\[[^\]]+\]/.test(slug)) return [slug];
|
|
2195
|
+
|
|
2196
|
+
let pageConfig;
|
|
2197
|
+
try {
|
|
2198
|
+
pageConfig = await readJsonFile(pageFilePath);
|
|
2199
|
+
} catch {
|
|
2200
|
+
return [slug];
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
const binding = pageConfig?.collection;
|
|
2204
|
+
if (!binding || typeof binding.source !== 'string' || typeof binding.paramKey !== 'string') {
|
|
2205
|
+
return [slug];
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
const token = `[${binding.paramKey}]`;
|
|
2209
|
+
if (!slug.includes(token)) return [slug];
|
|
2210
|
+
|
|
2211
|
+
const collectionPath = path.resolve(collectionsDir, binding.source, `${binding.source}.json`);
|
|
2212
|
+
let collection;
|
|
2213
|
+
try {
|
|
2214
|
+
collection = await readJsonFile(collectionPath);
|
|
2215
|
+
} catch {
|
|
2216
|
+
return [slug];
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
if (!collection || typeof collection !== 'object' || Array.isArray(collection)) return [slug];
|
|
2220
|
+
const itemIds = Object.keys(collection).sort((a, b) => a.localeCompare(b));
|
|
2221
|
+
return itemIds.length > 0 ? itemIds.map((itemId) => slug.replace(token, itemId)) : [slug];
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2177
2224
|
async function listJsonFilesRecursive(dir) {
|
|
2178
2225
|
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
2179
2226
|
const files = [];
|
|
@@ -2196,7 +2243,14 @@ async function discoverTargets() {
|
|
|
2196
2243
|
files = [];
|
|
2197
2244
|
}
|
|
2198
2245
|
|
|
2199
|
-
const rawSlugs =
|
|
2246
|
+
const rawSlugs = (
|
|
2247
|
+
await Promise.all(
|
|
2248
|
+
files.map(async (fullPath) => {
|
|
2249
|
+
const slug = toCanonicalSlug(path.relative(pagesDir, fullPath));
|
|
2250
|
+
return expandCollectionTarget(slug, fullPath);
|
|
2251
|
+
})
|
|
2252
|
+
)
|
|
2253
|
+
).flat();
|
|
2200
2254
|
const slugs = Array.from(new Set(rawSlugs)).sort((a, b) => a.localeCompare(b));
|
|
2201
2255
|
|
|
2202
2256
|
return slugs.map((slug) => {
|
|
@@ -2254,22 +2308,34 @@ const remoteStylesheetTags = getRemoteStylesheets()
|
|
|
2254
2308
|
const webMcpBuildState = getWebMcpBuildState();
|
|
2255
2309
|
|
|
2256
2310
|
for (const { slug } of targets) {
|
|
2257
|
-
const pageConfig = webMcpBuildState.pages
|
|
2311
|
+
const pageConfig = resolvePageMatchFromRegistry(webMcpBuildState.pages, slug)?.page;
|
|
2258
2312
|
if (!pageConfig) continue;
|
|
2313
|
+
const resolvedPageDocument = resolvePublicPageDocument({
|
|
2314
|
+
slug,
|
|
2315
|
+
pages: webMcpBuildState.pages,
|
|
2316
|
+
siteConfig: webMcpBuildState.siteConfig,
|
|
2317
|
+
themeConfig: webMcpBuildState.themeConfig,
|
|
2318
|
+
menuConfig: webMcpBuildState.menuConfig,
|
|
2319
|
+
collections: webMcpBuildState.collections,
|
|
2320
|
+
collectionSchemas: webMcpBuildState.collectionSchemas,
|
|
2321
|
+
refDocuments: webMcpBuildState.refDocuments,
|
|
2322
|
+
});
|
|
2323
|
+
const publicPageConfig = resolvedPageDocument?.page ?? pageConfig;
|
|
2259
2324
|
|
|
2260
|
-
// Export the
|
|
2261
|
-
await writeJsonTargets(`pages/${slug}.json`,
|
|
2325
|
+
// Export the resolved public JSON data for the agentic web (so readResource matches runtime).
|
|
2326
|
+
await writeJsonTargets(`pages/${slug}.json`, publicPageConfig);
|
|
2262
2327
|
|
|
2263
2328
|
const contract = buildPageContract({
|
|
2264
2329
|
slug,
|
|
2265
|
-
pageConfig,
|
|
2330
|
+
pageConfig: publicPageConfig,
|
|
2266
2331
|
schemas: webMcpBuildState.schemas,
|
|
2332
|
+
submissionSchemas: webMcpBuildState.submissionSchemas,
|
|
2267
2333
|
siteConfig: webMcpBuildState.siteConfig,
|
|
2268
2334
|
});
|
|
2269
|
-
await
|
|
2335
|
+
await writeSsrJson(`schemas/${slug}.schema.json`, contract);
|
|
2270
2336
|
const pageManifest = buildPageManifest({
|
|
2271
2337
|
slug,
|
|
2272
|
-
pageConfig,
|
|
2338
|
+
pageConfig: publicPageConfig,
|
|
2273
2339
|
schemas: webMcpBuildState.schemas,
|
|
2274
2340
|
siteConfig: webMcpBuildState.siteConfig,
|
|
2275
2341
|
});
|
|
@@ -2342,6 +2408,7 @@ for (const { slug, out, depth } of targets) {
|
|
|
2342
2408
|
console.log('\n[bake] All pages baked. OK\n');
|
|
2343
2409
|
|
|
2344
2410
|
END_OF_FILE_CONTENT
|
|
2411
|
+
# SKIP: scripts/bake.mjs:Zone.Identifier is binary and cannot be embedded as text.
|
|
2345
2412
|
echo "Creating scripts/generate-llms-txt.mjs..."
|
|
2346
2413
|
cat << 'END_OF_FILE_CONTENT' > "scripts/generate-llms-txt.mjs"
|
|
2347
2414
|
import fs from 'fs';
|
|
@@ -2537,6 +2604,8 @@ const __dirname = path.dirname(__filename);
|
|
|
2537
2604
|
const rootDir = path.resolve(__dirname, '..');
|
|
2538
2605
|
const sourceDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
2539
2606
|
const targetDir = path.join(rootDir, 'public', 'pages');
|
|
2607
|
+
const sourceCollectionsDir = path.join(rootDir, 'src', 'data', 'collections');
|
|
2608
|
+
const targetCollectionsDir = path.join(rootDir, 'public', 'collections');
|
|
2540
2609
|
const sourceSiteConfigPath = path.join(rootDir, 'src', 'data', 'config', 'site.json');
|
|
2541
2610
|
const targetConfigDir = path.join(rootDir, 'public', 'config');
|
|
2542
2611
|
const targetSiteConfigPath = path.join(targetConfigDir, 'site.json');
|
|
@@ -2550,12 +2619,18 @@ fs.rmSync(targetDir, { recursive: true, force: true });
|
|
|
2550
2619
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
2551
2620
|
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
|
2552
2621
|
|
|
2622
|
+
fs.rmSync(targetCollectionsDir, { recursive: true, force: true });
|
|
2623
|
+
if (fs.existsSync(sourceCollectionsDir)) {
|
|
2624
|
+
fs.mkdirSync(targetCollectionsDir, { recursive: true });
|
|
2625
|
+
fs.cpSync(sourceCollectionsDir, targetCollectionsDir, { recursive: true });
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2553
2628
|
if (fs.existsSync(sourceSiteConfigPath)) {
|
|
2554
2629
|
fs.mkdirSync(targetConfigDir, { recursive: true });
|
|
2555
2630
|
fs.cpSync(sourceSiteConfigPath, targetSiteConfigPath);
|
|
2556
2631
|
}
|
|
2557
2632
|
|
|
2558
|
-
console.log('[sync-pages-to-public] Synced pages
|
|
2633
|
+
console.log('[sync-pages-to-public] Synced pages, collections, and site config to public/');
|
|
2559
2634
|
|
|
2560
2635
|
END_OF_FILE_CONTENT
|
|
2561
2636
|
echo "Creating scripts/webmcp-feature-check.mjs..."
|
|
@@ -2873,7 +2948,8 @@ import { JsonPagesEngine } from '@olonjs/core';
|
|
|
2873
2948
|
import type { JsonPagesConfig, LibraryImageEntry, ProjectState } from '@olonjs/core';
|
|
2874
2949
|
import { normalizeBasePath, withBasePath } from '@olonjs/core';
|
|
2875
2950
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
2876
|
-
import {
|
|
2951
|
+
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
2952
|
+
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
2877
2953
|
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
2878
2954
|
import { getHydratedData } from '@/lib/draftStorage';
|
|
2879
2955
|
import type { SiteConfig, ThemeConfig, MenuConfig, PageConfig } from '@/types';
|
|
@@ -2883,6 +2959,7 @@ import { startCloudSaveStream } from '@olonjs/core';
|
|
|
2883
2959
|
import siteData from '@/data/config/site.json';
|
|
2884
2960
|
import themeData from '@/data/config/theme.json';
|
|
2885
2961
|
import menuData from '@/data/config/menu.json';
|
|
2962
|
+
import libriData from '@/data/collections/libri/libri.json';
|
|
2886
2963
|
import { getFilePages } from '@/lib/getFilePages';
|
|
2887
2964
|
import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
2888
2965
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
@@ -2914,6 +2991,9 @@ const TENANT_ID = 'alpha';
|
|
|
2914
2991
|
|
|
2915
2992
|
const filePages = getFilePages();
|
|
2916
2993
|
const fileSiteConfig = siteData as unknown as SiteConfig;
|
|
2994
|
+
const collections = {
|
|
2995
|
+
libri: libriData as unknown as Record<string, unknown>,
|
|
2996
|
+
} satisfies NonNullable<JsonPagesConfig['collections']>;
|
|
2917
2997
|
const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
|
|
2918
2998
|
const ASSET_UPLOAD_MAX_RETRIES = 2;
|
|
2919
2999
|
const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
|
|
@@ -2951,6 +3031,7 @@ type CachedCloudContent = {
|
|
|
2951
3031
|
savedAt: number;
|
|
2952
3032
|
siteConfig: unknown | null;
|
|
2953
3033
|
pages: Record<string, unknown>;
|
|
3034
|
+
collections?: JsonPagesConfig['collections'];
|
|
2954
3035
|
};
|
|
2955
3036
|
|
|
2956
3037
|
const CLOUD_CACHE_KEY = 'jp_cloud_content_cache_v1';
|
|
@@ -2996,7 +3077,7 @@ function asString(value: unknown, fallback: string): string {
|
|
|
2996
3077
|
function normalizeRouteSlug(value: string): string {
|
|
2997
3078
|
return value
|
|
2998
3079
|
.toLowerCase()
|
|
2999
|
-
.replace(/[^a-z0-9/_-]/g, '-')
|
|
3080
|
+
.replace(/[^a-z0-9/_[\]-]/g, '-')
|
|
3000
3081
|
.replace(/^\/+|\/+$/g, '') || 'home';
|
|
3001
3082
|
}
|
|
3002
3083
|
|
|
@@ -3022,6 +3103,7 @@ function coercePageConfig(slug: string, value: unknown): PageConfig | null {
|
|
|
3022
3103
|
slug: normalizedSlug,
|
|
3023
3104
|
meta: { title, description },
|
|
3024
3105
|
sections: input.sections as PageConfig['sections'],
|
|
3106
|
+
...(isObjectRecord(input.collection) ? { collection: input.collection as unknown as PageConfig['collection'] } : {}),
|
|
3025
3107
|
...(typeof input['global-header'] === 'boolean' ? { 'global-header': input['global-header'] } : {}),
|
|
3026
3108
|
};
|
|
3027
3109
|
}
|
|
@@ -3037,6 +3119,7 @@ function coerceSiteConfig(value: unknown): SiteConfig | null {
|
|
|
3037
3119
|
}
|
|
3038
3120
|
if (!isObjectRecord(input)) return null;
|
|
3039
3121
|
if (!isObjectRecord(input.identity)) return null;
|
|
3122
|
+
if (!Array.isArray(input.pages)) return null;
|
|
3040
3123
|
|
|
3041
3124
|
return input as unknown as SiteConfig;
|
|
3042
3125
|
}
|
|
@@ -3162,7 +3245,7 @@ function normalizeSlugForCache(slug: string): string {
|
|
|
3162
3245
|
slug
|
|
3163
3246
|
.trim()
|
|
3164
3247
|
.toLowerCase()
|
|
3165
|
-
.replace(/[^a-z0-9/_-]/g, '-')
|
|
3248
|
+
.replace(/[^a-z0-9/_[\]-]/g, '-')
|
|
3166
3249
|
.replace(/^\/+|\/+$/g, '') || 'home'
|
|
3167
3250
|
);
|
|
3168
3251
|
}
|
|
@@ -3727,14 +3810,15 @@ function App() {
|
|
|
3727
3810
|
basePath: APP_BASE_PATH,
|
|
3728
3811
|
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
3729
3812
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
3730
|
-
|
|
3813
|
+
collectionSchemas: CollectionRegistry as unknown as JsonPagesConfig['collectionSchemas'],
|
|
3731
3814
|
pages,
|
|
3732
3815
|
siteConfig,
|
|
3733
3816
|
themeConfig,
|
|
3734
3817
|
menuConfig,
|
|
3818
|
+
collections,
|
|
3735
3819
|
refDocuments,
|
|
3736
|
-
iconRegistry: iconMap,
|
|
3737
3820
|
themeCss: { tenant: resolvedTenantCss },
|
|
3821
|
+
iconRegistry: iconMap,
|
|
3738
3822
|
addSection: addSectionConfig,
|
|
3739
3823
|
webmcp: {
|
|
3740
3824
|
enabled: true,
|
|
@@ -3768,6 +3852,7 @@ function App() {
|
|
|
3768
3852
|
slug,
|
|
3769
3853
|
page: state.page,
|
|
3770
3854
|
siteConfig: state.site,
|
|
3855
|
+
collections: state.collections,
|
|
3771
3856
|
}),
|
|
3772
3857
|
});
|
|
3773
3858
|
const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
|
|
@@ -3781,6 +3866,7 @@ function App() {
|
|
|
3781
3866
|
keyFingerprint,
|
|
3782
3867
|
savedAt: Date.now(),
|
|
3783
3868
|
siteConfig: state.site ?? null,
|
|
3869
|
+
collections: state.collections,
|
|
3784
3870
|
pages: {
|
|
3785
3871
|
...(existing?.pages ?? {}),
|
|
3786
3872
|
[normalizedSlug]: state.page,
|
|
@@ -3950,13 +4036,7 @@ function App() {
|
|
|
3950
4036
|
</div>
|
|
3951
4037
|
</div>
|
|
3952
4038
|
) : null}
|
|
3953
|
-
{shouldRenderEngine ? (
|
|
3954
|
-
isTenantEmpty ? (
|
|
3955
|
-
<EmptyTenantView />
|
|
3956
|
-
) : (
|
|
3957
|
-
<JsonPagesEngine config={config} />
|
|
3958
|
-
)
|
|
3959
|
-
) : null}
|
|
4039
|
+
{shouldRenderEngine ? (isTenantEmpty ? <EmptyTenantView /> : <JsonPagesEngine config={config} />) : null}
|
|
3960
4040
|
{isCloudMode && (contentMode === 'error' || contentFallback?.reasonCode === 'CLOUD_REFRESH_FAILED') ? (
|
|
3961
4041
|
<div
|
|
3962
4042
|
role="status"
|
|
@@ -4031,6 +4111,40 @@ function App() {
|
|
|
4031
4111
|
|
|
4032
4112
|
export default App;
|
|
4033
4113
|
|
|
4114
|
+
END_OF_FILE_CONTENT
|
|
4115
|
+
# SKIP: src/App.tsx:Zone.Identifier is binary and cannot be embedded as text.
|
|
4116
|
+
mkdir -p "src/collections"
|
|
4117
|
+
mkdir -p "src/collections/libri"
|
|
4118
|
+
echo "Creating src/collections/libri/index.ts..."
|
|
4119
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/index.ts"
|
|
4120
|
+
export { LibroSchema, LibriCollectionSchema } from './schema';
|
|
4121
|
+
export type { Libro, LibriCollection } from './types';
|
|
4122
|
+
|
|
4123
|
+
END_OF_FILE_CONTENT
|
|
4124
|
+
echo "Creating src/collections/libri/schema.ts..."
|
|
4125
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/schema.ts"
|
|
4126
|
+
import { z } from 'zod';
|
|
4127
|
+
import { BaseCollectionItem } from '@olonjs/core';
|
|
4128
|
+
|
|
4129
|
+
export const LibroSchema = BaseCollectionItem.extend({
|
|
4130
|
+
title: z.string().describe('ui:text'),
|
|
4131
|
+
author: z.string().describe('ui:text'),
|
|
4132
|
+
year: z.number().describe('ui:number'),
|
|
4133
|
+
genre: z.string().describe('ui:text'),
|
|
4134
|
+
summary: z.string().describe('ui:textarea'),
|
|
4135
|
+
});
|
|
4136
|
+
|
|
4137
|
+
export const LibriCollectionSchema = z.record(z.string(), LibroSchema);
|
|
4138
|
+
|
|
4139
|
+
END_OF_FILE_CONTENT
|
|
4140
|
+
echo "Creating src/collections/libri/types.ts..."
|
|
4141
|
+
cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/types.ts"
|
|
4142
|
+
import { z } from 'zod';
|
|
4143
|
+
import { LibroSchema, LibriCollectionSchema } from './schema';
|
|
4144
|
+
|
|
4145
|
+
export type Libro = z.infer<typeof LibroSchema>;
|
|
4146
|
+
export type LibriCollection = z.infer<typeof LibriCollectionSchema>;
|
|
4147
|
+
|
|
4034
4148
|
END_OF_FILE_CONTENT
|
|
4035
4149
|
mkdir -p "src/components"
|
|
4036
4150
|
echo "Creating src/components/ThemeProvider.tsx..."
|
|
@@ -4095,6 +4209,221 @@ export function useTheme() {
|
|
|
4095
4209
|
return useContext(ThemeContext)
|
|
4096
4210
|
}
|
|
4097
4211
|
|
|
4212
|
+
END_OF_FILE_CONTENT
|
|
4213
|
+
mkdir -p "src/components/book-detail"
|
|
4214
|
+
echo "Creating src/components/book-detail/View.tsx..."
|
|
4215
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/View.tsx"
|
|
4216
|
+
import type { BookDetailData } from './types';
|
|
4217
|
+
|
|
4218
|
+
type BookDetailViewProps = {
|
|
4219
|
+
data: BookDetailData;
|
|
4220
|
+
};
|
|
4221
|
+
|
|
4222
|
+
export function BookDetailView({ data }: BookDetailViewProps) {
|
|
4223
|
+
const book = data.item;
|
|
4224
|
+
|
|
4225
|
+
return (
|
|
4226
|
+
<main className="min-h-screen bg-background text-foreground px-6 py-16">
|
|
4227
|
+
<article
|
|
4228
|
+
data-jp-field="item"
|
|
4229
|
+
data-jp-item-id={book.id}
|
|
4230
|
+
data-jp-item-field="item"
|
|
4231
|
+
className="mx-auto w-full max-w-3xl rounded-2xl border border-border bg-card p-8 shadow-sm"
|
|
4232
|
+
>
|
|
4233
|
+
<a
|
|
4234
|
+
href="/libri"
|
|
4235
|
+
className="text-sm font-medium text-muted-foreground hover:text-foreground"
|
|
4236
|
+
>
|
|
4237
|
+
{data.backLabel}
|
|
4238
|
+
</a>
|
|
4239
|
+
<p className="mt-10 text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
|
4240
|
+
{book.genre} · {book.year}
|
|
4241
|
+
</p>
|
|
4242
|
+
<h1 className="mt-3 text-4xl font-semibold tracking-tight sm:text-5xl">
|
|
4243
|
+
{book.title}
|
|
4244
|
+
</h1>
|
|
4245
|
+
<p className="mt-4 text-lg text-muted-foreground">
|
|
4246
|
+
{book.author}
|
|
4247
|
+
</p>
|
|
4248
|
+
<p className="mt-8 text-base leading-8 text-muted-foreground">
|
|
4249
|
+
{book.summary}
|
|
4250
|
+
</p>
|
|
4251
|
+
</article>
|
|
4252
|
+
</main>
|
|
4253
|
+
);
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
END_OF_FILE_CONTENT
|
|
4257
|
+
echo "Creating src/components/book-detail/index.ts..."
|
|
4258
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/index.ts"
|
|
4259
|
+
export * from './View';
|
|
4260
|
+
export * from './schema';
|
|
4261
|
+
export * from './types';
|
|
4262
|
+
|
|
4263
|
+
END_OF_FILE_CONTENT
|
|
4264
|
+
echo "Creating src/components/book-detail/schema.ts..."
|
|
4265
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/schema.ts"
|
|
4266
|
+
import { z } from 'zod';
|
|
4267
|
+
import { BaseSectionData } from '@olonjs/core';
|
|
4268
|
+
import { LibroSchema } from '@/collections/libri';
|
|
4269
|
+
|
|
4270
|
+
export const BookDetailSchema = BaseSectionData.extend({
|
|
4271
|
+
item: LibroSchema.describe('ui:collection-ref'),
|
|
4272
|
+
backLabel: z.string().default('Torna ai libri').describe('ui:text'),
|
|
4273
|
+
});
|
|
4274
|
+
|
|
4275
|
+
export const BookDetailSettingsSchema = z.object({});
|
|
4276
|
+
|
|
4277
|
+
END_OF_FILE_CONTENT
|
|
4278
|
+
echo "Creating src/components/book-detail/types.ts..."
|
|
4279
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/types.ts"
|
|
4280
|
+
import { z } from 'zod';
|
|
4281
|
+
import { BookDetailSchema, BookDetailSettingsSchema } from './schema';
|
|
4282
|
+
|
|
4283
|
+
export type BookDetailData = z.infer<typeof BookDetailSchema>;
|
|
4284
|
+
export type BookDetailSettings = z.infer<typeof BookDetailSettingsSchema>;
|
|
4285
|
+
|
|
4286
|
+
END_OF_FILE_CONTENT
|
|
4287
|
+
mkdir -p "src/components/books-list"
|
|
4288
|
+
echo "Creating src/components/books-list/View.tsx..."
|
|
4289
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/View.tsx"
|
|
4290
|
+
import { useMemo, useState } from 'react';
|
|
4291
|
+
import type { Libro } from '@/collections/libri';
|
|
4292
|
+
import type { BooksListData } from './types';
|
|
4293
|
+
|
|
4294
|
+
type BooksListViewProps = {
|
|
4295
|
+
data: BooksListData;
|
|
4296
|
+
};
|
|
4297
|
+
|
|
4298
|
+
function toBooks(items: BooksListData['items']): Libro[] {
|
|
4299
|
+
return Object.values(items ?? {}).sort((a, b) => a.title.localeCompare(b.title));
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
export function BooksListView({ data }: BooksListViewProps) {
|
|
4303
|
+
const books = useMemo(() => toBooks(data.items), [data.items]);
|
|
4304
|
+
const pageSize = Math.max(1, Math.floor(data.pageSize || 10));
|
|
4305
|
+
const totalPages = Math.max(1, Math.ceil(books.length / pageSize));
|
|
4306
|
+
const [page, setPage] = useState(1);
|
|
4307
|
+
const currentPage = Math.min(page, totalPages);
|
|
4308
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
4309
|
+
const visibleBooks = books.slice(startIndex, startIndex + pageSize);
|
|
4310
|
+
|
|
4311
|
+
return (
|
|
4312
|
+
<main className="min-h-screen bg-background text-foreground px-6 py-16">
|
|
4313
|
+
<section className="mx-auto flex w-full max-w-5xl flex-col gap-10">
|
|
4314
|
+
<div className="max-w-2xl">
|
|
4315
|
+
{data.eyebrow && (
|
|
4316
|
+
<p
|
|
4317
|
+
data-jp-field="eyebrow"
|
|
4318
|
+
className="text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground"
|
|
4319
|
+
>
|
|
4320
|
+
{data.eyebrow}
|
|
4321
|
+
</p>
|
|
4322
|
+
)}
|
|
4323
|
+
<h1
|
|
4324
|
+
data-jp-field="title"
|
|
4325
|
+
className="mt-3 text-4xl font-semibold tracking-tight sm:text-5xl"
|
|
4326
|
+
>
|
|
4327
|
+
{data.title}
|
|
4328
|
+
</h1>
|
|
4329
|
+
{data.description && (
|
|
4330
|
+
<p
|
|
4331
|
+
data-jp-field="description"
|
|
4332
|
+
className="mt-4 text-base leading-7 text-muted-foreground"
|
|
4333
|
+
>
|
|
4334
|
+
{data.description}
|
|
4335
|
+
</p>
|
|
4336
|
+
)}
|
|
4337
|
+
</div>
|
|
4338
|
+
|
|
4339
|
+
<div data-jp-field="items" className="grid gap-4">
|
|
4340
|
+
{visibleBooks.map((book) => (
|
|
4341
|
+
<article
|
|
4342
|
+
key={book.id}
|
|
4343
|
+
data-jp-item-id={book.id}
|
|
4344
|
+
data-jp-item-field="items"
|
|
4345
|
+
className="rounded-xl border border-border bg-card p-5 shadow-sm"
|
|
4346
|
+
>
|
|
4347
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
4348
|
+
<div>
|
|
4349
|
+
<h2 className="text-xl font-semibold">{book.title}</h2>
|
|
4350
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
4351
|
+
{book.author} · {book.year} · {book.genre}
|
|
4352
|
+
</p>
|
|
4353
|
+
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
4354
|
+
{book.summary}
|
|
4355
|
+
</p>
|
|
4356
|
+
</div>
|
|
4357
|
+
<a
|
|
4358
|
+
href={`/libri/${book.id}`}
|
|
4359
|
+
className="inline-flex shrink-0 items-center justify-center rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-muted"
|
|
4360
|
+
>
|
|
4361
|
+
Apri scheda
|
|
4362
|
+
</a>
|
|
4363
|
+
</div>
|
|
4364
|
+
</article>
|
|
4365
|
+
))}
|
|
4366
|
+
</div>
|
|
4367
|
+
|
|
4368
|
+
<nav className="flex items-center justify-between border-t border-border pt-6 text-sm">
|
|
4369
|
+
<button
|
|
4370
|
+
type="button"
|
|
4371
|
+
onClick={() => setPage((value) => Math.max(1, value - 1))}
|
|
4372
|
+
disabled={currentPage === 1}
|
|
4373
|
+
className="rounded-md border border-border px-3 py-2 font-medium disabled:cursor-not-allowed disabled:opacity-40 hover:bg-muted"
|
|
4374
|
+
>
|
|
4375
|
+
Precedente
|
|
4376
|
+
</button>
|
|
4377
|
+
<span className="text-muted-foreground">
|
|
4378
|
+
Pagina {currentPage} di {totalPages} · {books.length} libri
|
|
4379
|
+
</span>
|
|
4380
|
+
<button
|
|
4381
|
+
type="button"
|
|
4382
|
+
onClick={() => setPage((value) => Math.min(totalPages, value + 1))}
|
|
4383
|
+
disabled={currentPage === totalPages}
|
|
4384
|
+
className="rounded-md border border-border px-3 py-2 font-medium disabled:cursor-not-allowed disabled:opacity-40 hover:bg-muted"
|
|
4385
|
+
>
|
|
4386
|
+
Successiva
|
|
4387
|
+
</button>
|
|
4388
|
+
</nav>
|
|
4389
|
+
</section>
|
|
4390
|
+
</main>
|
|
4391
|
+
);
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
END_OF_FILE_CONTENT
|
|
4395
|
+
echo "Creating src/components/books-list/index.ts..."
|
|
4396
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/index.ts"
|
|
4397
|
+
export * from './View';
|
|
4398
|
+
export * from './schema';
|
|
4399
|
+
export * from './types';
|
|
4400
|
+
|
|
4401
|
+
END_OF_FILE_CONTENT
|
|
4402
|
+
echo "Creating src/components/books-list/schema.ts..."
|
|
4403
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/schema.ts"
|
|
4404
|
+
import { z } from 'zod';
|
|
4405
|
+
import { BaseSectionData } from '@olonjs/core';
|
|
4406
|
+
import { LibroSchema } from '@/collections/libri';
|
|
4407
|
+
|
|
4408
|
+
export const BooksListSchema = BaseSectionData.extend({
|
|
4409
|
+
eyebrow: z.string().optional().describe('ui:text'),
|
|
4410
|
+
title: z.string().describe('ui:text'),
|
|
4411
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
4412
|
+
items: z.record(z.string(), LibroSchema).describe('ui:collection-ref'),
|
|
4413
|
+
pageSize: z.number().default(10).describe('ui:number'),
|
|
4414
|
+
});
|
|
4415
|
+
|
|
4416
|
+
export const BooksListSettingsSchema = z.object({});
|
|
4417
|
+
|
|
4418
|
+
END_OF_FILE_CONTENT
|
|
4419
|
+
echo "Creating src/components/books-list/types.ts..."
|
|
4420
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/types.ts"
|
|
4421
|
+
import { z } from 'zod';
|
|
4422
|
+
import { BooksListSchema, BooksListSettingsSchema } from './schema';
|
|
4423
|
+
|
|
4424
|
+
export type BooksListData = z.infer<typeof BooksListSchema>;
|
|
4425
|
+
export type BooksListSettings = z.infer<typeof BooksListSettingsSchema>;
|
|
4426
|
+
|
|
4098
4427
|
END_OF_FILE_CONTENT
|
|
4099
4428
|
mkdir -p "src/components/empty-tenant"
|
|
4100
4429
|
echo "Creating src/components/empty-tenant/View.tsx..."
|
|
@@ -4148,6 +4477,123 @@ import { EmptyTenantSchema, EmptyTenantSettingsSchema } from './schema';
|
|
|
4148
4477
|
export type EmptyTenantData = z.infer<typeof EmptyTenantSchema>;
|
|
4149
4478
|
export type EmptyTenantSettings = z.infer<typeof EmptyTenantSettingsSchema>;
|
|
4150
4479
|
|
|
4480
|
+
END_OF_FILE_CONTENT
|
|
4481
|
+
mkdir -p "src/components/footer"
|
|
4482
|
+
echo "Creating src/components/footer/View.tsx..."
|
|
4483
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/View.tsx"
|
|
4484
|
+
import type React from 'react';
|
|
4485
|
+
import type { FooterData, FooterSettings } from './types';
|
|
4486
|
+
|
|
4487
|
+
type FooterViewProps = {
|
|
4488
|
+
data: FooterData;
|
|
4489
|
+
settings?: FooterSettings;
|
|
4490
|
+
};
|
|
4491
|
+
|
|
4492
|
+
export function FooterView({ data, settings }: FooterViewProps) {
|
|
4493
|
+
const links = data.links ?? [];
|
|
4494
|
+
|
|
4495
|
+
return (
|
|
4496
|
+
<footer
|
|
4497
|
+
style={{
|
|
4498
|
+
'--local-bg': 'var(--background)',
|
|
4499
|
+
'--local-text': 'var(--foreground)',
|
|
4500
|
+
'--local-muted': 'var(--muted-foreground)',
|
|
4501
|
+
'--local-border': 'var(--border)',
|
|
4502
|
+
} as React.CSSProperties}
|
|
4503
|
+
className="border-t border-[var(--local-border)] bg-[var(--local-bg)] px-6 py-10 text-[var(--local-text)]"
|
|
4504
|
+
>
|
|
4505
|
+
<div className="mx-auto flex w-full max-w-5xl flex-col gap-8 md:flex-row md:items-end md:justify-between">
|
|
4506
|
+
<div className="max-w-md">
|
|
4507
|
+
<div className="flex items-center gap-3">
|
|
4508
|
+
{settings?.showLogo !== false && (
|
|
4509
|
+
<span className="flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--local-border)] text-sm font-semibold">
|
|
4510
|
+
O
|
|
4511
|
+
</span>
|
|
4512
|
+
)}
|
|
4513
|
+
<p data-jp-field="brandText" className="text-lg font-semibold tracking-tight">
|
|
4514
|
+
{data.brandText}
|
|
4515
|
+
</p>
|
|
4516
|
+
</div>
|
|
4517
|
+
{data.description && (
|
|
4518
|
+
<p data-jp-field="description" className="mt-3 text-sm leading-6 text-[var(--local-muted)]">
|
|
4519
|
+
{data.description}
|
|
4520
|
+
</p>
|
|
4521
|
+
)}
|
|
4522
|
+
<p data-jp-field="copyright" className="mt-4 text-xs text-[var(--local-muted)]">
|
|
4523
|
+
{data.copyright}
|
|
4524
|
+
</p>
|
|
4525
|
+
</div>
|
|
4526
|
+
|
|
4527
|
+
<nav data-jp-field="links" className="flex flex-wrap gap-4 text-sm">
|
|
4528
|
+
{links.map((link) => (
|
|
4529
|
+
<a
|
|
4530
|
+
key={link.id}
|
|
4531
|
+
data-jp-item-id={link.id}
|
|
4532
|
+
data-jp-item-field="links"
|
|
4533
|
+
href={link.href}
|
|
4534
|
+
target={link.external ? '_blank' : undefined}
|
|
4535
|
+
rel={link.external ? 'noreferrer' : undefined}
|
|
4536
|
+
className="text-[var(--local-muted)] transition-colors hover:text-[var(--local-text)]"
|
|
4537
|
+
>
|
|
4538
|
+
{link.label}
|
|
4539
|
+
</a>
|
|
4540
|
+
))}
|
|
4541
|
+
{data.designSystemHref && (
|
|
4542
|
+
<a
|
|
4543
|
+
data-jp-field="designSystemHref"
|
|
4544
|
+
href={data.designSystemHref}
|
|
4545
|
+
className="text-[var(--local-muted)] transition-colors hover:text-[var(--local-text)]"
|
|
4546
|
+
>
|
|
4547
|
+
Design system
|
|
4548
|
+
</a>
|
|
4549
|
+
)}
|
|
4550
|
+
</nav>
|
|
4551
|
+
</div>
|
|
4552
|
+
</footer>
|
|
4553
|
+
);
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
END_OF_FILE_CONTENT
|
|
4557
|
+
echo "Creating src/components/footer/index.ts..."
|
|
4558
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/index.ts"
|
|
4559
|
+
export * from './View';
|
|
4560
|
+
export * from './schema';
|
|
4561
|
+
export * from './types';
|
|
4562
|
+
|
|
4563
|
+
END_OF_FILE_CONTENT
|
|
4564
|
+
echo "Creating src/components/footer/schema.ts..."
|
|
4565
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/schema.ts"
|
|
4566
|
+
import { z } from 'zod';
|
|
4567
|
+
import { BaseArrayItem, BaseSectionData } from '@olonjs/core';
|
|
4568
|
+
|
|
4569
|
+
export const FooterLinkSchema = BaseArrayItem.extend({
|
|
4570
|
+
id: z.string(),
|
|
4571
|
+
label: z.string().describe('ui:text'),
|
|
4572
|
+
href: z.string().describe('ui:text'),
|
|
4573
|
+
external: z.boolean().optional(),
|
|
4574
|
+
});
|
|
4575
|
+
|
|
4576
|
+
export const FooterSchema = BaseSectionData.extend({
|
|
4577
|
+
brandText: z.string().describe('ui:text'),
|
|
4578
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
4579
|
+
copyright: z.string().describe('ui:text'),
|
|
4580
|
+
links: z.array(FooterLinkSchema).default([]).describe('ui:list'),
|
|
4581
|
+
designSystemHref: z.string().optional().describe('ui:text'),
|
|
4582
|
+
});
|
|
4583
|
+
|
|
4584
|
+
export const FooterSettingsSchema = z.object({
|
|
4585
|
+
showLogo: z.boolean().default(true),
|
|
4586
|
+
});
|
|
4587
|
+
|
|
4588
|
+
END_OF_FILE_CONTENT
|
|
4589
|
+
echo "Creating src/components/footer/types.ts..."
|
|
4590
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/footer/types.ts"
|
|
4591
|
+
import { z } from 'zod';
|
|
4592
|
+
import { FooterSchema, FooterSettingsSchema } from './schema';
|
|
4593
|
+
|
|
4594
|
+
export type FooterData = z.infer<typeof FooterSchema>;
|
|
4595
|
+
export type FooterSettings = z.infer<typeof FooterSettingsSchema>;
|
|
4596
|
+
|
|
4151
4597
|
END_OF_FILE_CONTENT
|
|
4152
4598
|
mkdir -p "src/components/form-demo"
|
|
4153
4599
|
echo "Creating src/components/form-demo/Untitled..."
|
|
@@ -8560,6 +9006,253 @@ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
|
|
8560
9006
|
END_OF_FILE_CONTENT
|
|
8561
9007
|
# SKIP: src/components/ui/tooltip.tsx:Zone.Identifier is binary and cannot be embedded as text.
|
|
8562
9008
|
mkdir -p "src/data"
|
|
9009
|
+
mkdir -p "src/data/collections"
|
|
9010
|
+
mkdir -p "src/data/collections/libri"
|
|
9011
|
+
echo "Creating src/data/collections/libri/libri.json..."
|
|
9012
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/collections/libri/libri.json"
|
|
9013
|
+
{
|
|
9014
|
+
"1984": {
|
|
9015
|
+
"id": "1984",
|
|
9016
|
+
"title": "1984",
|
|
9017
|
+
"author": "Guido Seiero",
|
|
9018
|
+
"year": 1949,
|
|
9019
|
+
"genre": "Distopia",
|
|
9020
|
+
"summary": "Un regime totalitario controlla linguaggio, memoria e pensiero."
|
|
9021
|
+
},
|
|
9022
|
+
"il-nome-della-rosa": {
|
|
9023
|
+
"id": "il-nome-della-rosa",
|
|
9024
|
+
"title": "Il nome della rosa",
|
|
9025
|
+
"author": "Umberto Eco",
|
|
9026
|
+
"year": 1980,
|
|
9027
|
+
"genre": "Romanzo storico",
|
|
9028
|
+
"summary": "Un'indagine in un'abbazia medievale diventa una riflessione su conoscenza, potere e interpretazione."
|
|
9029
|
+
},
|
|
9030
|
+
"se-questo-e-un-uomo": {
|
|
9031
|
+
"id": "se-questo-e-un-uomo",
|
|
9032
|
+
"title": "Se questo e un uomo",
|
|
9033
|
+
"author": "Primo Levi",
|
|
9034
|
+
"year": 1947,
|
|
9035
|
+
"genre": "Memoria",
|
|
9036
|
+
"summary": "La testimonianza essenziale di Levi sull'esperienza del lager e sulla dignita umana."
|
|
9037
|
+
},
|
|
9038
|
+
"le-citta-invisibili": {
|
|
9039
|
+
"id": "le-citta-invisibili",
|
|
9040
|
+
"title": "Le citta invisibili",
|
|
9041
|
+
"author": "Italo Calvino",
|
|
9042
|
+
"year": 1972,
|
|
9043
|
+
"genre": "Letteratura fantastica",
|
|
9044
|
+
"summary": "Marco Polo racconta a Kublai Khan citta immaginarie che parlano di memoria, desiderio e linguaggio."
|
|
9045
|
+
},
|
|
9046
|
+
"il-gattopardo": {
|
|
9047
|
+
"id": "il-gattopardo",
|
|
9048
|
+
"title": "Il Gattopardo",
|
|
9049
|
+
"author": "Giuseppe Tomasi di Lampedusa",
|
|
9050
|
+
"year": 1958,
|
|
9051
|
+
"genre": "Romanzo storico",
|
|
9052
|
+
"summary": "Il tramonto dell'aristocrazia siciliana durante l'unificazione italiana."
|
|
9053
|
+
},
|
|
9054
|
+
"la-coscienza-di-zeno": {
|
|
9055
|
+
"id": "la-coscienza-di-zeno",
|
|
9056
|
+
"title": "La coscienza di Zeno",
|
|
9057
|
+
"author": "Italo Svevo",
|
|
9058
|
+
"year": 1923,
|
|
9059
|
+
"genre": "Romanzo psicologico",
|
|
9060
|
+
"summary": "Un diario ironico e nevrotico attraversa memoria, terapia e autoinganno."
|
|
9061
|
+
},
|
|
9062
|
+
"i-promessi-sposi": {
|
|
9063
|
+
"id": "i-promessi-sposi",
|
|
9064
|
+
"title": "I promessi sposi",
|
|
9065
|
+
"author": "Alessandro Manzoni",
|
|
9066
|
+
"year": 1842,
|
|
9067
|
+
"genre": "Classico",
|
|
9068
|
+
"summary": "La vicenda di Renzo e Lucia dentro carestia, guerra, peste e provvidenza."
|
|
9069
|
+
},
|
|
9070
|
+
"il-barone-rampante": {
|
|
9071
|
+
"id": "il-barone-rampante",
|
|
9072
|
+
"title": "Il barone rampante",
|
|
9073
|
+
"author": "Italo Calvino",
|
|
9074
|
+
"year": 1957,
|
|
9075
|
+
"genre": "Romanzo filosofico",
|
|
9076
|
+
"summary": "Cosimo sceglie di vivere sugli alberi e trasforma la distanza in una forma di liberta."
|
|
9077
|
+
},
|
|
9078
|
+
"gomorra": {
|
|
9079
|
+
"id": "gomorra",
|
|
9080
|
+
"title": "Gomorra",
|
|
9081
|
+
"author": "Roberto Saviano",
|
|
9082
|
+
"year": 2006,
|
|
9083
|
+
"genre": "Inchiesta",
|
|
9084
|
+
"summary": "Un reportage narrativo sulle economie e le violenze del sistema camorristico."
|
|
9085
|
+
},
|
|
9086
|
+
"oceano-mare": {
|
|
9087
|
+
"id": "oceano-mare",
|
|
9088
|
+
"title": "Oceano mare",
|
|
9089
|
+
"author": "Alessandro Baricco",
|
|
9090
|
+
"year": 1993,
|
|
9091
|
+
"genre": "Romanzo letterario",
|
|
9092
|
+
"summary": "Storie diverse si incontrano in una locanda sul mare, tra cura, naufragio e mistero."
|
|
9093
|
+
},
|
|
9094
|
+
"lessico-famigliare": {
|
|
9095
|
+
"id": "lessico-famigliare",
|
|
9096
|
+
"title": "Lessico famigliare",
|
|
9097
|
+
"author": "Natalia Ginzburg",
|
|
9098
|
+
"year": 1963,
|
|
9099
|
+
"genre": "Memoria narrativa",
|
|
9100
|
+
"summary": "Una famiglia prende forma attraverso parole, tic linguistici e memoria civile."
|
|
9101
|
+
},
|
|
9102
|
+
"cent-anni-di-solitudine": {
|
|
9103
|
+
"id": "cent-anni-di-solitudine",
|
|
9104
|
+
"title": "Cent'anni di solitudine",
|
|
9105
|
+
"author": "Gabriel Garcia Marquez",
|
|
9106
|
+
"year": 1967,
|
|
9107
|
+
"genre": "Realismo magico",
|
|
9108
|
+
"summary": "La saga dei Buendia e di Macondo intreccia mito, storia e destino."
|
|
9109
|
+
},
|
|
9110
|
+
"fahrenheit-451": {
|
|
9111
|
+
"id": "fahrenheit-451",
|
|
9112
|
+
"title": "Fahrenheit 451",
|
|
9113
|
+
"author": "Ray Bradbury",
|
|
9114
|
+
"year": 1953,
|
|
9115
|
+
"genre": "Distopia",
|
|
9116
|
+
"summary": "In un futuro dove i libri bruciano, leggere diventa un atto di resistenza."
|
|
9117
|
+
},
|
|
9118
|
+
"dune": {
|
|
9119
|
+
"id": "dune",
|
|
9120
|
+
"title": "Dune",
|
|
9121
|
+
"author": "Frank Herbert",
|
|
9122
|
+
"year": 1965,
|
|
9123
|
+
"genre": "Fantascienza",
|
|
9124
|
+
"summary": "Politica, ecologia e messianismo si scontrano sul pianeta desertico Arrakis."
|
|
9125
|
+
},
|
|
9126
|
+
"neuromancer": {
|
|
9127
|
+
"id": "neuromancer",
|
|
9128
|
+
"title": "Neuromancer",
|
|
9129
|
+
"author": "William Gibson",
|
|
9130
|
+
"year": 1984,
|
|
9131
|
+
"genre": "Cyberpunk",
|
|
9132
|
+
"summary": "Un hacker decaduto viene trascinato in un colpo che attraversa cyberspazio e intelligenze artificiali."
|
|
9133
|
+
},
|
|
9134
|
+
"il-signore-degli-anelli": {
|
|
9135
|
+
"id": "il-signore-degli-anelli",
|
|
9136
|
+
"title": "Il Signore degli Anelli",
|
|
9137
|
+
"author": "J. R. R. Tolkien",
|
|
9138
|
+
"year": 1954,
|
|
9139
|
+
"genre": "Fantasy",
|
|
9140
|
+
"summary": "La Compagnia affronta il potere dell'Anello in una delle grandi epopee moderne."
|
|
9141
|
+
},
|
|
9142
|
+
"harry-potter-e-la-pietra-filosofale": {
|
|
9143
|
+
"id": "harry-potter-e-la-pietra-filosofale",
|
|
9144
|
+
"title": "Harry Potter e la pietra filosofale",
|
|
9145
|
+
"author": "J. K. Rowling",
|
|
9146
|
+
"year": 1997,
|
|
9147
|
+
"genre": "Fantasy",
|
|
9148
|
+
"summary": "Un ragazzo scopre il mondo magico e il proprio posto in una storia piu grande."
|
|
9149
|
+
},
|
|
9150
|
+
"orgoglio-e-pregiudizio": {
|
|
9151
|
+
"id": "orgoglio-e-pregiudizio",
|
|
9152
|
+
"title": "Orgoglio e pregiudizio",
|
|
9153
|
+
"author": "Jane Austen",
|
|
9154
|
+
"year": 1813,
|
|
9155
|
+
"genre": "Classico",
|
|
9156
|
+
"summary": "Elizabeth Bennet e Mr. Darcy si misurano con classe, carattere e giudizio sociale."
|
|
9157
|
+
},
|
|
9158
|
+
"moby-dick": {
|
|
9159
|
+
"id": "moby-dick",
|
|
9160
|
+
"title": "Moby Dick",
|
|
9161
|
+
"author": "Herman Melville",
|
|
9162
|
+
"year": 1851,
|
|
9163
|
+
"genre": "Avventura",
|
|
9164
|
+
"summary": "La caccia alla balena bianca diventa ossessione metafisica e viaggio nell'abisso."
|
|
9165
|
+
},
|
|
9166
|
+
"delitto-e-castigo": {
|
|
9167
|
+
"id": "delitto-e-castigo",
|
|
9168
|
+
"title": "Delitto e castigo",
|
|
9169
|
+
"author": "Fedor Dostoevskij",
|
|
9170
|
+
"year": 1866,
|
|
9171
|
+
"genre": "Romanzo psicologico",
|
|
9172
|
+
"summary": "Raskolnikov attraversa colpa, febbre morale e possibilita di redenzione."
|
|
9173
|
+
},
|
|
9174
|
+
"anna-karenina": {
|
|
9175
|
+
"id": "anna-karenina",
|
|
9176
|
+
"title": "Anna Karenina",
|
|
9177
|
+
"author": "Lev Tolstoj",
|
|
9178
|
+
"year": 1877,
|
|
9179
|
+
"genre": "Classico",
|
|
9180
|
+
"summary": "Una storia d'amore e rovina dentro la societa russa dell'Ottocento."
|
|
9181
|
+
},
|
|
9182
|
+
"il-maestro-e-margherita": {
|
|
9183
|
+
"id": "il-maestro-e-margherita",
|
|
9184
|
+
"title": "Il maestro e Margherita",
|
|
9185
|
+
"author": "Michail Bulgakov",
|
|
9186
|
+
"year": 1967,
|
|
9187
|
+
"genre": "Satira fantastica",
|
|
9188
|
+
"summary": "Il diavolo visita Mosca in un romanzo visionario su arte, censura e amore."
|
|
9189
|
+
},
|
|
9190
|
+
"la-strada": {
|
|
9191
|
+
"id": "la-strada",
|
|
9192
|
+
"title": "La strada",
|
|
9193
|
+
"author": "Cormac McCarthy",
|
|
9194
|
+
"year": 2006,
|
|
9195
|
+
"genre": "Post-apocalittico",
|
|
9196
|
+
"summary": "Padre e figlio attraversano un mondo bruciato portando con se una fragile idea di bene."
|
|
9197
|
+
},
|
|
9198
|
+
"cloud-atlas": {
|
|
9199
|
+
"id": "cloud-atlas",
|
|
9200
|
+
"title": "Cloud Atlas",
|
|
9201
|
+
"author": "David Mitchell",
|
|
9202
|
+
"year": 2004,
|
|
9203
|
+
"genre": "Romanzo corale",
|
|
9204
|
+
"summary": "Sei storie in epoche diverse compongono una meditazione su potere, memoria e reincorrenza."
|
|
9205
|
+
},
|
|
9206
|
+
"kafka-sulla-spiaggia": {
|
|
9207
|
+
"id": "kafka-sulla-spiaggia",
|
|
9208
|
+
"title": "Kafka sulla spiaggia",
|
|
9209
|
+
"author": "Haruki Murakami",
|
|
9210
|
+
"year": 2002,
|
|
9211
|
+
"genre": "Surrealismo",
|
|
9212
|
+
"summary": "Fuga, destino e sogno si intrecciano in un romanzo sospeso tra reale e mitico."
|
|
9213
|
+
},
|
|
9214
|
+
"americanah": {
|
|
9215
|
+
"id": "americanah",
|
|
9216
|
+
"title": "Americanah",
|
|
9217
|
+
"author": "Chimamanda Ngozi Adichie",
|
|
9218
|
+
"year": 2013,
|
|
9219
|
+
"genre": "Romanzo contemporaneo",
|
|
9220
|
+
"summary": "Migrazione, razza e identita raccontate attraverso una storia d'amore tra Nigeria e Stati Uniti."
|
|
9221
|
+
},
|
|
9222
|
+
"la-casa-degli-spiriti": {
|
|
9223
|
+
"id": "la-casa-degli-spiriti",
|
|
9224
|
+
"title": "La casa degli spiriti",
|
|
9225
|
+
"author": "Isabel Allende",
|
|
9226
|
+
"year": 1982,
|
|
9227
|
+
"genre": "Saga familiare",
|
|
9228
|
+
"summary": "La storia della famiglia Trueba intreccia politica, memoria e realismo magico."
|
|
9229
|
+
},
|
|
9230
|
+
"beloved": {
|
|
9231
|
+
"id": "beloved",
|
|
9232
|
+
"title": "Beloved",
|
|
9233
|
+
"author": "Toni Morrison",
|
|
9234
|
+
"year": 1987,
|
|
9235
|
+
"genre": "Romanzo storico",
|
|
9236
|
+
"summary": "Il trauma della schiavitu ritorna come presenza viva nella casa di Sethe."
|
|
9237
|
+
},
|
|
9238
|
+
"la-breve-favolosa-vita-di-oscar-wao": {
|
|
9239
|
+
"id": "la-breve-favolosa-vita-di-oscar-wao",
|
|
9240
|
+
"title": "La breve favolosa vita di Oscar Wao",
|
|
9241
|
+
"author": "Junot Diaz",
|
|
9242
|
+
"year": 2007,
|
|
9243
|
+
"genre": "Romanzo contemporaneo",
|
|
9244
|
+
"summary": "Famiglia, diaspora dominicana e cultura pop si intrecciano nella storia di Oscar."
|
|
9245
|
+
},
|
|
9246
|
+
"le-correzioni": {
|
|
9247
|
+
"id": "le-correzioni",
|
|
9248
|
+
"title": "Le correzioni",
|
|
9249
|
+
"author": "Jonathan Franzen",
|
|
9250
|
+
"year": 2001,
|
|
9251
|
+
"genre": "Romanzo familiare",
|
|
9252
|
+
"summary": "Una famiglia americana tenta di ritrovarsi mentre ciascuno affronta le proprie fratture."
|
|
9253
|
+
}
|
|
9254
|
+
}
|
|
9255
|
+
END_OF_FILE_CONTENT
|
|
8563
9256
|
mkdir -p "src/data/config"
|
|
8564
9257
|
echo "Creating src/data/config/menu.json..."
|
|
8565
9258
|
cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
|
|
@@ -8585,6 +9278,24 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
|
|
|
8585
9278
|
"label": "GitHub",
|
|
8586
9279
|
"href": "https://github.com/olonjs/core"
|
|
8587
9280
|
}
|
|
9281
|
+
],
|
|
9282
|
+
"footer": [
|
|
9283
|
+
{
|
|
9284
|
+
"id": "footer-home",
|
|
9285
|
+
"label": "Home",
|
|
9286
|
+
"href": "/"
|
|
9287
|
+
},
|
|
9288
|
+
{
|
|
9289
|
+
"id": "footer-libri",
|
|
9290
|
+
"label": "Libri",
|
|
9291
|
+
"href": "/libri"
|
|
9292
|
+
},
|
|
9293
|
+
{
|
|
9294
|
+
"id": "footer-github",
|
|
9295
|
+
"label": "GitHub",
|
|
9296
|
+
"href": "https://github.com/olonjs/core",
|
|
9297
|
+
"external": true
|
|
9298
|
+
}
|
|
8588
9299
|
]
|
|
8589
9300
|
}
|
|
8590
9301
|
END_OF_FILE_CONTENT
|
|
@@ -8640,7 +9351,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
8640
9351
|
"title": "OlonJS",
|
|
8641
9352
|
"logoUrl": "/brand/mark/olon-mark-dark.svg"
|
|
8642
9353
|
},
|
|
8643
|
-
|
|
8644
9354
|
"header": {
|
|
8645
9355
|
"id": "global-header",
|
|
8646
9356
|
"type": "header",
|
|
@@ -8679,13 +9389,11 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
8679
9389
|
"type": "footer",
|
|
8680
9390
|
"data": {
|
|
8681
9391
|
"brandText": "OlonJS",
|
|
9392
|
+
"description": "AI-native content infrastructure for deterministic, sovereign, git-backed sites.",
|
|
8682
9393
|
"copyright": "© 2026 OlonJS · v1.5 · Guido Serio",
|
|
8683
|
-
"links":
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
"href": "https://github.com/olonjs/core"
|
|
8687
|
-
}
|
|
8688
|
-
],
|
|
9394
|
+
"links": {
|
|
9395
|
+
"$ref": "../config/menu.json#/footer"
|
|
9396
|
+
},
|
|
8689
9397
|
"designSystemHref": ""
|
|
8690
9398
|
},
|
|
8691
9399
|
"settings": {
|
|
@@ -8889,6 +9597,65 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
|
8889
9597
|
"global-header": false
|
|
8890
9598
|
}
|
|
8891
9599
|
END_OF_FILE_CONTENT
|
|
9600
|
+
mkdir -p "src/data/pages/libri"
|
|
9601
|
+
echo "Creating src/data/pages/libri.json..."
|
|
9602
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/libri.json"
|
|
9603
|
+
{
|
|
9604
|
+
"id": "libri-page",
|
|
9605
|
+
"slug": "libri",
|
|
9606
|
+
"meta": {
|
|
9607
|
+
"title": "Libri",
|
|
9608
|
+
"description": "Catalogo libri dimostrativo alimentato da COP collections."
|
|
9609
|
+
},
|
|
9610
|
+
"sections": [
|
|
9611
|
+
{
|
|
9612
|
+
"id": "books-list-1",
|
|
9613
|
+
"type": "books-list",
|
|
9614
|
+
"data": {
|
|
9615
|
+
"anchorId": "catalogo-libri",
|
|
9616
|
+
"eyebrow": "Collection demo",
|
|
9617
|
+
"title": "Libri",
|
|
9618
|
+
"description": "Una pagina collection con 30 titoli, paginazione lato componente e link alle schede dinamiche.",
|
|
9619
|
+
"items": {
|
|
9620
|
+
"$ref": "../collections/libri/libri.json"
|
|
9621
|
+
},
|
|
9622
|
+
"pageSize": 10
|
|
9623
|
+
}
|
|
9624
|
+
}
|
|
9625
|
+
],
|
|
9626
|
+
"global-header": false
|
|
9627
|
+
}
|
|
9628
|
+
|
|
9629
|
+
END_OF_FILE_CONTENT
|
|
9630
|
+
echo "Creating src/data/pages/libri/[slug].json..."
|
|
9631
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/libri/[slug].json"
|
|
9632
|
+
{
|
|
9633
|
+
"id": "libro-detail-page",
|
|
9634
|
+
"slug": "libri/[slug]",
|
|
9635
|
+
"meta": {
|
|
9636
|
+
"title": "Dettaglio libro",
|
|
9637
|
+
"description": "Pagina dinamica dettaglio libro alimentata dalla collection libri."
|
|
9638
|
+
},
|
|
9639
|
+
"sections": [
|
|
9640
|
+
{
|
|
9641
|
+
"id": "book-detail-1",
|
|
9642
|
+
"type": "book-detail",
|
|
9643
|
+
"data": {
|
|
9644
|
+
"anchorId": "dettaglio-libro",
|
|
9645
|
+
"item": {
|
|
9646
|
+
"$ref": "collection:current"
|
|
9647
|
+
},
|
|
9648
|
+
"backLabel": "← Torna ai libri"
|
|
9649
|
+
}
|
|
9650
|
+
}
|
|
9651
|
+
],
|
|
9652
|
+
"collection": {
|
|
9653
|
+
"source": "libri",
|
|
9654
|
+
"paramKey": "slug"
|
|
9655
|
+
},
|
|
9656
|
+
"global-header": false
|
|
9657
|
+
}
|
|
9658
|
+
END_OF_FILE_CONTENT
|
|
8892
9659
|
mkdir -p "src/emails"
|
|
8893
9660
|
echo "Creating src/emails/LeadNotificationEmail.tsx..."
|
|
8894
9661
|
cat << 'END_OF_FILE_CONTENT' > "src/emails/LeadNotificationEmail.tsx"
|
|
@@ -9206,12 +9973,19 @@ echo "Creating src/entry-ssg.tsx..."
|
|
|
9206
9973
|
cat << 'END_OF_FILE_CONTENT' > "src/entry-ssg.tsx"
|
|
9207
9974
|
import { renderToString } from 'react-dom/server';
|
|
9208
9975
|
import { StaticRouter } from 'react-router-dom/server';
|
|
9209
|
-
import {
|
|
9976
|
+
import {
|
|
9977
|
+
ConfigProvider,
|
|
9978
|
+
PageRenderer,
|
|
9979
|
+
StudioProvider,
|
|
9980
|
+
contract,
|
|
9981
|
+
resolvePageMatchFromRegistry,
|
|
9982
|
+
resolveRuntimeConfig,
|
|
9983
|
+
} from '@olonjs/core';
|
|
9210
9984
|
import type { JsonPagesConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
9211
9985
|
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
9212
9986
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
9213
9987
|
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
9214
|
-
import { menuConfig, pages, refDocuments, siteConfig, themeConfig } from '@/runtime';
|
|
9988
|
+
import { collectionSchemas, collections, menuConfig, pages, refDocuments, siteConfig, themeConfig } from '@/runtime';
|
|
9215
9989
|
import tenantCss from '@/index.css?inline';
|
|
9216
9990
|
|
|
9217
9991
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -9226,10 +10000,16 @@ function getSortedSlugs(): string[] {
|
|
|
9226
10000
|
return Object.keys(pages).sort((a, b) => a.localeCompare(b));
|
|
9227
10001
|
}
|
|
9228
10002
|
|
|
9229
|
-
function resolvePage(slug: string): { slug: string; page: PageConfig } {
|
|
10003
|
+
function resolvePage(slug: string): { slug: string; registrySlug: string; page: PageConfig; params: Record<string, string> } {
|
|
9230
10004
|
const normalized = normalizeSlug(slug);
|
|
9231
|
-
|
|
9232
|
-
|
|
10005
|
+
const pageMatch = resolvePageMatchFromRegistry(pages, normalized);
|
|
10006
|
+
if (pageMatch) {
|
|
10007
|
+
return {
|
|
10008
|
+
slug: normalized || pageMatch.registrySlug,
|
|
10009
|
+
registrySlug: pageMatch.registrySlug,
|
|
10010
|
+
page: pageMatch.page,
|
|
10011
|
+
params: pageMatch.params,
|
|
10012
|
+
};
|
|
9233
10013
|
}
|
|
9234
10014
|
|
|
9235
10015
|
const slugs = getSortedSlugs();
|
|
@@ -9239,7 +10019,7 @@ function resolvePage(slug: string): { slug: string; page: PageConfig } {
|
|
|
9239
10019
|
|
|
9240
10020
|
const home = slugs.find((item) => item === 'home');
|
|
9241
10021
|
const fallbackSlug = home ?? slugs[0];
|
|
9242
|
-
return { slug: fallbackSlug, page: pages[fallbackSlug] };
|
|
10022
|
+
return { slug: fallbackSlug, registrySlug: fallbackSlug, page: pages[fallbackSlug], params: {} };
|
|
9243
10023
|
}
|
|
9244
10024
|
|
|
9245
10025
|
function flattenThemeTokens(
|
|
@@ -9327,14 +10107,18 @@ function resolveTenantId(): string {
|
|
|
9327
10107
|
export function render(slug: string): string {
|
|
9328
10108
|
const resolved = resolvePage(slug);
|
|
9329
10109
|
const location = resolved.slug === 'home' ? '/' : `/${resolved.slug}`;
|
|
10110
|
+
const collectionContext = contract.resolveCollectionContext(resolved.page, resolved.params, collections);
|
|
9330
10111
|
const resolvedRuntime = resolveRuntimeConfig({
|
|
9331
|
-
pages,
|
|
10112
|
+
pages: { [resolved.registrySlug]: resolved.page },
|
|
9332
10113
|
siteConfig,
|
|
9333
10114
|
themeConfig,
|
|
9334
10115
|
menuConfig,
|
|
10116
|
+
collections,
|
|
10117
|
+
collectionSchemas,
|
|
10118
|
+
collectionContext,
|
|
9335
10119
|
refDocuments,
|
|
9336
10120
|
});
|
|
9337
|
-
const resolvedPage = resolvedRuntime.pages[resolved.
|
|
10121
|
+
const resolvedPage = resolvedRuntime.pages[resolved.registrySlug] ?? resolved.page;
|
|
9338
10122
|
|
|
9339
10123
|
return renderToString(
|
|
9340
10124
|
<StaticRouter location={location}>
|
|
@@ -9384,12 +10168,22 @@ export function getPageMeta(slug: string): { title: string; description: string
|
|
|
9384
10168
|
export function getWebMcpBuildState(): {
|
|
9385
10169
|
pages: Record<string, PageConfig>;
|
|
9386
10170
|
schemas: JsonPagesConfig['schemas'];
|
|
10171
|
+
collections: JsonPagesConfig['collections'];
|
|
10172
|
+
collectionSchemas: JsonPagesConfig['collectionSchemas'];
|
|
9387
10173
|
siteConfig: SiteConfig;
|
|
10174
|
+
themeConfig: ThemeConfig;
|
|
10175
|
+
menuConfig: JsonPagesConfig['menuConfig'];
|
|
10176
|
+
refDocuments: JsonPagesConfig['refDocuments'];
|
|
9388
10177
|
} {
|
|
9389
10178
|
return {
|
|
9390
10179
|
pages,
|
|
9391
10180
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
10181
|
+
collections,
|
|
10182
|
+
collectionSchemas,
|
|
9392
10183
|
siteConfig,
|
|
10184
|
+
themeConfig,
|
|
10185
|
+
menuConfig,
|
|
10186
|
+
refDocuments,
|
|
9393
10187
|
};
|
|
9394
10188
|
}
|
|
9395
10189
|
|
|
@@ -10337,17 +11131,34 @@ cat << 'END_OF_FILE_CONTENT' > "src/index.css"
|
|
|
10337
11131
|
|
|
10338
11132
|
END_OF_FILE_CONTENT
|
|
10339
11133
|
mkdir -p "src/lib"
|
|
11134
|
+
echo "Creating src/lib/CollectionRegistry.ts..."
|
|
11135
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/CollectionRegistry.ts"
|
|
11136
|
+
import { LibriCollectionSchema } from '@/collections/libri';
|
|
11137
|
+
|
|
11138
|
+
export const CollectionRegistry = {
|
|
11139
|
+
libri: LibriCollectionSchema,
|
|
11140
|
+
} as const;
|
|
11141
|
+
|
|
11142
|
+
export type CollectionType = keyof typeof CollectionRegistry;
|
|
11143
|
+
|
|
11144
|
+
END_OF_FILE_CONTENT
|
|
10340
11145
|
echo "Creating src/lib/ComponentRegistry.tsx..."
|
|
10341
11146
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/ComponentRegistry.tsx"
|
|
10342
11147
|
import type { SectionType } from '@/types';
|
|
10343
11148
|
import type { SectionComponentPropsMap } from '@/types';
|
|
11149
|
+
import { BookDetailView } from '@/components/book-detail';
|
|
11150
|
+
import { BooksListView } from '@/components/books-list';
|
|
10344
11151
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
11152
|
+
import { FooterView } from '@/components/footer';
|
|
10345
11153
|
import { FormDemoView } from '@/components/form-demo';
|
|
10346
11154
|
|
|
10347
11155
|
export const ComponentRegistry: {
|
|
10348
11156
|
[K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
|
|
10349
11157
|
} = {
|
|
11158
|
+
'book-detail': BookDetailView as React.FC<SectionComponentPropsMap['book-detail']>,
|
|
11159
|
+
'books-list': BooksListView as React.FC<SectionComponentPropsMap['books-list']>,
|
|
10350
11160
|
'empty-tenant': EmptyTenantView as React.FC<SectionComponentPropsMap['empty-tenant']>,
|
|
11161
|
+
footer: FooterView as React.FC<SectionComponentPropsMap['footer']>,
|
|
10351
11162
|
'form-demo': FormDemoView as React.FC<SectionComponentPropsMap['form-demo']>,
|
|
10352
11163
|
};
|
|
10353
11164
|
|
|
@@ -10416,20 +11227,44 @@ echo "Creating src/lib/addSectionConfig.ts..."
|
|
|
10416
11227
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/addSectionConfig.ts"
|
|
10417
11228
|
import type { AddSectionConfig } from '@olonjs/core';
|
|
10418
11229
|
|
|
10419
|
-
const addableSectionTypes = ['empty-tenant', 'form-demo'] as const;
|
|
11230
|
+
const addableSectionTypes = ['book-detail', 'books-list', 'empty-tenant', 'footer', 'form-demo'] as const;
|
|
10420
11231
|
|
|
10421
11232
|
const sectionTypeLabels: Record<string, string> = {
|
|
11233
|
+
'book-detail': 'Book Detail',
|
|
11234
|
+
'books-list': 'Books List',
|
|
10422
11235
|
'empty-tenant': 'Empty Tenant',
|
|
11236
|
+
footer: 'Footer',
|
|
10423
11237
|
'form-demo': 'Form Demo',
|
|
10424
11238
|
};
|
|
10425
11239
|
|
|
10426
11240
|
function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
10427
11241
|
switch (type) {
|
|
11242
|
+
case 'book-detail':
|
|
11243
|
+
return {
|
|
11244
|
+
item: { $ref: 'collection:current' },
|
|
11245
|
+
backLabel: 'Torna ai libri',
|
|
11246
|
+
};
|
|
11247
|
+
case 'books-list':
|
|
11248
|
+
return {
|
|
11249
|
+
eyebrow: 'Collection demo',
|
|
11250
|
+
title: 'Libri',
|
|
11251
|
+
description: 'Catalogo dimostrativo alimentato dalla collection libri.',
|
|
11252
|
+
items: { $ref: '../collections/libri/libri.json' },
|
|
11253
|
+
pageSize: 10,
|
|
11254
|
+
};
|
|
10428
11255
|
case 'empty-tenant':
|
|
10429
11256
|
return {
|
|
10430
11257
|
title: 'Your tenant is empty.',
|
|
10431
11258
|
description: 'Create your first page to start building your site.',
|
|
10432
11259
|
};
|
|
11260
|
+
case 'footer':
|
|
11261
|
+
return {
|
|
11262
|
+
brandText: 'OlonJS',
|
|
11263
|
+
description: 'AI-native content infrastructure for deterministic, git-backed sites.',
|
|
11264
|
+
copyright: '© 2026 OlonJS',
|
|
11265
|
+
links: [],
|
|
11266
|
+
designSystemHref: '',
|
|
11267
|
+
};
|
|
10433
11268
|
case 'form-demo':
|
|
10434
11269
|
return {
|
|
10435
11270
|
title: 'Contattaci',
|
|
@@ -10528,11 +11363,17 @@ export function getFilePages(): Record<string, PageConfig> {
|
|
|
10528
11363
|
END_OF_FILE_CONTENT
|
|
10529
11364
|
echo "Creating src/lib/schemas.ts..."
|
|
10530
11365
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
11366
|
+
import { BookDetailSchema } from '@/components/book-detail';
|
|
11367
|
+
import { BooksListSchema } from '@/components/books-list';
|
|
10531
11368
|
import { EmptyTenantSchema } from '@/components/empty-tenant';
|
|
11369
|
+
import { FooterSchema } from '@/components/footer';
|
|
10532
11370
|
import { FormDemoSchema, FormDemoSubmissionSchema } from '@/components/form-demo';
|
|
10533
11371
|
|
|
10534
11372
|
export const SECTION_SCHEMAS = {
|
|
11373
|
+
'book-detail': BookDetailSchema,
|
|
11374
|
+
'books-list': BooksListSchema,
|
|
10535
11375
|
'empty-tenant': EmptyTenantSchema,
|
|
11376
|
+
footer: FooterSchema,
|
|
10536
11377
|
'form-demo': FormDemoSchema,
|
|
10537
11378
|
} as const;
|
|
10538
11379
|
|
|
@@ -10552,6 +11393,7 @@ export type SectionType = keyof typeof SECTION_SCHEMAS;
|
|
|
10552
11393
|
export {
|
|
10553
11394
|
BaseSectionData,
|
|
10554
11395
|
BaseArrayItem,
|
|
11396
|
+
BaseCollectionItem,
|
|
10555
11397
|
BaseSectionSettingsSchema,
|
|
10556
11398
|
CtaSchema,
|
|
10557
11399
|
ImageSelectionSchema,
|
|
@@ -10810,16 +11652,22 @@ END_OF_FILE_CONTENT
|
|
|
10810
11652
|
echo "Creating src/runtime.ts..."
|
|
10811
11653
|
cat << 'END_OF_FILE_CONTENT' > "src/runtime.ts"
|
|
10812
11654
|
import type { JsonPagesConfig, MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
10813
|
-
import {
|
|
11655
|
+
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
11656
|
+
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
10814
11657
|
import { getFilePages } from '@/lib/getFilePages';
|
|
10815
11658
|
import siteData from '@/data/config/site.json';
|
|
10816
11659
|
import menuData from '@/data/config/menu.json';
|
|
10817
11660
|
import themeData from '@/data/config/theme.json';
|
|
11661
|
+
import libriData from '@/data/collections/libri/libri.json';
|
|
10818
11662
|
|
|
10819
11663
|
export const siteConfig = siteData as unknown as SiteConfig;
|
|
10820
11664
|
export const themeConfig = themeData as unknown as ThemeConfig;
|
|
10821
11665
|
export const menuConfig = menuData as unknown as MenuConfig;
|
|
10822
11666
|
export const pages = getFilePages();
|
|
11667
|
+
export const collections = {
|
|
11668
|
+
libri: libriData as unknown as Record<string, unknown>,
|
|
11669
|
+
} satisfies NonNullable<JsonPagesConfig['collections']>;
|
|
11670
|
+
export const collectionSchemas = CollectionRegistry as unknown as JsonPagesConfig['collectionSchemas'];
|
|
10823
11671
|
export const refDocuments = {
|
|
10824
11672
|
'menu.json': menuConfig,
|
|
10825
11673
|
'config/menu.json': menuConfig,
|
|
@@ -10829,42 +11677,122 @@ export const refDocuments = {
|
|
|
10829
11677
|
export function getWebMcpBuildState(): {
|
|
10830
11678
|
pages: Record<string, PageConfig>;
|
|
10831
11679
|
schemas: JsonPagesConfig['schemas'];
|
|
10832
|
-
|
|
11680
|
+
collectionSchemas: JsonPagesConfig['collectionSchemas'];
|
|
11681
|
+
collections: JsonPagesConfig['collections'];
|
|
10833
11682
|
siteConfig: SiteConfig;
|
|
11683
|
+
themeConfig: ThemeConfig;
|
|
11684
|
+
menuConfig: MenuConfig;
|
|
11685
|
+
refDocuments: JsonPagesConfig['refDocuments'];
|
|
10834
11686
|
} {
|
|
10835
11687
|
return {
|
|
10836
11688
|
pages,
|
|
10837
11689
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
10838
|
-
|
|
11690
|
+
collectionSchemas,
|
|
11691
|
+
collections,
|
|
10839
11692
|
siteConfig,
|
|
11693
|
+
themeConfig,
|
|
11694
|
+
menuConfig,
|
|
11695
|
+
refDocuments,
|
|
10840
11696
|
};
|
|
10841
11697
|
}
|
|
10842
11698
|
|
|
10843
11699
|
END_OF_FILE_CONTENT
|
|
11700
|
+
# SKIP: src/runtime.ts:Zone.Identifier is binary and cannot be embedded as text.
|
|
10844
11701
|
echo "Creating src/types.ts..."
|
|
10845
11702
|
cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
|
|
11703
|
+
import type { BookDetailData, BookDetailSettings } from '@/components/book-detail';
|
|
11704
|
+
import type { BooksListData, BooksListSettings } from '@/components/books-list';
|
|
10846
11705
|
import type { EmptyTenantData, EmptyTenantSettings } from '@/components/empty-tenant';
|
|
11706
|
+
import type { FooterData, FooterSettings } from '@/components/footer';
|
|
10847
11707
|
import type { FormDemoData, FormDemoSettings } from '@/components/form-demo';
|
|
11708
|
+
import type { Libro } from '@/collections/libri';
|
|
10848
11709
|
|
|
10849
11710
|
export type SectionComponentPropsMap = {
|
|
11711
|
+
'book-detail': { data: BookDetailData; settings?: BookDetailSettings };
|
|
11712
|
+
'books-list': { data: BooksListData; settings?: BooksListSettings };
|
|
10850
11713
|
'empty-tenant': { data: EmptyTenantData; settings?: EmptyTenantSettings };
|
|
11714
|
+
footer: { data: FooterData; settings?: FooterSettings };
|
|
10851
11715
|
'form-demo': { data: FormDemoData; settings?: FormDemoSettings };
|
|
10852
11716
|
};
|
|
10853
11717
|
|
|
10854
11718
|
declare module '@olonjs/core' {
|
|
10855
11719
|
export interface SectionDataRegistry {
|
|
11720
|
+
'book-detail': BookDetailData;
|
|
11721
|
+
'books-list': BooksListData;
|
|
10856
11722
|
'empty-tenant': EmptyTenantData;
|
|
11723
|
+
footer: FooterData;
|
|
10857
11724
|
'form-demo': FormDemoData;
|
|
10858
11725
|
}
|
|
10859
11726
|
export interface SectionSettingsRegistry {
|
|
11727
|
+
'book-detail': BookDetailSettings;
|
|
11728
|
+
'books-list': BooksListSettings;
|
|
10860
11729
|
'empty-tenant': EmptyTenantSettings;
|
|
11730
|
+
footer: FooterSettings;
|
|
10861
11731
|
'form-demo': FormDemoSettings;
|
|
10862
11732
|
}
|
|
11733
|
+
export interface CollectionItemRegistry {
|
|
11734
|
+
libri: Libro;
|
|
11735
|
+
}
|
|
10863
11736
|
}
|
|
10864
11737
|
|
|
10865
11738
|
export * from '@olonjs/core';
|
|
10866
11739
|
|
|
10867
11740
|
END_OF_FILE_CONTENT
|
|
11741
|
+
echo "Creating src/vercel.json..."
|
|
11742
|
+
cat << 'END_OF_FILE_CONTENT' > "src/vercel.json"
|
|
11743
|
+
{
|
|
11744
|
+
"rewrites": [
|
|
11745
|
+
{
|
|
11746
|
+
"source": "/robots.txt",
|
|
11747
|
+
"destination": "https://bat5elmxofxdroan.public.blob.vercel-storage.com/tenants/santa1/robots.txt"
|
|
11748
|
+
},
|
|
11749
|
+
{
|
|
11750
|
+
"source": "/sitemap.xml",
|
|
11751
|
+
"destination": "https://bat5elmxofxdroan.public.blob.vercel-storage.com/tenants/santa1/sitemap.xml"
|
|
11752
|
+
},
|
|
11753
|
+
{
|
|
11754
|
+
"source": "/llms.txt",
|
|
11755
|
+
"destination": "https://bat5elmxofxdroan.public.blob.vercel-storage.com/tenants/santa1/llms.txt"
|
|
11756
|
+
},
|
|
11757
|
+
{
|
|
11758
|
+
"source": "/.well-known/agent-card.json",
|
|
11759
|
+
"destination": "https://bat5elmxofxdroan.public.blob.vercel-storage.com/tenants/santa1/.well-known/agent-card.json"
|
|
11760
|
+
},
|
|
11761
|
+
{
|
|
11762
|
+
"source": "/mcp-manifest.json",
|
|
11763
|
+
"destination": "https://bat5elmxofxdroan.public.blob.vercel-storage.com/tenants/santa1/mcp-manifest.json"
|
|
11764
|
+
},
|
|
11765
|
+
{
|
|
11766
|
+
"source": "/mcp-manifests/:path*.json",
|
|
11767
|
+
"destination": "https://bat5elmxofxdroan.public.blob.vercel-storage.com/tenants/santa1/mcp-manifests/:path*.json"
|
|
11768
|
+
},
|
|
11769
|
+
{
|
|
11770
|
+
"source": "/schemas/:path*.json",
|
|
11771
|
+
"destination": "https://bat5elmxofxdroan.public.blob.vercel-storage.com/tenants/santa1/schemas/:path*.json"
|
|
11772
|
+
},
|
|
11773
|
+
{
|
|
11774
|
+
"source": "/:path*.json",
|
|
11775
|
+
"destination": "https://bat5elmxofxdroan.public.blob.vercel-storage.com/tenants/santa1/pages/:path*.json"
|
|
11776
|
+
},
|
|
11777
|
+
{
|
|
11778
|
+
"source": "/(.*)",
|
|
11779
|
+
"destination": "/index.html"
|
|
11780
|
+
}
|
|
11781
|
+
],
|
|
11782
|
+
"headers": [
|
|
11783
|
+
{
|
|
11784
|
+
"source": "/assets/(.*)",
|
|
11785
|
+
"headers": [
|
|
11786
|
+
{
|
|
11787
|
+
"key": "Cache-Control",
|
|
11788
|
+
"value": "public, max-age=31536000, immutable"
|
|
11789
|
+
}
|
|
11790
|
+
]
|
|
11791
|
+
}
|
|
11792
|
+
]
|
|
11793
|
+
}
|
|
11794
|
+
END_OF_FILE_CONTENT
|
|
11795
|
+
# SKIP: src/vercel.json:Zone.Identifier is binary and cannot be embedded as text.
|
|
10868
11796
|
echo "Creating src/vite-env.d.ts..."
|
|
10869
11797
|
cat << 'END_OF_FILE_CONTENT' > "src/vite-env.d.ts"
|
|
10870
11798
|
/// <reference types="vite/client" />
|
|
@@ -10966,6 +11894,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
10966
11894
|
const ASSETS_IMAGES_DIR = path.resolve(__dirname, 'public', 'assets', 'images');
|
|
10967
11895
|
const DATA_CONFIG_DIR = path.resolve(__dirname, 'src', 'data', 'config');
|
|
10968
11896
|
const DATA_PAGES_DIR = path.resolve(__dirname, 'src', 'data', 'pages');
|
|
11897
|
+
const DATA_COLLECTIONS_DIR = path.resolve(__dirname, 'src', 'data', 'collections');
|
|
10969
11898
|
const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']);
|
|
10970
11899
|
const IMAGE_MIMES = new Set([
|
|
10971
11900
|
'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/avif',
|
|
@@ -11002,6 +11931,20 @@ function sendJsonFile(res, filePath) {
|
|
|
11002
11931
|
}
|
|
11003
11932
|
}
|
|
11004
11933
|
|
|
11934
|
+
function safeDataSlugPath(rootDir, rawSlug, fallback) {
|
|
11935
|
+
const slug = String(rawSlug || fallback)
|
|
11936
|
+
.replace(/\\/g, '/')
|
|
11937
|
+
.replace(/^\/+|\/+$/g, '');
|
|
11938
|
+
const segments = slug
|
|
11939
|
+
.split('/')
|
|
11940
|
+
.map((segment) => segment.replace(/[^a-zA-Z0-9_[\]-]/g, '_'))
|
|
11941
|
+
.filter(Boolean);
|
|
11942
|
+
const candidate = path.resolve(rootDir, `${segments.join(path.sep) || fallback}.json`);
|
|
11943
|
+
const isInsideRoot = candidate.startsWith(`${rootDir}${path.sep}`) || candidate === rootDir;
|
|
11944
|
+
if (!isInsideRoot) throw new Error('Invalid data path');
|
|
11945
|
+
return candidate;
|
|
11946
|
+
}
|
|
11947
|
+
|
|
11005
11948
|
function isTenantPageJsonRequest(req, pathname) {
|
|
11006
11949
|
if (req.method !== 'GET' || !pathname.endsWith('.json')) return false;
|
|
11007
11950
|
const viteOrStaticPrefixes = ['/api/', '/assets/', '/src/', '/node_modules/', '/public/', '/@'];
|
|
@@ -11059,15 +12002,23 @@ export default defineConfig({
|
|
|
11059
12002
|
const pageManifestMatch = pathname.match(/^\/mcp-manifests\/(.+)\.json$/i);
|
|
11060
12003
|
if (pageManifestMatch && req.method === 'GET') {
|
|
11061
12004
|
const slug = normalizeManifestSlug(pageManifestMatch[1]);
|
|
11062
|
-
const
|
|
11063
|
-
|
|
12005
|
+
const resolved = core.resolvePublicPageDocument({
|
|
12006
|
+
slug,
|
|
12007
|
+
pages: buildState.pages,
|
|
12008
|
+
siteConfig: buildState.siteConfig,
|
|
12009
|
+
themeConfig: buildState.themeConfig,
|
|
12010
|
+
menuConfig: buildState.menuConfig,
|
|
12011
|
+
collections: buildState.collections,
|
|
12012
|
+
refDocuments: buildState.refDocuments,
|
|
12013
|
+
});
|
|
12014
|
+
if (!resolved) {
|
|
11064
12015
|
sendJson(res, 404, { error: 'Page manifest not found' });
|
|
11065
12016
|
return true;
|
|
11066
12017
|
}
|
|
11067
12018
|
|
|
11068
12019
|
sendJson(res, 200, buildPageManifest({
|
|
11069
12020
|
slug,
|
|
11070
|
-
pageConfig,
|
|
12021
|
+
pageConfig: resolved.page,
|
|
11071
12022
|
schemas: buildState.schemas,
|
|
11072
12023
|
submissionSchemas: buildState.submissionSchemas,
|
|
11073
12024
|
siteConfig: buildState.siteConfig,
|
|
@@ -11078,15 +12029,23 @@ export default defineConfig({
|
|
|
11078
12029
|
const schemaMatch = pathname.match(/^\/schemas\/(.+)\.schema\.json$/i);
|
|
11079
12030
|
if (schemaMatch && req.method === 'GET') {
|
|
11080
12031
|
const slug = normalizeManifestSlug(schemaMatch[1]);
|
|
11081
|
-
const
|
|
11082
|
-
|
|
12032
|
+
const resolved = core.resolvePublicPageDocument({
|
|
12033
|
+
slug,
|
|
12034
|
+
pages: buildState.pages,
|
|
12035
|
+
siteConfig: buildState.siteConfig,
|
|
12036
|
+
themeConfig: buildState.themeConfig,
|
|
12037
|
+
menuConfig: buildState.menuConfig,
|
|
12038
|
+
collections: buildState.collections,
|
|
12039
|
+
refDocuments: buildState.refDocuments,
|
|
12040
|
+
});
|
|
12041
|
+
if (!resolved) {
|
|
11083
12042
|
sendJson(res, 404, { error: 'Schema contract not found' });
|
|
11084
12043
|
return true;
|
|
11085
12044
|
}
|
|
11086
12045
|
|
|
11087
12046
|
sendJson(res, 200, buildPageContract({
|
|
11088
12047
|
slug,
|
|
11089
|
-
pageConfig,
|
|
12048
|
+
pageConfig: resolved.page,
|
|
11090
12049
|
schemas: buildState.schemas,
|
|
11091
12050
|
submissionSchemas: buildState.submissionSchemas,
|
|
11092
12051
|
siteConfig: buildState.siteConfig,
|
|
@@ -11127,13 +12086,31 @@ export default defineConfig({
|
|
|
11127
12086
|
.replace(/^pages\//i, '')
|
|
11128
12087
|
.replace(/\.json$/i, '')
|
|
11129
12088
|
.replace(/^\/+|\/+$/g, '');
|
|
11130
|
-
|
|
11131
|
-
const isInsidePagesDir = candidate.startsWith(`${DATA_PAGES_DIR}${path.sep}`) || candidate === DATA_PAGES_DIR;
|
|
11132
|
-
if (!slug || !isInsidePagesDir || !fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
|
|
12089
|
+
if (!slug) {
|
|
11133
12090
|
sendJson(res, 404, { error: 'Page JSON not found' });
|
|
11134
12091
|
return;
|
|
11135
12092
|
}
|
|
11136
|
-
|
|
12093
|
+
void (async () => {
|
|
12094
|
+
const core = await loadWebMcpBuilders();
|
|
12095
|
+
const runtime = await server.ssrLoadModule('/src/runtime.ts');
|
|
12096
|
+
const buildState = runtime.getWebMcpBuildState();
|
|
12097
|
+
const resolved = core.resolvePublicPageDocument({
|
|
12098
|
+
slug,
|
|
12099
|
+
pages: buildState.pages,
|
|
12100
|
+
siteConfig: buildState.siteConfig,
|
|
12101
|
+
themeConfig: buildState.themeConfig,
|
|
12102
|
+
menuConfig: buildState.menuConfig,
|
|
12103
|
+
collections: buildState.collections,
|
|
12104
|
+
refDocuments: buildState.refDocuments,
|
|
12105
|
+
});
|
|
12106
|
+
if (!resolved) {
|
|
12107
|
+
sendJson(res, 404, { error: 'Page JSON not found' });
|
|
12108
|
+
return;
|
|
12109
|
+
}
|
|
12110
|
+
sendJson(res, 200, resolved.page);
|
|
12111
|
+
})().catch((error) => {
|
|
12112
|
+
sendJson(res, 500, { error: error?.message || 'Page JSON resolution failed' });
|
|
12113
|
+
});
|
|
11137
12114
|
return;
|
|
11138
12115
|
}
|
|
11139
12116
|
if (req.method === 'GET' && req.url === '/api/list-assets') {
|
|
@@ -11152,12 +12129,23 @@ export default defineConfig({
|
|
|
11152
12129
|
if (!projectState || typeof slug !== 'string') { sendJson(res, 400, { error: 'Missing projectState or slug' }); return; }
|
|
11153
12130
|
if (!fs.existsSync(DATA_CONFIG_DIR)) fs.mkdirSync(DATA_CONFIG_DIR, { recursive: true });
|
|
11154
12131
|
if (!fs.existsSync(DATA_PAGES_DIR)) fs.mkdirSync(DATA_PAGES_DIR, { recursive: true });
|
|
12132
|
+
if (!fs.existsSync(DATA_COLLECTIONS_DIR)) fs.mkdirSync(DATA_COLLECTIONS_DIR, { recursive: true });
|
|
11155
12133
|
if (projectState.site != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'site.json'), JSON.stringify(projectState.site, null, 2), 'utf8');
|
|
11156
12134
|
if (projectState.menu != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'menu.json'), JSON.stringify(projectState.menu, null, 2), 'utf8');
|
|
11157
12135
|
if (projectState.theme != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'theme.json'), JSON.stringify(projectState.theme, null, 2), 'utf8');
|
|
11158
12136
|
if (projectState.page != null) {
|
|
11159
|
-
const
|
|
11160
|
-
fs.
|
|
12137
|
+
const pagePath = safeDataSlugPath(DATA_PAGES_DIR, slug, 'page');
|
|
12138
|
+
fs.mkdirSync(path.dirname(pagePath), { recursive: true });
|
|
12139
|
+
fs.writeFileSync(pagePath, JSON.stringify(projectState.page, null, 2), 'utf8');
|
|
12140
|
+
}
|
|
12141
|
+
if (projectState.collections && typeof projectState.collections === 'object') {
|
|
12142
|
+
for (const [source, collection] of Object.entries(projectState.collections)) {
|
|
12143
|
+
const sourceSlug = String(source).replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
12144
|
+
if (!sourceSlug) continue;
|
|
12145
|
+
const collectionDir = path.join(DATA_COLLECTIONS_DIR, sourceSlug);
|
|
12146
|
+
fs.mkdirSync(collectionDir, { recursive: true });
|
|
12147
|
+
fs.writeFileSync(path.join(collectionDir, `${sourceSlug}.json`), JSON.stringify(collection, null, 2), 'utf8');
|
|
12148
|
+
}
|
|
11161
12149
|
}
|
|
11162
12150
|
sendJson(res, 200, { ok: true });
|
|
11163
12151
|
} catch (e) { sendJson(res, 500, { error: e?.message || 'Save to file failed' }); }
|