@olonjs/cli 3.0.144 → 3.0.146

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.
@@ -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.10",
1820
+ "@olonjs/core": "^1.1.12",
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,6 +2144,7 @@ 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');
2149
2150
  const distSsrDir = path.resolve(root, 'dist-ssr');
@@ -2184,6 +2185,42 @@ function toCanonicalSlug(relativeJsonPath) {
2184
2185
  return slug;
2185
2186
  }
2186
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
+
2187
2224
  async function listJsonFilesRecursive(dir) {
2188
2225
  const items = await fs.readdir(dir, { withFileTypes: true });
2189
2226
  const files = [];
@@ -2206,7 +2243,14 @@ async function discoverTargets() {
2206
2243
  files = [];
2207
2244
  }
2208
2245
 
2209
- const rawSlugs = files.map((fullPath) => toCanonicalSlug(path.relative(pagesDir, fullPath)));
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();
2210
2254
  const slugs = Array.from(new Set(rawSlugs)).sort((a, b) => a.localeCompare(b));
2211
2255
 
2212
2256
  return slugs.map((slug) => {
@@ -2264,15 +2308,26 @@ const remoteStylesheetTags = getRemoteStylesheets()
2264
2308
  const webMcpBuildState = getWebMcpBuildState();
2265
2309
 
2266
2310
  for (const { slug } of targets) {
2267
- const pageConfig = webMcpBuildState.pages[slug];
2311
+ const pageConfig = resolvePageMatchFromRegistry(webMcpBuildState.pages, slug)?.page;
2268
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;
2269
2324
 
2270
- // Export the raw JSON data for the agentic web (so readResource works on SSG)
2271
- await writeJsonTargets(`pages/${slug}.json`, pageConfig);
2325
+ // Export the resolved public JSON data for the agentic web (so readResource matches runtime).
2326
+ await writeJsonTargets(`pages/${slug}.json`, publicPageConfig);
2272
2327
 
2273
2328
  const contract = buildPageContract({
2274
2329
  slug,
2275
- pageConfig,
2330
+ pageConfig: publicPageConfig,
2276
2331
  schemas: webMcpBuildState.schemas,
2277
2332
  submissionSchemas: webMcpBuildState.submissionSchemas,
2278
2333
  siteConfig: webMcpBuildState.siteConfig,
@@ -2280,7 +2335,7 @@ for (const { slug } of targets) {
2280
2335
  await writeSsrJson(`schemas/${slug}.schema.json`, contract);
2281
2336
  const pageManifest = buildPageManifest({
2282
2337
  slug,
2283
- pageConfig,
2338
+ pageConfig: publicPageConfig,
2284
2339
  schemas: webMcpBuildState.schemas,
2285
2340
  siteConfig: webMcpBuildState.siteConfig,
2286
2341
  });
@@ -2549,6 +2604,8 @@ const __dirname = path.dirname(__filename);
2549
2604
  const rootDir = path.resolve(__dirname, '..');
2550
2605
  const sourceDir = path.join(rootDir, 'src', 'data', 'pages');
2551
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');
2552
2609
  const sourceSiteConfigPath = path.join(rootDir, 'src', 'data', 'config', 'site.json');
2553
2610
  const targetConfigDir = path.join(rootDir, 'public', 'config');
2554
2611
  const targetSiteConfigPath = path.join(targetConfigDir, 'site.json');
@@ -2562,12 +2619,18 @@ fs.rmSync(targetDir, { recursive: true, force: true });
2562
2619
  fs.mkdirSync(targetDir, { recursive: true });
2563
2620
  fs.cpSync(sourceDir, targetDir, { recursive: true });
2564
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
+
2565
2628
  if (fs.existsSync(sourceSiteConfigPath)) {
2566
2629
  fs.mkdirSync(targetConfigDir, { recursive: true });
2567
2630
  fs.cpSync(sourceSiteConfigPath, targetSiteConfigPath);
2568
2631
  }
2569
2632
 
2570
- console.log('[sync-pages-to-public] Synced pages to public/pages and site config to public/config/site.json');
2633
+ console.log('[sync-pages-to-public] Synced pages, collections, and site config to public/');
2571
2634
 
2572
2635
  END_OF_FILE_CONTENT
2573
2636
  echo "Creating scripts/webmcp-feature-check.mjs..."
@@ -2885,6 +2948,7 @@ import { JsonPagesEngine } from '@olonjs/core';
2885
2948
  import type { JsonPagesConfig, LibraryImageEntry, ProjectState } from '@olonjs/core';
2886
2949
  import { normalizeBasePath, withBasePath } from '@olonjs/core';
2887
2950
  import { ComponentRegistry } from '@/lib/ComponentRegistry';
2951
+ import { CollectionRegistry } from '@/lib/CollectionRegistry';
2888
2952
  import { SECTION_SCHEMAS } from '@/lib/schemas';
2889
2953
  import { addSectionConfig } from '@/lib/addSectionConfig';
2890
2954
  import { getHydratedData } from '@/lib/draftStorage';
@@ -2895,6 +2959,7 @@ import { startCloudSaveStream } from '@olonjs/core';
2895
2959
  import siteData from '@/data/config/site.json';
2896
2960
  import themeData from '@/data/config/theme.json';
2897
2961
  import menuData from '@/data/config/menu.json';
2962
+ import { getFileCollections } from '@/lib/getFileCollections';
2898
2963
  import { getFilePages } from '@/lib/getFilePages';
2899
2964
  import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
2900
2965
  import { EmptyTenantView } from '@/components/empty-tenant';
@@ -2926,6 +2991,7 @@ const TENANT_ID = 'alpha';
2926
2991
 
2927
2992
  const filePages = getFilePages();
2928
2993
  const fileSiteConfig = siteData as unknown as SiteConfig;
2994
+ const collections = getFileCollections();
2929
2995
  const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
2930
2996
  const ASSET_UPLOAD_MAX_RETRIES = 2;
2931
2997
  const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
@@ -2963,6 +3029,7 @@ type CachedCloudContent = {
2963
3029
  savedAt: number;
2964
3030
  siteConfig: unknown | null;
2965
3031
  pages: Record<string, unknown>;
3032
+ collections?: JsonPagesConfig['collections'];
2966
3033
  };
2967
3034
 
2968
3035
  const CLOUD_CACHE_KEY = 'jp_cloud_content_cache_v1';
@@ -3008,7 +3075,7 @@ function asString(value: unknown, fallback: string): string {
3008
3075
  function normalizeRouteSlug(value: string): string {
3009
3076
  return value
3010
3077
  .toLowerCase()
3011
- .replace(/[^a-z0-9/_-]/g, '-')
3078
+ .replace(/[^a-z0-9/_[\]-]/g, '-')
3012
3079
  .replace(/^\/+|\/+$/g, '') || 'home';
3013
3080
  }
3014
3081
 
@@ -3034,6 +3101,7 @@ function coercePageConfig(slug: string, value: unknown): PageConfig | null {
3034
3101
  slug: normalizedSlug,
3035
3102
  meta: { title, description },
3036
3103
  sections: input.sections as PageConfig['sections'],
3104
+ ...(isObjectRecord(input.collection) ? { collection: input.collection as unknown as PageConfig['collection'] } : {}),
3037
3105
  ...(typeof input['global-header'] === 'boolean' ? { 'global-header': input['global-header'] } : {}),
3038
3106
  };
3039
3107
  }
@@ -3175,7 +3243,7 @@ function normalizeSlugForCache(slug: string): string {
3175
3243
  slug
3176
3244
  .trim()
3177
3245
  .toLowerCase()
3178
- .replace(/[^a-z0-9/_-]/g, '-')
3246
+ .replace(/[^a-z0-9/_[\]-]/g, '-')
3179
3247
  .replace(/^\/+|\/+$/g, '') || 'home'
3180
3248
  );
3181
3249
  }
@@ -3740,10 +3808,12 @@ function App() {
3740
3808
  basePath: APP_BASE_PATH,
3741
3809
  registry: ComponentRegistry as JsonPagesConfig['registry'],
3742
3810
  schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
3811
+ collectionSchemas: CollectionRegistry as unknown as JsonPagesConfig['collectionSchemas'],
3743
3812
  pages,
3744
3813
  siteConfig,
3745
3814
  themeConfig,
3746
3815
  menuConfig,
3816
+ collections,
3747
3817
  refDocuments,
3748
3818
  themeCss: { tenant: resolvedTenantCss },
3749
3819
  iconRegistry: iconMap,
@@ -3780,6 +3850,7 @@ function App() {
3780
3850
  slug,
3781
3851
  page: state.page,
3782
3852
  siteConfig: state.site,
3853
+ collections: state.collections,
3783
3854
  }),
3784
3855
  });
3785
3856
  const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
@@ -3793,6 +3864,7 @@ function App() {
3793
3864
  keyFingerprint,
3794
3865
  savedAt: Date.now(),
3795
3866
  siteConfig: state.site ?? null,
3867
+ collections: state.collections,
3796
3868
  pages: {
3797
3869
  ...(existing?.pages ?? {}),
3798
3870
  [normalizedSlug]: state.page,
@@ -4039,6 +4111,72 @@ export default App;
4039
4111
 
4040
4112
  END_OF_FILE_CONTENT
4041
4113
  # SKIP: src/App.tsx:Zone.Identifier is binary and cannot be embedded as text.
4114
+ mkdir -p "src/collections"
4115
+ mkdir -p "src/collections/autori"
4116
+ echo "Creating src/collections/autori/index.ts..."
4117
+ cat << 'END_OF_FILE_CONTENT' > "src/collections/autori/index.ts"
4118
+ export { AutoreSchema, AutoriCollectionSchema } from './schema';
4119
+ export type { Autore, AutoriCollection } from './types';
4120
+
4121
+ END_OF_FILE_CONTENT
4122
+ echo "Creating src/collections/autori/schema.ts..."
4123
+ cat << 'END_OF_FILE_CONTENT' > "src/collections/autori/schema.ts"
4124
+ import { z } from 'zod';
4125
+ import { BaseCollectionItem } from '@olonjs/core';
4126
+
4127
+ export const AutoreSchema = BaseCollectionItem.extend({
4128
+ name: z.string().describe('ui:text'),
4129
+ });
4130
+
4131
+ export const AutoriCollectionSchema = z.record(z.string(), AutoreSchema);
4132
+
4133
+ END_OF_FILE_CONTENT
4134
+ echo "Creating src/collections/autori/types.ts..."
4135
+ cat << 'END_OF_FILE_CONTENT' > "src/collections/autori/types.ts"
4136
+ import { z } from 'zod';
4137
+ import { AutoreSchema, AutoriCollectionSchema } from './schema';
4138
+
4139
+ export type Autore = z.infer<typeof AutoreSchema>;
4140
+ export type AutoriCollection = z.infer<typeof AutoriCollectionSchema>;
4141
+
4142
+ END_OF_FILE_CONTENT
4143
+ mkdir -p "src/collections/libri"
4144
+ echo "Creating src/collections/libri/index.ts..."
4145
+ cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/index.ts"
4146
+ export { LibroSchema, LibriCollectionSchema } from './schema';
4147
+ export type { Libro, LibriCollection } from './types';
4148
+
4149
+ END_OF_FILE_CONTENT
4150
+ echo "Creating src/collections/libri/schema.ts..."
4151
+ cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/schema.ts"
4152
+ import { z } from 'zod';
4153
+ import { BaseCollectionItem } from '@olonjs/core';
4154
+ import { AutoreSchema } from '@/collections/autori';
4155
+
4156
+ const CollectionRefSchema = z.object({
4157
+ $ref: z.string(),
4158
+ });
4159
+
4160
+ export const LibroSchema = BaseCollectionItem.extend({
4161
+ title: z.string().describe('ui:text'),
4162
+ author: z.union([AutoreSchema, CollectionRefSchema]).describe('ui:collection-ref:autori'),
4163
+ year: z.number().describe('ui:number'),
4164
+ genre: z.string().describe('ui:text'),
4165
+ summary: z.string().describe('ui:textarea'),
4166
+ });
4167
+
4168
+ export const LibriCollectionSchema = z.record(z.string(), LibroSchema);
4169
+
4170
+ END_OF_FILE_CONTENT
4171
+ echo "Creating src/collections/libri/types.ts..."
4172
+ cat << 'END_OF_FILE_CONTENT' > "src/collections/libri/types.ts"
4173
+ import { z } from 'zod';
4174
+ import { LibroSchema, LibriCollectionSchema } from './schema';
4175
+
4176
+ export type Libro = z.infer<typeof LibroSchema>;
4177
+ export type LibriCollection = z.infer<typeof LibriCollectionSchema>;
4178
+
4179
+ END_OF_FILE_CONTENT
4042
4180
  mkdir -p "src/components"
4043
4181
  echo "Creating src/components/ThemeProvider.tsx..."
4044
4182
  cat << 'END_OF_FILE_CONTENT' > "src/components/ThemeProvider.tsx"
@@ -4102,6 +4240,370 @@ export function useTheme() {
4102
4240
  return useContext(ThemeContext)
4103
4241
  }
4104
4242
 
4243
+ END_OF_FILE_CONTENT
4244
+ mkdir -p "src/components/authors-list"
4245
+ echo "Creating src/components/authors-list/View.tsx..."
4246
+ cat << 'END_OF_FILE_CONTENT' > "src/components/authors-list/View.tsx"
4247
+ import { useMemo } from 'react';
4248
+ import type { Autore } from '@/collections/autori';
4249
+ import type { AuthorsListData } from './types';
4250
+
4251
+ type AuthorsListViewProps = {
4252
+ data: AuthorsListData;
4253
+ };
4254
+
4255
+ function toAuthors(items: AuthorsListData['items']): Autore[] {
4256
+ return Object.values(items ?? {}).sort((a, b) => a.name.localeCompare(b.name));
4257
+ }
4258
+
4259
+ export function AuthorsListView({ data }: AuthorsListViewProps) {
4260
+ const authors = useMemo(() => toAuthors(data.items), [data.items]);
4261
+
4262
+ return (
4263
+ <main className="min-h-screen bg-background px-6 py-16 text-foreground">
4264
+ <section className="mx-auto flex w-full max-w-5xl flex-col gap-10">
4265
+ <div className="max-w-2xl">
4266
+ {data.eyebrow && (
4267
+ <p
4268
+ data-jp-field="eyebrow"
4269
+ className="text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground"
4270
+ >
4271
+ {data.eyebrow}
4272
+ </p>
4273
+ )}
4274
+ <h1
4275
+ data-jp-field="title"
4276
+ className="mt-3 text-4xl font-semibold tracking-tight sm:text-5xl"
4277
+ >
4278
+ {data.title}
4279
+ </h1>
4280
+ {data.description && (
4281
+ <p
4282
+ data-jp-field="description"
4283
+ className="mt-4 text-base leading-7 text-muted-foreground"
4284
+ >
4285
+ {data.description}
4286
+ </p>
4287
+ )}
4288
+ </div>
4289
+
4290
+ <div data-jp-field="items" className="grid gap-4 sm:grid-cols-2">
4291
+ {authors.map((author) => (
4292
+ <a
4293
+ key={author.id}
4294
+ href={`/authors/${encodeURIComponent(author.id)}/libri`}
4295
+ data-jp-item-id={author.id}
4296
+ data-jp-item-field="items"
4297
+ className="block rounded-xl border border-border bg-card p-5 shadow-sm transition-colors hover:bg-muted/40"
4298
+ >
4299
+ <h2 className="text-xl font-semibold">{author.name}</h2>
4300
+ <p className="mt-2 text-sm text-muted-foreground">
4301
+ Vedi libri di {author.name}
4302
+ </p>
4303
+ </a>
4304
+ ))}
4305
+ </div>
4306
+ </section>
4307
+ </main>
4308
+ );
4309
+ }
4310
+
4311
+ END_OF_FILE_CONTENT
4312
+ echo "Creating src/components/authors-list/index.ts..."
4313
+ cat << 'END_OF_FILE_CONTENT' > "src/components/authors-list/index.ts"
4314
+ export { AuthorsListView } from './View';
4315
+ export { AuthorsListSchema, AuthorsListSettingsSchema } from './schema';
4316
+ export type { AuthorsListData, AuthorsListSettings } from './types';
4317
+
4318
+ END_OF_FILE_CONTENT
4319
+ echo "Creating src/components/authors-list/schema.ts..."
4320
+ cat << 'END_OF_FILE_CONTENT' > "src/components/authors-list/schema.ts"
4321
+ import { z } from 'zod';
4322
+ import { BaseSectionData } from '@olonjs/core';
4323
+ import { AutoreSchema } from '@/collections/autori';
4324
+
4325
+ export const AuthorsListSchema = BaseSectionData.extend({
4326
+ eyebrow: z.string().optional().describe('ui:text'),
4327
+ title: z.string().describe('ui:text'),
4328
+ description: z.string().optional().describe('ui:textarea'),
4329
+ items: z.record(z.string(), AutoreSchema).describe('ui:collection-ref:autori'),
4330
+ });
4331
+
4332
+ export const AuthorsListSettingsSchema = z.object({});
4333
+
4334
+ END_OF_FILE_CONTENT
4335
+ echo "Creating src/components/authors-list/types.ts..."
4336
+ cat << 'END_OF_FILE_CONTENT' > "src/components/authors-list/types.ts"
4337
+ import { z } from 'zod';
4338
+ import { AuthorsListSchema, AuthorsListSettingsSchema } from './schema';
4339
+
4340
+ export type AuthorsListData = z.infer<typeof AuthorsListSchema>;
4341
+ export type AuthorsListSettings = z.infer<typeof AuthorsListSettingsSchema>;
4342
+
4343
+ END_OF_FILE_CONTENT
4344
+ mkdir -p "src/components/book-detail"
4345
+ echo "Creating src/components/book-detail/View.tsx..."
4346
+ cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/View.tsx"
4347
+ import type { BookDetailData } from './types';
4348
+
4349
+ type BookDetailViewProps = {
4350
+ data: BookDetailData;
4351
+ };
4352
+
4353
+ function getAuthorName(author: BookDetailData['item']['author']): string {
4354
+ if (typeof author === 'object' && author !== null && 'name' in author) {
4355
+ return String(author.name);
4356
+ }
4357
+ return 'Autore';
4358
+ }
4359
+
4360
+ export function BookDetailView({ data }: BookDetailViewProps) {
4361
+ const book = data.item;
4362
+
4363
+ return (
4364
+ <main className="min-h-screen bg-background text-foreground px-6 py-16">
4365
+ <article
4366
+ data-jp-field="item"
4367
+ data-jp-item-id={book.id}
4368
+ data-jp-item-field="item"
4369
+ className="mx-auto w-full max-w-3xl rounded-2xl border border-border bg-card p-8 shadow-sm"
4370
+ >
4371
+ <a
4372
+ href="/"
4373
+ className="text-sm font-medium text-muted-foreground hover:text-foreground"
4374
+ >
4375
+ {data.backLabel}
4376
+ </a>
4377
+ <p className="mt-10 text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground">
4378
+ {book.genre} · {book.year}
4379
+ </p>
4380
+ <h1 className="mt-3 text-4xl font-semibold tracking-tight sm:text-5xl">
4381
+ {book.title}
4382
+ </h1>
4383
+ <p className="mt-4 text-lg text-muted-foreground">
4384
+ {getAuthorName(book.author)}
4385
+ </p>
4386
+ <p className="mt-8 text-base leading-8 text-muted-foreground">
4387
+ {book.summary}
4388
+ </p>
4389
+ </article>
4390
+ </main>
4391
+ );
4392
+ }
4393
+
4394
+ END_OF_FILE_CONTENT
4395
+ echo "Creating src/components/book-detail/index.ts..."
4396
+ cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/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/book-detail/schema.ts..."
4403
+ cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/schema.ts"
4404
+ import { z } from 'zod';
4405
+ import { BaseSectionData } from '@olonjs/core';
4406
+ import { LibroSchema } from '@/collections/libri';
4407
+
4408
+ export const BookDetailSchema = BaseSectionData.extend({
4409
+ item: LibroSchema.describe('ui:collection-ref'),
4410
+ backLabel: z.string().default('Torna ai libri').describe('ui:text'),
4411
+ });
4412
+
4413
+ export const BookDetailSettingsSchema = z.object({});
4414
+
4415
+ END_OF_FILE_CONTENT
4416
+ echo "Creating src/components/book-detail/types.ts..."
4417
+ cat << 'END_OF_FILE_CONTENT' > "src/components/book-detail/types.ts"
4418
+ import { z } from 'zod';
4419
+ import { BookDetailSchema, BookDetailSettingsSchema } from './schema';
4420
+
4421
+ export type BookDetailData = z.infer<typeof BookDetailSchema>;
4422
+ export type BookDetailSettings = z.infer<typeof BookDetailSettingsSchema>;
4423
+
4424
+ END_OF_FILE_CONTENT
4425
+ mkdir -p "src/components/books-list"
4426
+ echo "Creating src/components/books-list/View.tsx..."
4427
+ cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/View.tsx"
4428
+ import { useEffect, useMemo, useState } from 'react';
4429
+ import { useLocation } from 'react-router-dom';
4430
+ import type { Libro } from '@/collections/libri';
4431
+ import type { BooksListData } from './types';
4432
+
4433
+ type BooksListViewProps = {
4434
+ data: BooksListData;
4435
+ };
4436
+
4437
+ function toBooks(items: BooksListData['items']): Libro[] {
4438
+ return Object.values(items ?? {}).sort((a, b) => a.title.localeCompare(b.title));
4439
+ }
4440
+
4441
+ function getAuthorName(author: Libro['author']): string {
4442
+ if (typeof author === 'object' && author !== null && 'name' in author) {
4443
+ return String(author.name);
4444
+ }
4445
+ return 'Autore';
4446
+ }
4447
+
4448
+ function getAuthorId(author: Libro['author']): string | null {
4449
+ if (typeof author === 'object' && author !== null && 'id' in author && typeof author.id === 'string') {
4450
+ return author.id;
4451
+ }
4452
+ if (typeof author === 'object' && author !== null && '$ref' in author && typeof author.$ref === 'string') {
4453
+ const pointer = author.$ref.split('#')[1]?.replace(/^\//, '') ?? '';
4454
+ return pointer.split('/')[0] || null;
4455
+ }
4456
+ return null;
4457
+ }
4458
+
4459
+ function getAuthorFilterFromPath(pathname: string): string | null {
4460
+ const match = pathname.match(/^\/authors\/([^/]+)\/libri\/?$/);
4461
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
4462
+ }
4463
+
4464
+ export function BooksListView({ data }: BooksListViewProps) {
4465
+ const location = useLocation();
4466
+ const authorFilter = useMemo(() => {
4467
+ const queryAuthor = new URLSearchParams(location.search).get('author');
4468
+ return queryAuthor || getAuthorFilterFromPath(location.pathname);
4469
+ }, [location.pathname, location.search]);
4470
+ const books = useMemo(() => toBooks(data.items), [data.items]);
4471
+ const filteredBooks = useMemo(
4472
+ () => authorFilter ? books.filter((book) => getAuthorId(book.author) === authorFilter) : books,
4473
+ [authorFilter, books]
4474
+ );
4475
+ const pageSize = Math.max(1, Math.floor(data.pageSize || 10));
4476
+ const totalPages = Math.max(1, Math.ceil(filteredBooks.length / pageSize));
4477
+ const [page, setPage] = useState(1);
4478
+ const currentPage = Math.min(page, totalPages);
4479
+ const startIndex = (currentPage - 1) * pageSize;
4480
+ const visibleBooks = filteredBooks.slice(startIndex, startIndex + pageSize);
4481
+
4482
+ useEffect(() => {
4483
+ setPage(1);
4484
+ }, [authorFilter]);
4485
+
4486
+ return (
4487
+ <main className="min-h-screen bg-background text-foreground px-6 py-16">
4488
+ <section className="mx-auto flex w-full max-w-5xl flex-col gap-10">
4489
+ <div className="max-w-2xl">
4490
+ {data.eyebrow && (
4491
+ <p
4492
+ data-jp-field="eyebrow"
4493
+ className="text-sm font-medium uppercase tracking-[0.18em] text-muted-foreground"
4494
+ >
4495
+ {data.eyebrow}
4496
+ </p>
4497
+ )}
4498
+ <h1
4499
+ data-jp-field="title"
4500
+ className="mt-3 text-4xl font-semibold tracking-tight sm:text-5xl"
4501
+ >
4502
+ {data.title}
4503
+ </h1>
4504
+ {data.description && (
4505
+ <p
4506
+ data-jp-field="description"
4507
+ className="mt-4 text-base leading-7 text-muted-foreground"
4508
+ >
4509
+ {data.description}
4510
+ </p>
4511
+ )}
4512
+ </div>
4513
+
4514
+ <div data-jp-field="items" className="grid gap-4">
4515
+ {authorFilter && (
4516
+ <p className="text-sm text-muted-foreground">
4517
+ Filtro autore: {authorFilter} · {filteredBooks.length} libri
4518
+ </p>
4519
+ )}
4520
+ {visibleBooks.map((book) => (
4521
+ <article
4522
+ key={book.id}
4523
+ data-jp-item-id={book.id}
4524
+ data-jp-item-field="items"
4525
+ className="rounded-xl border border-border bg-card p-5 shadow-sm"
4526
+ >
4527
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
4528
+ <div>
4529
+ <h2 className="text-xl font-semibold">{book.title}</h2>
4530
+ <p className="mt-1 text-sm text-muted-foreground">
4531
+ {getAuthorName(book.author)} · {book.year} · {book.genre}
4532
+ </p>
4533
+ <p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
4534
+ {book.summary}
4535
+ </p>
4536
+ </div>
4537
+ <a
4538
+ href={`/libri/${book.id}`}
4539
+ 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"
4540
+ >
4541
+ Apri scheda
4542
+ </a>
4543
+ </div>
4544
+ </article>
4545
+ ))}
4546
+ </div>
4547
+
4548
+ <nav className="flex items-center justify-between border-t border-border pt-6 text-sm">
4549
+ <button
4550
+ type="button"
4551
+ onClick={() => setPage((value) => Math.max(1, value - 1))}
4552
+ disabled={currentPage === 1}
4553
+ className="rounded-md border border-border px-3 py-2 font-medium disabled:cursor-not-allowed disabled:opacity-40 hover:bg-muted"
4554
+ >
4555
+ Precedente
4556
+ </button>
4557
+ <span className="text-muted-foreground">
4558
+ Pagina {currentPage} di {totalPages} · {filteredBooks.length} libri
4559
+ </span>
4560
+ <button
4561
+ type="button"
4562
+ onClick={() => setPage((value) => Math.min(totalPages, value + 1))}
4563
+ disabled={currentPage === totalPages}
4564
+ className="rounded-md border border-border px-3 py-2 font-medium disabled:cursor-not-allowed disabled:opacity-40 hover:bg-muted"
4565
+ >
4566
+ Successiva
4567
+ </button>
4568
+ </nav>
4569
+ </section>
4570
+ </main>
4571
+ );
4572
+ }
4573
+
4574
+ END_OF_FILE_CONTENT
4575
+ echo "Creating src/components/books-list/index.ts..."
4576
+ cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/index.ts"
4577
+ export * from './View';
4578
+ export * from './schema';
4579
+ export * from './types';
4580
+
4581
+ END_OF_FILE_CONTENT
4582
+ echo "Creating src/components/books-list/schema.ts..."
4583
+ cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/schema.ts"
4584
+ import { z } from 'zod';
4585
+ import { BaseSectionData } from '@olonjs/core';
4586
+ import { LibroSchema } from '@/collections/libri';
4587
+
4588
+ export const BooksListSchema = BaseSectionData.extend({
4589
+ eyebrow: z.string().optional().describe('ui:text'),
4590
+ title: z.string().describe('ui:text'),
4591
+ description: z.string().optional().describe('ui:textarea'),
4592
+ items: z.record(z.string(), LibroSchema).describe('ui:collection-ref:libri'),
4593
+ pageSize: z.number().default(10).describe('ui:number'),
4594
+ });
4595
+
4596
+ export const BooksListSettingsSchema = z.object({});
4597
+
4598
+ END_OF_FILE_CONTENT
4599
+ echo "Creating src/components/books-list/types.ts..."
4600
+ cat << 'END_OF_FILE_CONTENT' > "src/components/books-list/types.ts"
4601
+ import { z } from 'zod';
4602
+ import { BooksListSchema, BooksListSettingsSchema } from './schema';
4603
+
4604
+ export type BooksListData = z.infer<typeof BooksListSchema>;
4605
+ export type BooksListSettings = z.infer<typeof BooksListSettingsSchema>;
4606
+
4105
4607
  END_OF_FILE_CONTENT
4106
4608
  mkdir -p "src/components/empty-tenant"
4107
4609
  echo "Creating src/components/empty-tenant/View.tsx..."
@@ -4155,6 +4657,123 @@ import { EmptyTenantSchema, EmptyTenantSettingsSchema } from './schema';
4155
4657
  export type EmptyTenantData = z.infer<typeof EmptyTenantSchema>;
4156
4658
  export type EmptyTenantSettings = z.infer<typeof EmptyTenantSettingsSchema>;
4157
4659
 
4660
+ END_OF_FILE_CONTENT
4661
+ mkdir -p "src/components/footer"
4662
+ echo "Creating src/components/footer/View.tsx..."
4663
+ cat << 'END_OF_FILE_CONTENT' > "src/components/footer/View.tsx"
4664
+ import type React from 'react';
4665
+ import type { FooterData, FooterSettings } from './types';
4666
+
4667
+ type FooterViewProps = {
4668
+ data: FooterData;
4669
+ settings?: FooterSettings;
4670
+ };
4671
+
4672
+ export function FooterView({ data, settings }: FooterViewProps) {
4673
+ const links = data.links ?? [];
4674
+
4675
+ return (
4676
+ <footer
4677
+ style={{
4678
+ '--local-bg': 'var(--background)',
4679
+ '--local-text': 'var(--foreground)',
4680
+ '--local-muted': 'var(--muted-foreground)',
4681
+ '--local-border': 'var(--border)',
4682
+ } as React.CSSProperties}
4683
+ className="border-t border-[var(--local-border)] bg-[var(--local-bg)] px-6 py-10 text-[var(--local-text)]"
4684
+ >
4685
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-8 md:flex-row md:items-end md:justify-between">
4686
+ <div className="max-w-md">
4687
+ <div className="flex items-center gap-3">
4688
+ {settings?.showLogo !== false && (
4689
+ <span className="flex h-9 w-9 items-center justify-center rounded-lg border border-[var(--local-border)] text-sm font-semibold">
4690
+ O
4691
+ </span>
4692
+ )}
4693
+ <p data-jp-field="brandText" className="text-lg font-semibold tracking-tight">
4694
+ {data.brandText}
4695
+ </p>
4696
+ </div>
4697
+ {data.description && (
4698
+ <p data-jp-field="description" className="mt-3 text-sm leading-6 text-[var(--local-muted)]">
4699
+ {data.description}
4700
+ </p>
4701
+ )}
4702
+ <p data-jp-field="copyright" className="mt-4 text-xs text-[var(--local-muted)]">
4703
+ {data.copyright}
4704
+ </p>
4705
+ </div>
4706
+
4707
+ <nav data-jp-field="links" className="flex flex-wrap gap-4 text-sm">
4708
+ {links.map((link) => (
4709
+ <a
4710
+ key={link.id}
4711
+ data-jp-item-id={link.id}
4712
+ data-jp-item-field="links"
4713
+ href={link.href}
4714
+ target={link.external ? '_blank' : undefined}
4715
+ rel={link.external ? 'noreferrer' : undefined}
4716
+ className="text-[var(--local-muted)] transition-colors hover:text-[var(--local-text)]"
4717
+ >
4718
+ {link.label}
4719
+ </a>
4720
+ ))}
4721
+ {data.designSystemHref && (
4722
+ <a
4723
+ data-jp-field="designSystemHref"
4724
+ href={data.designSystemHref}
4725
+ className="text-[var(--local-muted)] transition-colors hover:text-[var(--local-text)]"
4726
+ >
4727
+ Design system
4728
+ </a>
4729
+ )}
4730
+ </nav>
4731
+ </div>
4732
+ </footer>
4733
+ );
4734
+ }
4735
+
4736
+ END_OF_FILE_CONTENT
4737
+ echo "Creating src/components/footer/index.ts..."
4738
+ cat << 'END_OF_FILE_CONTENT' > "src/components/footer/index.ts"
4739
+ export * from './View';
4740
+ export * from './schema';
4741
+ export * from './types';
4742
+
4743
+ END_OF_FILE_CONTENT
4744
+ echo "Creating src/components/footer/schema.ts..."
4745
+ cat << 'END_OF_FILE_CONTENT' > "src/components/footer/schema.ts"
4746
+ import { z } from 'zod';
4747
+ import { BaseArrayItem, BaseSectionData } from '@olonjs/core';
4748
+
4749
+ export const FooterLinkSchema = BaseArrayItem.extend({
4750
+ id: z.string(),
4751
+ label: z.string().describe('ui:text'),
4752
+ href: z.string().describe('ui:text'),
4753
+ external: z.boolean().optional(),
4754
+ });
4755
+
4756
+ export const FooterSchema = BaseSectionData.extend({
4757
+ brandText: z.string().describe('ui:text'),
4758
+ description: z.string().optional().describe('ui:textarea'),
4759
+ copyright: z.string().describe('ui:text'),
4760
+ links: z.array(FooterLinkSchema).default([]).describe('ui:list'),
4761
+ designSystemHref: z.string().optional().describe('ui:text'),
4762
+ });
4763
+
4764
+ export const FooterSettingsSchema = z.object({
4765
+ showLogo: z.boolean().default(true),
4766
+ });
4767
+
4768
+ END_OF_FILE_CONTENT
4769
+ echo "Creating src/components/footer/types.ts..."
4770
+ cat << 'END_OF_FILE_CONTENT' > "src/components/footer/types.ts"
4771
+ import { z } from 'zod';
4772
+ import { FooterSchema, FooterSettingsSchema } from './schema';
4773
+
4774
+ export type FooterData = z.infer<typeof FooterSchema>;
4775
+ export type FooterSettings = z.infer<typeof FooterSettingsSchema>;
4776
+
4158
4777
  END_OF_FILE_CONTENT
4159
4778
  mkdir -p "src/components/form-demo"
4160
4779
  echo "Creating src/components/form-demo/Untitled..."
@@ -8567,6 +9186,436 @@ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
8567
9186
  END_OF_FILE_CONTENT
8568
9187
  # SKIP: src/components/ui/tooltip.tsx:Zone.Identifier is binary and cannot be embedded as text.
8569
9188
  mkdir -p "src/data"
9189
+ mkdir -p "src/data/collections"
9190
+ mkdir -p "src/data/collections/autori"
9191
+ echo "Creating src/data/collections/autori/autori.json..."
9192
+ cat << 'END_OF_FILE_CONTENT' > "src/data/collections/autori/autori.json"
9193
+ {
9194
+ "george-orwell": {
9195
+ "id": "george-orwell",
9196
+ "name": "George Orwell"
9197
+ },
9198
+ "umberto-eco": {
9199
+ "id": "umberto-eco",
9200
+ "name": "Umberto Eco"
9201
+ },
9202
+ "primo-levi": {
9203
+ "id": "primo-levi",
9204
+ "name": "Primo Levi"
9205
+ },
9206
+ "italo-calvino": {
9207
+ "id": "italo-calvino",
9208
+ "name": "Italo Calvino"
9209
+ },
9210
+ "giuseppe-tomasi-di-lampedusa": {
9211
+ "id": "giuseppe-tomasi-di-lampedusa",
9212
+ "name": "Giuseppe Tomasi di Lampedusa"
9213
+ },
9214
+ "italo-svevo": {
9215
+ "id": "italo-svevo",
9216
+ "name": "Italo Svevo"
9217
+ },
9218
+ "alessandro-manzoni": {
9219
+ "id": "alessandro-manzoni",
9220
+ "name": "Alessandro Manzoni"
9221
+ },
9222
+ "roberto-saviano": {
9223
+ "id": "roberto-saviano",
9224
+ "name": "Roberto Saviano"
9225
+ },
9226
+ "alessandro-baricco": {
9227
+ "id": "alessandro-baricco",
9228
+ "name": "Alessandro Baricco"
9229
+ },
9230
+ "natalia-ginzburg": {
9231
+ "id": "natalia-ginzburg",
9232
+ "name": "Natalia Ginzburg"
9233
+ },
9234
+ "gabriel-garcia-marquez": {
9235
+ "id": "gabriel-garcia-marquez",
9236
+ "name": "Gabriel Garcia Marquez"
9237
+ },
9238
+ "ray-bradbury": {
9239
+ "id": "ray-bradbury",
9240
+ "name": "Ray Bradbury"
9241
+ },
9242
+ "frank-herbert": {
9243
+ "id": "frank-herbert",
9244
+ "name": "Frank Herbert"
9245
+ },
9246
+ "william-gibson": {
9247
+ "id": "william-gibson",
9248
+ "name": "William Gibson"
9249
+ },
9250
+ "j-r-r-tolkien": {
9251
+ "id": "j-r-r-tolkien",
9252
+ "name": "J. R. R. Tolkien"
9253
+ },
9254
+ "j-k-rowling": {
9255
+ "id": "j-k-rowling",
9256
+ "name": "J. K. Rowling"
9257
+ },
9258
+ "jane-austen": {
9259
+ "id": "jane-austen",
9260
+ "name": "Jane Austen"
9261
+ },
9262
+ "herman-melville": {
9263
+ "id": "herman-melville",
9264
+ "name": "Herman Melville"
9265
+ },
9266
+ "fedor-dostoevskij": {
9267
+ "id": "fedor-dostoevskij",
9268
+ "name": "Fedor Dostoevskij"
9269
+ },
9270
+ "lev-tolstoj": {
9271
+ "id": "lev-tolstoj",
9272
+ "name": "Lev Tolstoj"
9273
+ },
9274
+ "michail-bulgakov": {
9275
+ "id": "michail-bulgakov",
9276
+ "name": "Michail Bulgakov"
9277
+ },
9278
+ "cormac-mccarthy": {
9279
+ "id": "cormac-mccarthy",
9280
+ "name": "Cormac McCarthy"
9281
+ },
9282
+ "david-mitchell": {
9283
+ "id": "david-mitchell",
9284
+ "name": "David Mitchell"
9285
+ },
9286
+ "haruki-murakami": {
9287
+ "id": "haruki-murakami",
9288
+ "name": "Haruki Murakami"
9289
+ },
9290
+ "chimamanda-ngozi-adichie": {
9291
+ "id": "chimamanda-ngozi-adichie",
9292
+ "name": "Chimamanda Ngozi Adichie"
9293
+ },
9294
+ "isabel-allende": {
9295
+ "id": "isabel-allende",
9296
+ "name": "Isabel Allende"
9297
+ },
9298
+ "toni-morrison": {
9299
+ "id": "toni-morrison",
9300
+ "name": "Toni Morrison"
9301
+ },
9302
+ "junot-diaz": {
9303
+ "id": "junot-diaz",
9304
+ "name": "Junot Diaz"
9305
+ },
9306
+ "jonathan-franzen": {
9307
+ "id": "jonathan-franzen",
9308
+ "name": "Jonathan Franzen"
9309
+ }
9310
+ }
9311
+
9312
+ END_OF_FILE_CONTENT
9313
+ mkdir -p "src/data/collections/libri"
9314
+ echo "Creating src/data/collections/libri/libri.json..."
9315
+ cat << 'END_OF_FILE_CONTENT' > "src/data/collections/libri/libri.json"
9316
+ {
9317
+ "1984": {
9318
+ "id": "1984",
9319
+ "title": "1984",
9320
+ "author": {
9321
+ "$ref": "../autori/autori.json#/george-orwell"
9322
+ },
9323
+ "year": 1949,
9324
+ "genre": "Distopia",
9325
+ "summary": "Un regime totalitario controlla linguaggio, memoria e pensiero."
9326
+ },
9327
+ "il-nome-della-rosa": {
9328
+ "id": "il-nome-della-rosa",
9329
+ "title": "Il nome della rosa",
9330
+ "author": {
9331
+ "$ref": "../autori/autori.json#/umberto-eco"
9332
+ },
9333
+ "year": 1980,
9334
+ "genre": "Romanzo storico",
9335
+ "summary": "Un'indagine in un'abbazia medievale diventa una riflessione su conoscenza, potere e interpretazione."
9336
+ },
9337
+ "se-questo-e-un-uomo": {
9338
+ "id": "se-questo-e-un-uomo",
9339
+ "title": "Se questo e un uomo",
9340
+ "author": {
9341
+ "$ref": "../autori/autori.json#/primo-levi"
9342
+ },
9343
+ "year": 1947,
9344
+ "genre": "Memoria",
9345
+ "summary": "La testimonianza essenziale di Levi sull'esperienza del lager e sulla dignita umana."
9346
+ },
9347
+ "le-citta-invisibili": {
9348
+ "id": "le-citta-invisibili",
9349
+ "title": "Le citta invisibili",
9350
+ "author": {
9351
+ "$ref": "../autori/autori.json#/italo-calvino"
9352
+ },
9353
+ "year": 1972,
9354
+ "genre": "Letteratura fantastica",
9355
+ "summary": "Marco Polo racconta a Kublai Khan citta immaginarie che parlano di memoria, desiderio e linguaggio."
9356
+ },
9357
+ "il-gattopardo": {
9358
+ "id": "il-gattopardo",
9359
+ "title": "Il Gattopardo",
9360
+ "author": {
9361
+ "$ref": "../autori/autori.json#/giuseppe-tomasi-di-lampedusa"
9362
+ },
9363
+ "year": 1958,
9364
+ "genre": "Romanzo storico",
9365
+ "summary": "Il tramonto dell'aristocrazia siciliana durante l'unificazione italiana."
9366
+ },
9367
+ "la-coscienza-di-zeno": {
9368
+ "id": "la-coscienza-di-zeno",
9369
+ "title": "La coscienza di Zeno",
9370
+ "author": {
9371
+ "$ref": "../autori/autori.json#/italo-svevo"
9372
+ },
9373
+ "year": 1923,
9374
+ "genre": "Romanzo psicologico",
9375
+ "summary": "Un diario ironico e nevrotico attraversa memoria, terapia e autoinganno."
9376
+ },
9377
+ "i-promessi-sposi": {
9378
+ "id": "i-promessi-sposi",
9379
+ "title": "I promessi sposi",
9380
+ "author": {
9381
+ "$ref": "../autori/autori.json#/alessandro-manzoni"
9382
+ },
9383
+ "year": 1842,
9384
+ "genre": "Classico",
9385
+ "summary": "La vicenda di Renzo e Lucia dentro carestia, guerra, peste e provvidenza."
9386
+ },
9387
+ "il-barone-rampante": {
9388
+ "id": "il-barone-rampante",
9389
+ "title": "Il barone rampante",
9390
+ "author": {
9391
+ "$ref": "../autori/autori.json#/italo-calvino"
9392
+ },
9393
+ "year": 1957,
9394
+ "genre": "Romanzo filosofico",
9395
+ "summary": "Cosimo sceglie di vivere sugli alberi e trasforma la distanza in una forma di liberta."
9396
+ },
9397
+ "gomorra": {
9398
+ "id": "gomorra",
9399
+ "title": "Gomorra",
9400
+ "author": {
9401
+ "$ref": "../autori/autori.json#/roberto-saviano"
9402
+ },
9403
+ "year": 2006,
9404
+ "genre": "Inchiesta",
9405
+ "summary": "Un reportage narrativo sulle economie e le violenze del sistema camorristico."
9406
+ },
9407
+ "oceano-mare": {
9408
+ "id": "oceano-mare",
9409
+ "title": "Oceano mare",
9410
+ "author": {
9411
+ "$ref": "../autori/autori.json#/alessandro-baricco"
9412
+ },
9413
+ "year": 1993,
9414
+ "genre": "Romanzo letterario",
9415
+ "summary": "Storie diverse si incontrano in una locanda sul mare, tra cura, naufragio e mistero."
9416
+ },
9417
+ "lessico-famigliare": {
9418
+ "id": "lessico-famigliare",
9419
+ "title": "Lessico famigliare",
9420
+ "author": {
9421
+ "$ref": "../autori/autori.json#/natalia-ginzburg"
9422
+ },
9423
+ "year": 1963,
9424
+ "genre": "Memoria narrativa",
9425
+ "summary": "Una famiglia prende forma attraverso parole, tic linguistici e memoria civile."
9426
+ },
9427
+ "cent-anni-di-solitudine": {
9428
+ "id": "cent-anni-di-solitudine",
9429
+ "title": "Cent'anni di solitudine",
9430
+ "author": {
9431
+ "$ref": "../autori/autori.json#/gabriel-garcia-marquez"
9432
+ },
9433
+ "year": 1967,
9434
+ "genre": "Realismo magico",
9435
+ "summary": "La saga dei Buendia e di Macondo intreccia mito, storia e destino."
9436
+ },
9437
+ "fahrenheit-451": {
9438
+ "id": "fahrenheit-451",
9439
+ "title": "Fahrenheit 451",
9440
+ "author": {
9441
+ "$ref": "../autori/autori.json#/ray-bradbury"
9442
+ },
9443
+ "year": 1953,
9444
+ "genre": "Distopia",
9445
+ "summary": "In un futuro dove i libri bruciano, leggere diventa un atto di resistenza."
9446
+ },
9447
+ "dune": {
9448
+ "id": "dune",
9449
+ "title": "Dune",
9450
+ "author": {
9451
+ "$ref": "../autori/autori.json#/frank-herbert"
9452
+ },
9453
+ "year": 1965,
9454
+ "genre": "Fantascienza",
9455
+ "summary": "Politica, ecologia e messianismo si scontrano sul pianeta desertico Arrakis."
9456
+ },
9457
+ "neuromancer": {
9458
+ "id": "neuromancer",
9459
+ "title": "Neuromancer",
9460
+ "author": {
9461
+ "$ref": "../autori/autori.json#/william-gibson"
9462
+ },
9463
+ "year": 1984,
9464
+ "genre": "Cyberpunk",
9465
+ "summary": "Un hacker decaduto viene trascinato in un colpo che attraversa cyberspazio e intelligenze artificiali."
9466
+ },
9467
+ "il-signore-degli-anelli": {
9468
+ "id": "il-signore-degli-anelli",
9469
+ "title": "Il Signore degli Anelli",
9470
+ "author": {
9471
+ "$ref": "../autori/autori.json#/j-r-r-tolkien"
9472
+ },
9473
+ "year": 1954,
9474
+ "genre": "Fantasy",
9475
+ "summary": "La Compagnia affronta il potere dell'Anello in una delle grandi epopee moderne."
9476
+ },
9477
+ "harry-potter-e-la-pietra-filosofale": {
9478
+ "id": "harry-potter-e-la-pietra-filosofale",
9479
+ "title": "Harry Potter e la pietra filosofale",
9480
+ "author": {
9481
+ "$ref": "../autori/autori.json#/j-k-rowling"
9482
+ },
9483
+ "year": 1997,
9484
+ "genre": "Fantasy",
9485
+ "summary": "Un ragazzo scopre il mondo magico e il proprio posto in una storia piu grande."
9486
+ },
9487
+ "orgoglio-e-pregiudizio": {
9488
+ "id": "orgoglio-e-pregiudizio",
9489
+ "title": "Orgoglio e pregiudizio",
9490
+ "author": {
9491
+ "$ref": "../autori/autori.json#/jane-austen"
9492
+ },
9493
+ "year": 1813,
9494
+ "genre": "Classico",
9495
+ "summary": "Elizabeth Bennet e Mr. Darcy si misurano con classe, carattere e giudizio sociale."
9496
+ },
9497
+ "moby-dick": {
9498
+ "id": "moby-dick",
9499
+ "title": "Moby Dick",
9500
+ "author": {
9501
+ "$ref": "../autori/autori.json#/herman-melville"
9502
+ },
9503
+ "year": 1851,
9504
+ "genre": "Avventura",
9505
+ "summary": "La caccia alla balena bianca diventa ossessione metafisica e viaggio nell'abisso."
9506
+ },
9507
+ "delitto-e-castigo": {
9508
+ "id": "delitto-e-castigo",
9509
+ "title": "Delitto e castigo",
9510
+ "author": {
9511
+ "$ref": "../autori/autori.json#/fedor-dostoevskij"
9512
+ },
9513
+ "year": 1866,
9514
+ "genre": "Romanzo psicologico",
9515
+ "summary": "Raskolnikov attraversa colpa, febbre morale e possibilita di redenzione."
9516
+ },
9517
+ "anna-karenina": {
9518
+ "id": "anna-karenina",
9519
+ "title": "Anna Karenina",
9520
+ "author": {
9521
+ "$ref": "../autori/autori.json#/lev-tolstoj"
9522
+ },
9523
+ "year": 1877,
9524
+ "genre": "Classico",
9525
+ "summary": "Una storia d'amore e rovina dentro la societa russa dell'Ottocento."
9526
+ },
9527
+ "il-maestro-e-margherita": {
9528
+ "id": "il-maestro-e-margherita",
9529
+ "title": "Il maestro e Margherita",
9530
+ "author": {
9531
+ "$ref": "../autori/autori.json#/michail-bulgakov"
9532
+ },
9533
+ "year": 1967,
9534
+ "genre": "Satira fantastica",
9535
+ "summary": "Il diavolo visita Mosca in un romanzo visionario su arte, censura e amore."
9536
+ },
9537
+ "la-strada": {
9538
+ "id": "la-strada",
9539
+ "title": "La strada",
9540
+ "author": {
9541
+ "$ref": "../autori/autori.json#/cormac-mccarthy"
9542
+ },
9543
+ "year": 2006,
9544
+ "genre": "Post-apocalittico",
9545
+ "summary": "Padre e figlio attraversano un mondo bruciato portando con se una fragile idea di bene."
9546
+ },
9547
+ "cloud-atlas": {
9548
+ "id": "cloud-atlas",
9549
+ "title": "Cloud Atlas",
9550
+ "author": {
9551
+ "$ref": "../autori/autori.json#/david-mitchell"
9552
+ },
9553
+ "year": 2004,
9554
+ "genre": "Romanzo corale",
9555
+ "summary": "Sei storie in epoche diverse compongono una meditazione su potere, memoria e reincorrenza."
9556
+ },
9557
+ "kafka-sulla-spiaggia": {
9558
+ "id": "kafka-sulla-spiaggia",
9559
+ "title": "Kafka sulla spiaggia",
9560
+ "author": {
9561
+ "$ref": "../autori/autori.json#/haruki-murakami"
9562
+ },
9563
+ "year": 2002,
9564
+ "genre": "Surrealismo",
9565
+ "summary": "Fuga, destino e sogno si intrecciano in un romanzo sospeso tra reale e mitico."
9566
+ },
9567
+ "americanah": {
9568
+ "id": "americanah",
9569
+ "title": "Americanah",
9570
+ "author": {
9571
+ "$ref": "../autori/autori.json#/chimamanda-ngozi-adichie"
9572
+ },
9573
+ "year": 2013,
9574
+ "genre": "Romanzo contemporaneo",
9575
+ "summary": "Migrazione, razza e identita raccontate attraverso una storia d'amore tra Nigeria e Stati Uniti."
9576
+ },
9577
+ "la-casa-degli-spiriti": {
9578
+ "id": "la-casa-degli-spiriti",
9579
+ "title": "La casa degli spiriti",
9580
+ "author": {
9581
+ "$ref": "../autori/autori.json#/isabel-allende"
9582
+ },
9583
+ "year": 1982,
9584
+ "genre": "Saga familiare",
9585
+ "summary": "La storia della famiglia Trueba intreccia politica, memoria e realismo magico."
9586
+ },
9587
+ "beloved": {
9588
+ "id": "beloved",
9589
+ "title": "Beloved",
9590
+ "author": {
9591
+ "$ref": "../autori/autori.json#/toni-morrison"
9592
+ },
9593
+ "year": 1987,
9594
+ "genre": "Romanzo storico",
9595
+ "summary": "Il trauma della schiavitu ritorna come presenza viva nella casa di Sethe."
9596
+ },
9597
+ "la-breve-favolosa-vita-di-oscar-wao": {
9598
+ "id": "la-breve-favolosa-vita-di-oscar-wao",
9599
+ "title": "La breve favolosa vita di Oscar Wao",
9600
+ "author": {
9601
+ "$ref": "../autori/autori.json#/junot-diaz"
9602
+ },
9603
+ "year": 2007,
9604
+ "genre": "Romanzo contemporaneo",
9605
+ "summary": "Famiglia, diaspora dominicana e cultura pop si intrecciano nella storia di Oscar."
9606
+ },
9607
+ "le-correzioni": {
9608
+ "id": "le-correzioni",
9609
+ "title": "Le correzioni",
9610
+ "author": {
9611
+ "$ref": "../autori/autori.json#/jonathan-franzen"
9612
+ },
9613
+ "year": 2001,
9614
+ "genre": "Romanzo familiare",
9615
+ "summary": "Una famiglia americana tenta di ritrovarsi mentre ciascuno affronta le proprie fratture."
9616
+ }
9617
+ }
9618
+ END_OF_FILE_CONTENT
8570
9619
  mkdir -p "src/data/config"
8571
9620
  echo "Creating src/data/config/menu.json..."
8572
9621
  cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
@@ -8592,6 +9641,24 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/menu.json"
8592
9641
  "label": "GitHub",
8593
9642
  "href": "https://github.com/olonjs/core"
8594
9643
  }
9644
+ ],
9645
+ "footer": [
9646
+ {
9647
+ "id": "footer-home",
9648
+ "label": "Home",
9649
+ "href": "/"
9650
+ },
9651
+ {
9652
+ "id": "footer-libri",
9653
+ "label": "Libri",
9654
+ "href": "/libri"
9655
+ },
9656
+ {
9657
+ "id": "footer-github",
9658
+ "label": "GitHub",
9659
+ "href": "https://github.com/olonjs/core",
9660
+ "external": true
9661
+ }
8595
9662
  ]
8596
9663
  }
8597
9664
  END_OF_FILE_CONTENT
@@ -8647,7 +9714,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
8647
9714
  "title": "OlonJS",
8648
9715
  "logoUrl": "/brand/mark/olon-mark-dark.svg"
8649
9716
  },
8650
-
8651
9717
  "header": {
8652
9718
  "id": "global-header",
8653
9719
  "type": "header",
@@ -8686,13 +9752,11 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
8686
9752
  "type": "footer",
8687
9753
  "data": {
8688
9754
  "brandText": "OlonJS",
9755
+ "description": "AI-native content infrastructure for deterministic, sovereign, git-backed sites.",
8689
9756
  "copyright": "© 2026 OlonJS · v1.5 · Guido Serio",
8690
- "links": [
8691
- {
8692
- "label": "GitHub",
8693
- "href": "https://github.com/olonjs/core"
8694
- }
8695
- ],
9757
+ "links": {
9758
+ "$ref": "../config/menu.json#/footer"
9759
+ },
8696
9760
  "designSystemHref": ""
8697
9761
  },
8698
9762
  "settings": {
@@ -8869,11 +9933,70 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/theme.json"
8869
9933
  }
8870
9934
  END_OF_FILE_CONTENT
8871
9935
  mkdir -p "src/data/pages"
8872
- echo "Creating src/data/pages/home.json..."
8873
- cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
9936
+ mkdir -p "src/data/pages/authors"
9937
+ echo "Creating src/data/pages/authors.json..."
9938
+ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/authors.json"
9939
+ {
9940
+ "id": "authors-page",
9941
+ "slug": "authors",
9942
+ "meta": {
9943
+ "title": "Authors",
9944
+ "description": "Author directory powered by the autori collection."
9945
+ },
9946
+ "sections": [
9947
+ {
9948
+ "id": "authors-list-1",
9949
+ "type": "authors-list",
9950
+ "data": {
9951
+ "anchorId": "authors",
9952
+ "eyebrow": "Collection demo",
9953
+ "title": "Authors",
9954
+ "description": "A collection-backed directory of authors referenced by books.",
9955
+ "items": {
9956
+ "$ref": "../collections/autori/autori.json"
9957
+ }
9958
+ }
9959
+ }
9960
+ ],
9961
+ "global-header": false
9962
+ }
9963
+
9964
+ END_OF_FILE_CONTENT
9965
+ mkdir -p "src/data/pages/authors/[authorId]"
9966
+ echo "Creating src/data/pages/authors/[authorId]/libri.json..."
9967
+ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/authors/[authorId]/libri.json"
9968
+ {
9969
+ "id": "author-books-page",
9970
+ "slug": "authors/[authorId]/libri",
9971
+ "meta": {
9972
+ "title": "Libri per autore",
9973
+ "description": "Catalogo libri filtrato per autore."
9974
+ },
9975
+ "sections": [
9976
+ {
9977
+ "id": "books-list-author-filter",
9978
+ "type": "books-list",
9979
+ "data": {
9980
+ "anchorId": "libri-autore",
9981
+ "eyebrow": "Author books",
9982
+ "title": "Libri",
9983
+ "description": "Libri filtrati per autore dalla collection autori.",
9984
+ "items": {
9985
+ "$ref": "../../collections/libri/libri.json"
9986
+ },
9987
+ "pageSize": 10
9988
+ }
9989
+ }
9990
+ ],
9991
+ "global-header": false
9992
+ }
9993
+
9994
+ END_OF_FILE_CONTENT
9995
+ echo "Creating src/data/pages/form.json..."
9996
+ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/form.json"
8874
9997
  {
8875
- "id": "home-page",
8876
- "slug": "home",
9998
+ "id": "form-page",
9999
+ "slug": "form",
8877
10000
  "meta": {
8878
10001
  "title": "Home",
8879
10002
  "description": "OlonJS tenant alpha — form smoke test"
@@ -8896,6 +10019,93 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
8896
10019
  "global-header": false
8897
10020
  }
8898
10021
  END_OF_FILE_CONTENT
10022
+ echo "Creating src/data/pages/home.json..."
10023
+ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
10024
+ {
10025
+ "id": "libri-page",
10026
+ "slug": "home",
10027
+ "meta": {
10028
+ "title": "Libri",
10029
+ "description": "Catalogo libri dimostrativo alimentato da COP collections."
10030
+ },
10031
+ "sections": [
10032
+ {
10033
+ "id": "books-list-1",
10034
+ "type": "books-list",
10035
+ "data": {
10036
+ "anchorId": "catalogo-libri",
10037
+ "eyebrow": "Collection demo",
10038
+ "title": "Libri",
10039
+ "description": "Una pagina collection con 30 titoli, paginazione lato componente e link alle schede dinamiche.",
10040
+ "items": {
10041
+ "$ref": "../collections/libri/libri.json"
10042
+ },
10043
+ "pageSize": 10
10044
+ }
10045
+ }
10046
+ ],
10047
+ "global-header": false
10048
+ }
10049
+ END_OF_FILE_CONTENT
10050
+ mkdir -p "src/data/pages/libri"
10051
+ echo "Creating src/data/pages/libri.json..."
10052
+ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/libri.json"
10053
+ {
10054
+ "id": "libri-page",
10055
+ "slug": "libri",
10056
+ "meta": {
10057
+ "title": "Libri",
10058
+ "description": "Catalogo libri dimostrativo alimentato da COP collections."
10059
+ },
10060
+ "sections": [
10061
+ {
10062
+ "id": "books-list-1",
10063
+ "type": "books-list",
10064
+ "data": {
10065
+ "anchorId": "catalogo-libri",
10066
+ "eyebrow": "Collection demo",
10067
+ "title": "Libri",
10068
+ "description": "Una pagina collection con titoli, paginazione lato componente e filtro autore.",
10069
+ "items": {
10070
+ "$ref": "../collections/libri/libri.json"
10071
+ },
10072
+ "pageSize": 10
10073
+ }
10074
+ }
10075
+ ],
10076
+ "global-header": false
10077
+ }
10078
+
10079
+ END_OF_FILE_CONTENT
10080
+ echo "Creating src/data/pages/libri/[slug].json..."
10081
+ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/libri/[slug].json"
10082
+ {
10083
+ "id": "libro-detail-page",
10084
+ "slug": "libri/[slug]",
10085
+ "meta": {
10086
+ "title": "Dettaglio libro",
10087
+ "description": "Pagina dinamica dettaglio libro alimentata dalla collection libri."
10088
+ },
10089
+ "sections": [
10090
+ {
10091
+ "id": "book-detail-1",
10092
+ "type": "book-detail",
10093
+ "data": {
10094
+ "anchorId": "dettaglio-libro",
10095
+ "item": {
10096
+ "$ref": "collection:current"
10097
+ },
10098
+ "backLabel": "← Torna ai libri"
10099
+ }
10100
+ }
10101
+ ],
10102
+ "collection": {
10103
+ "source": "libri",
10104
+ "paramKey": "slug"
10105
+ },
10106
+ "global-header": false
10107
+ }
10108
+ END_OF_FILE_CONTENT
8899
10109
  mkdir -p "src/emails"
8900
10110
  echo "Creating src/emails/LeadNotificationEmail.tsx..."
8901
10111
  cat << 'END_OF_FILE_CONTENT' > "src/emails/LeadNotificationEmail.tsx"
@@ -9213,12 +10423,19 @@ echo "Creating src/entry-ssg.tsx..."
9213
10423
  cat << 'END_OF_FILE_CONTENT' > "src/entry-ssg.tsx"
9214
10424
  import { renderToString } from 'react-dom/server';
9215
10425
  import { StaticRouter } from 'react-router-dom/server';
9216
- import { ConfigProvider, PageRenderer, StudioProvider, resolveRuntimeConfig } from '@olonjs/core';
10426
+ import {
10427
+ ConfigProvider,
10428
+ PageRenderer,
10429
+ StudioProvider,
10430
+ contract,
10431
+ resolvePageMatchFromRegistry,
10432
+ resolveRuntimeConfig,
10433
+ } from '@olonjs/core';
9217
10434
  import type { JsonPagesConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
9218
10435
  import { ThemeProvider } from '@/components/ThemeProvider';
9219
10436
  import { ComponentRegistry } from '@/lib/ComponentRegistry';
9220
10437
  import { SECTION_SCHEMAS } from '@/lib/schemas';
9221
- import { menuConfig, pages, refDocuments, siteConfig, themeConfig } from '@/runtime';
10438
+ import { collectionSchemas, collections, menuConfig, pages, refDocuments, siteConfig, themeConfig } from '@/runtime';
9222
10439
  import tenantCss from '@/index.css?inline';
9223
10440
 
9224
10441
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -9233,10 +10450,16 @@ function getSortedSlugs(): string[] {
9233
10450
  return Object.keys(pages).sort((a, b) => a.localeCompare(b));
9234
10451
  }
9235
10452
 
9236
- function resolvePage(slug: string): { slug: string; page: PageConfig } {
10453
+ function resolvePage(slug: string): { slug: string; registrySlug: string; page: PageConfig; params: Record<string, string> } {
9237
10454
  const normalized = normalizeSlug(slug);
9238
- if (normalized && pages[normalized]) {
9239
- return { slug: normalized, page: pages[normalized] };
10455
+ const pageMatch = resolvePageMatchFromRegistry(pages, normalized);
10456
+ if (pageMatch) {
10457
+ return {
10458
+ slug: normalized || pageMatch.registrySlug,
10459
+ registrySlug: pageMatch.registrySlug,
10460
+ page: pageMatch.page,
10461
+ params: pageMatch.params,
10462
+ };
9240
10463
  }
9241
10464
 
9242
10465
  const slugs = getSortedSlugs();
@@ -9246,7 +10469,7 @@ function resolvePage(slug: string): { slug: string; page: PageConfig } {
9246
10469
 
9247
10470
  const home = slugs.find((item) => item === 'home');
9248
10471
  const fallbackSlug = home ?? slugs[0];
9249
- return { slug: fallbackSlug, page: pages[fallbackSlug] };
10472
+ return { slug: fallbackSlug, registrySlug: fallbackSlug, page: pages[fallbackSlug], params: {} };
9250
10473
  }
9251
10474
 
9252
10475
  function flattenThemeTokens(
@@ -9334,14 +10557,18 @@ function resolveTenantId(): string {
9334
10557
  export function render(slug: string): string {
9335
10558
  const resolved = resolvePage(slug);
9336
10559
  const location = resolved.slug === 'home' ? '/' : `/${resolved.slug}`;
10560
+ const collectionContext = contract.resolveCollectionContext(resolved.page, resolved.params, collections);
9337
10561
  const resolvedRuntime = resolveRuntimeConfig({
9338
- pages,
10562
+ pages: { [resolved.registrySlug]: resolved.page },
9339
10563
  siteConfig,
9340
10564
  themeConfig,
9341
10565
  menuConfig,
10566
+ collections,
10567
+ collectionSchemas,
10568
+ collectionContext,
9342
10569
  refDocuments,
9343
10570
  });
9344
- const resolvedPage = resolvedRuntime.pages[resolved.slug] ?? resolved.page;
10571
+ const resolvedPage = resolvedRuntime.pages[resolved.registrySlug] ?? resolved.page;
9345
10572
 
9346
10573
  return renderToString(
9347
10574
  <StaticRouter location={location}>
@@ -9391,12 +10618,22 @@ export function getPageMeta(slug: string): { title: string; description: string
9391
10618
  export function getWebMcpBuildState(): {
9392
10619
  pages: Record<string, PageConfig>;
9393
10620
  schemas: JsonPagesConfig['schemas'];
10621
+ collections: JsonPagesConfig['collections'];
10622
+ collectionSchemas: JsonPagesConfig['collectionSchemas'];
9394
10623
  siteConfig: SiteConfig;
10624
+ themeConfig: ThemeConfig;
10625
+ menuConfig: JsonPagesConfig['menuConfig'];
10626
+ refDocuments: JsonPagesConfig['refDocuments'];
9395
10627
  } {
9396
10628
  return {
9397
10629
  pages,
9398
10630
  schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
10631
+ collections,
10632
+ collectionSchemas,
9399
10633
  siteConfig,
10634
+ themeConfig,
10635
+ menuConfig,
10636
+ refDocuments,
9400
10637
  };
9401
10638
  }
9402
10639
 
@@ -10344,17 +11581,38 @@ cat << 'END_OF_FILE_CONTENT' > "src/index.css"
10344
11581
 
10345
11582
  END_OF_FILE_CONTENT
10346
11583
  mkdir -p "src/lib"
11584
+ echo "Creating src/lib/CollectionRegistry.ts..."
11585
+ cat << 'END_OF_FILE_CONTENT' > "src/lib/CollectionRegistry.ts"
11586
+ import { AutoriCollectionSchema } from '@/collections/autori';
11587
+ import { LibriCollectionSchema } from '@/collections/libri';
11588
+
11589
+ export const CollectionRegistry = {
11590
+ autori: AutoriCollectionSchema,
11591
+ libri: LibriCollectionSchema,
11592
+ } as const;
11593
+
11594
+ export type CollectionType = keyof typeof CollectionRegistry;
11595
+
11596
+ END_OF_FILE_CONTENT
10347
11597
  echo "Creating src/lib/ComponentRegistry.tsx..."
10348
11598
  cat << 'END_OF_FILE_CONTENT' > "src/lib/ComponentRegistry.tsx"
10349
11599
  import type { SectionType } from '@/types';
10350
11600
  import type { SectionComponentPropsMap } from '@/types';
11601
+ import { AuthorsListView } from '@/components/authors-list';
11602
+ import { BookDetailView } from '@/components/book-detail';
11603
+ import { BooksListView } from '@/components/books-list';
10351
11604
  import { EmptyTenantView } from '@/components/empty-tenant';
11605
+ import { FooterView } from '@/components/footer';
10352
11606
  import { FormDemoView } from '@/components/form-demo';
10353
11607
 
10354
11608
  export const ComponentRegistry: {
10355
11609
  [K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
10356
11610
  } = {
11611
+ 'authors-list': AuthorsListView as React.FC<SectionComponentPropsMap['authors-list']>,
11612
+ 'book-detail': BookDetailView as React.FC<SectionComponentPropsMap['book-detail']>,
11613
+ 'books-list': BooksListView as React.FC<SectionComponentPropsMap['books-list']>,
10357
11614
  'empty-tenant': EmptyTenantView as React.FC<SectionComponentPropsMap['empty-tenant']>,
11615
+ footer: FooterView as React.FC<SectionComponentPropsMap['footer']>,
10358
11616
  'form-demo': FormDemoView as React.FC<SectionComponentPropsMap['form-demo']>,
10359
11617
  };
10360
11618
 
@@ -10423,20 +11681,52 @@ echo "Creating src/lib/addSectionConfig.ts..."
10423
11681
  cat << 'END_OF_FILE_CONTENT' > "src/lib/addSectionConfig.ts"
10424
11682
  import type { AddSectionConfig } from '@olonjs/core';
10425
11683
 
10426
- const addableSectionTypes = ['empty-tenant', 'form-demo'] as const;
11684
+ const addableSectionTypes = ['authors-list', 'book-detail', 'books-list', 'empty-tenant', 'footer', 'form-demo'] as const;
10427
11685
 
10428
11686
  const sectionTypeLabels: Record<string, string> = {
11687
+ 'authors-list': 'Authors List',
11688
+ 'book-detail': 'Book Detail',
11689
+ 'books-list': 'Books List',
10429
11690
  'empty-tenant': 'Empty Tenant',
11691
+ footer: 'Footer',
10430
11692
  'form-demo': 'Form Demo',
10431
11693
  };
10432
11694
 
10433
11695
  function getDefaultSectionData(type: string): Record<string, unknown> {
10434
11696
  switch (type) {
11697
+ case 'authors-list':
11698
+ return {
11699
+ eyebrow: 'Collection demo',
11700
+ title: 'Authors',
11701
+ description: 'Authors loaded from the autori collection.',
11702
+ items: { $ref: '../collections/autori/autori.json' },
11703
+ };
11704
+ case 'book-detail':
11705
+ return {
11706
+ item: { $ref: 'collection:current' },
11707
+ backLabel: 'Torna ai libri',
11708
+ };
11709
+ case 'books-list':
11710
+ return {
11711
+ eyebrow: 'Collection demo',
11712
+ title: 'Libri',
11713
+ description: 'Catalogo dimostrativo alimentato dalla collection libri.',
11714
+ items: { $ref: '../collections/libri/libri.json' },
11715
+ pageSize: 10,
11716
+ };
10435
11717
  case 'empty-tenant':
10436
11718
  return {
10437
11719
  title: 'Your tenant is empty.',
10438
11720
  description: 'Create your first page to start building your site.',
10439
11721
  };
11722
+ case 'footer':
11723
+ return {
11724
+ brandText: 'OlonJS',
11725
+ description: 'AI-native content infrastructure for deterministic, git-backed sites.',
11726
+ copyright: '© 2026 OlonJS',
11727
+ links: [],
11728
+ designSystemHref: '',
11729
+ };
10440
11730
  case 'form-demo':
10441
11731
  return {
10442
11732
  title: 'Contattaci',
@@ -10484,6 +11774,49 @@ export function getHydratedData(
10484
11774
  };
10485
11775
  }
10486
11776
 
11777
+ END_OF_FILE_CONTENT
11778
+ echo "Creating src/lib/getFileCollections.ts..."
11779
+ cat << 'END_OF_FILE_CONTENT' > "src/lib/getFileCollections.ts"
11780
+ import type { JsonPagesConfig } from '@/types';
11781
+
11782
+ type CollectionDocuments = NonNullable<JsonPagesConfig['collections']>;
11783
+
11784
+ function collectionSourceFromPath(filePath: string): string | null {
11785
+ const normalizedPath = filePath.replace(/\\/g, '/');
11786
+ const match = normalizedPath.match(/\/data\/collections\/([^/]+)\/[^/]+\.json$/i);
11787
+ return match?.[1]?.trim() || null;
11788
+ }
11789
+
11790
+ export function getFileCollections(): CollectionDocuments {
11791
+ const glob = import.meta.glob<{ default: unknown }>('@/data/collections/**/*.json', { eager: true });
11792
+ const bySource = new Map<string, Record<string, unknown>>();
11793
+ const entries = Object.entries(glob).sort(([a], [b]) => a.localeCompare(b));
11794
+
11795
+ for (const [path, mod] of entries) {
11796
+ const source = collectionSourceFromPath(path);
11797
+ const raw = mod?.default;
11798
+ if (!source) {
11799
+ console.warn(`[tenant-alpha:getFileCollections] Ignoring collection module with invalid path "${path}".`);
11800
+ continue;
11801
+ }
11802
+ if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
11803
+ console.warn(`[tenant-alpha:getFileCollections] Ignoring invalid collection module at "${path}".`);
11804
+ continue;
11805
+ }
11806
+ if (bySource.has(source)) {
11807
+ console.warn(`[tenant-alpha:getFileCollections] Duplicate collection source "${source}" at "${path}". Keeping latest match.`);
11808
+ }
11809
+ bySource.set(source, raw as Record<string, unknown>);
11810
+ }
11811
+
11812
+ const collections: CollectionDocuments = {};
11813
+ for (const source of Array.from(bySource.keys()).sort((a, b) => a.localeCompare(b))) {
11814
+ const collection = bySource.get(source);
11815
+ if (collection) collections[source] = collection;
11816
+ }
11817
+ return collections;
11818
+ }
11819
+
10487
11820
  END_OF_FILE_CONTENT
10488
11821
  echo "Creating src/lib/getFilePages.ts..."
10489
11822
  cat << 'END_OF_FILE_CONTENT' > "src/lib/getFilePages.ts"
@@ -10535,11 +11868,19 @@ export function getFilePages(): Record<string, PageConfig> {
10535
11868
  END_OF_FILE_CONTENT
10536
11869
  echo "Creating src/lib/schemas.ts..."
10537
11870
  cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
11871
+ import { AuthorsListSchema } from '@/components/authors-list';
11872
+ import { BookDetailSchema } from '@/components/book-detail';
11873
+ import { BooksListSchema } from '@/components/books-list';
10538
11874
  import { EmptyTenantSchema } from '@/components/empty-tenant';
11875
+ import { FooterSchema } from '@/components/footer';
10539
11876
  import { FormDemoSchema, FormDemoSubmissionSchema } from '@/components/form-demo';
10540
11877
 
10541
11878
  export const SECTION_SCHEMAS = {
11879
+ 'authors-list': AuthorsListSchema,
11880
+ 'book-detail': BookDetailSchema,
11881
+ 'books-list': BooksListSchema,
10542
11882
  'empty-tenant': EmptyTenantSchema,
11883
+ footer: FooterSchema,
10543
11884
  'form-demo': FormDemoSchema,
10544
11885
  } as const;
10545
11886
 
@@ -10559,6 +11900,7 @@ export type SectionType = keyof typeof SECTION_SCHEMAS;
10559
11900
  export {
10560
11901
  BaseSectionData,
10561
11902
  BaseArrayItem,
11903
+ BaseCollectionItem,
10562
11904
  BaseSectionSettingsSchema,
10563
11905
  CtaSchema,
10564
11906
  ImageSelectionSchema,
@@ -10817,7 +12159,9 @@ END_OF_FILE_CONTENT
10817
12159
  echo "Creating src/runtime.ts..."
10818
12160
  cat << 'END_OF_FILE_CONTENT' > "src/runtime.ts"
10819
12161
  import type { JsonPagesConfig, MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
12162
+ import { CollectionRegistry } from '@/lib/CollectionRegistry';
10820
12163
  import { SECTION_SCHEMAS } from '@/lib/schemas';
12164
+ import { getFileCollections } from '@/lib/getFileCollections';
10821
12165
  import { getFilePages } from '@/lib/getFilePages';
10822
12166
  import siteData from '@/data/config/site.json';
10823
12167
  import menuData from '@/data/config/menu.json';
@@ -10827,6 +12171,8 @@ export const siteConfig = siteData as unknown as SiteConfig;
10827
12171
  export const themeConfig = themeData as unknown as ThemeConfig;
10828
12172
  export const menuConfig = menuData as unknown as MenuConfig;
10829
12173
  export const pages = getFilePages();
12174
+ export const collections = getFileCollections();
12175
+ export const collectionSchemas = CollectionRegistry as unknown as JsonPagesConfig['collectionSchemas'];
10830
12176
  export const refDocuments = {
10831
12177
  'menu.json': menuConfig,
10832
12178
  'config/menu.json': menuConfig,
@@ -10836,12 +12182,22 @@ export const refDocuments = {
10836
12182
  export function getWebMcpBuildState(): {
10837
12183
  pages: Record<string, PageConfig>;
10838
12184
  schemas: JsonPagesConfig['schemas'];
12185
+ collectionSchemas: JsonPagesConfig['collectionSchemas'];
12186
+ collections: JsonPagesConfig['collections'];
10839
12187
  siteConfig: SiteConfig;
12188
+ themeConfig: ThemeConfig;
12189
+ menuConfig: MenuConfig;
12190
+ refDocuments: JsonPagesConfig['refDocuments'];
10840
12191
  } {
10841
12192
  return {
10842
12193
  pages,
10843
12194
  schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
12195
+ collectionSchemas,
12196
+ collections,
10844
12197
  siteConfig,
12198
+ themeConfig,
12199
+ menuConfig,
12200
+ refDocuments,
10845
12201
  };
10846
12202
  }
10847
12203
 
@@ -10849,23 +12205,45 @@ END_OF_FILE_CONTENT
10849
12205
  # SKIP: src/runtime.ts:Zone.Identifier is binary and cannot be embedded as text.
10850
12206
  echo "Creating src/types.ts..."
10851
12207
  cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
12208
+ import type { AuthorsListData, AuthorsListSettings } from '@/components/authors-list';
12209
+ import type { BookDetailData, BookDetailSettings } from '@/components/book-detail';
12210
+ import type { BooksListData, BooksListSettings } from '@/components/books-list';
10852
12211
  import type { EmptyTenantData, EmptyTenantSettings } from '@/components/empty-tenant';
12212
+ import type { FooterData, FooterSettings } from '@/components/footer';
10853
12213
  import type { FormDemoData, FormDemoSettings } from '@/components/form-demo';
12214
+ import type { Autore } from '@/collections/autori';
12215
+ import type { Libro } from '@/collections/libri';
10854
12216
 
10855
12217
  export type SectionComponentPropsMap = {
12218
+ 'authors-list': { data: AuthorsListData; settings?: AuthorsListSettings };
12219
+ 'book-detail': { data: BookDetailData; settings?: BookDetailSettings };
12220
+ 'books-list': { data: BooksListData; settings?: BooksListSettings };
10856
12221
  'empty-tenant': { data: EmptyTenantData; settings?: EmptyTenantSettings };
12222
+ footer: { data: FooterData; settings?: FooterSettings };
10857
12223
  'form-demo': { data: FormDemoData; settings?: FormDemoSettings };
10858
12224
  };
10859
12225
 
10860
12226
  declare module '@olonjs/core' {
10861
12227
  export interface SectionDataRegistry {
12228
+ 'authors-list': AuthorsListData;
12229
+ 'book-detail': BookDetailData;
12230
+ 'books-list': BooksListData;
10862
12231
  'empty-tenant': EmptyTenantData;
12232
+ footer: FooterData;
10863
12233
  'form-demo': FormDemoData;
10864
12234
  }
10865
12235
  export interface SectionSettingsRegistry {
12236
+ 'authors-list': AuthorsListSettings;
12237
+ 'book-detail': BookDetailSettings;
12238
+ 'books-list': BooksListSettings;
10866
12239
  'empty-tenant': EmptyTenantSettings;
12240
+ footer: FooterSettings;
10867
12241
  'form-demo': FormDemoSettings;
10868
12242
  }
12243
+ export interface CollectionItemRegistry {
12244
+ autori: Autore;
12245
+ libri: Libro;
12246
+ }
10869
12247
  }
10870
12248
 
10871
12249
  export * from '@olonjs/core';
@@ -11027,6 +12405,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11027
12405
  const ASSETS_IMAGES_DIR = path.resolve(__dirname, 'public', 'assets', 'images');
11028
12406
  const DATA_CONFIG_DIR = path.resolve(__dirname, 'src', 'data', 'config');
11029
12407
  const DATA_PAGES_DIR = path.resolve(__dirname, 'src', 'data', 'pages');
12408
+ const DATA_COLLECTIONS_DIR = path.resolve(__dirname, 'src', 'data', 'collections');
11030
12409
  const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']);
11031
12410
  const IMAGE_MIMES = new Set([
11032
12411
  'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/avif',
@@ -11063,6 +12442,20 @@ function sendJsonFile(res, filePath) {
11063
12442
  }
11064
12443
  }
11065
12444
 
12445
+ function safeDataSlugPath(rootDir, rawSlug, fallback) {
12446
+ const slug = String(rawSlug || fallback)
12447
+ .replace(/\\/g, '/')
12448
+ .replace(/^\/+|\/+$/g, '');
12449
+ const segments = slug
12450
+ .split('/')
12451
+ .map((segment) => segment.replace(/[^a-zA-Z0-9_[\]-]/g, '_'))
12452
+ .filter(Boolean);
12453
+ const candidate = path.resolve(rootDir, `${segments.join(path.sep) || fallback}.json`);
12454
+ const isInsideRoot = candidate.startsWith(`${rootDir}${path.sep}`) || candidate === rootDir;
12455
+ if (!isInsideRoot) throw new Error('Invalid data path');
12456
+ return candidate;
12457
+ }
12458
+
11066
12459
  function isTenantPageJsonRequest(req, pathname) {
11067
12460
  if (req.method !== 'GET' || !pathname.endsWith('.json')) return false;
11068
12461
  const viteOrStaticPrefixes = ['/api/', '/assets/', '/src/', '/node_modules/', '/public/', '/@'];
@@ -11120,15 +12513,23 @@ export default defineConfig({
11120
12513
  const pageManifestMatch = pathname.match(/^\/mcp-manifests\/(.+)\.json$/i);
11121
12514
  if (pageManifestMatch && req.method === 'GET') {
11122
12515
  const slug = normalizeManifestSlug(pageManifestMatch[1]);
11123
- const pageConfig = buildState.pages[slug];
11124
- if (!pageConfig) {
12516
+ const resolved = core.resolvePublicPageDocument({
12517
+ slug,
12518
+ pages: buildState.pages,
12519
+ siteConfig: buildState.siteConfig,
12520
+ themeConfig: buildState.themeConfig,
12521
+ menuConfig: buildState.menuConfig,
12522
+ collections: buildState.collections,
12523
+ refDocuments: buildState.refDocuments,
12524
+ });
12525
+ if (!resolved) {
11125
12526
  sendJson(res, 404, { error: 'Page manifest not found' });
11126
12527
  return true;
11127
12528
  }
11128
12529
 
11129
12530
  sendJson(res, 200, buildPageManifest({
11130
12531
  slug,
11131
- pageConfig,
12532
+ pageConfig: resolved.page,
11132
12533
  schemas: buildState.schemas,
11133
12534
  submissionSchemas: buildState.submissionSchemas,
11134
12535
  siteConfig: buildState.siteConfig,
@@ -11139,15 +12540,23 @@ export default defineConfig({
11139
12540
  const schemaMatch = pathname.match(/^\/schemas\/(.+)\.schema\.json$/i);
11140
12541
  if (schemaMatch && req.method === 'GET') {
11141
12542
  const slug = normalizeManifestSlug(schemaMatch[1]);
11142
- const pageConfig = buildState.pages[slug];
11143
- if (!pageConfig) {
12543
+ const resolved = core.resolvePublicPageDocument({
12544
+ slug,
12545
+ pages: buildState.pages,
12546
+ siteConfig: buildState.siteConfig,
12547
+ themeConfig: buildState.themeConfig,
12548
+ menuConfig: buildState.menuConfig,
12549
+ collections: buildState.collections,
12550
+ refDocuments: buildState.refDocuments,
12551
+ });
12552
+ if (!resolved) {
11144
12553
  sendJson(res, 404, { error: 'Schema contract not found' });
11145
12554
  return true;
11146
12555
  }
11147
12556
 
11148
12557
  sendJson(res, 200, buildPageContract({
11149
12558
  slug,
11150
- pageConfig,
12559
+ pageConfig: resolved.page,
11151
12560
  schemas: buildState.schemas,
11152
12561
  submissionSchemas: buildState.submissionSchemas,
11153
12562
  siteConfig: buildState.siteConfig,
@@ -11188,13 +12597,31 @@ export default defineConfig({
11188
12597
  .replace(/^pages\//i, '')
11189
12598
  .replace(/\.json$/i, '')
11190
12599
  .replace(/^\/+|\/+$/g, '');
11191
- const candidate = path.resolve(DATA_PAGES_DIR, `${slug}.json`);
11192
- const isInsidePagesDir = candidate.startsWith(`${DATA_PAGES_DIR}${path.sep}`) || candidate === DATA_PAGES_DIR;
11193
- if (!slug || !isInsidePagesDir || !fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
12600
+ if (!slug) {
11194
12601
  sendJson(res, 404, { error: 'Page JSON not found' });
11195
12602
  return;
11196
12603
  }
11197
- sendJsonFile(res, candidate);
12604
+ void (async () => {
12605
+ const core = await loadWebMcpBuilders();
12606
+ const runtime = await server.ssrLoadModule('/src/runtime.ts');
12607
+ const buildState = runtime.getWebMcpBuildState();
12608
+ const resolved = core.resolvePublicPageDocument({
12609
+ slug,
12610
+ pages: buildState.pages,
12611
+ siteConfig: buildState.siteConfig,
12612
+ themeConfig: buildState.themeConfig,
12613
+ menuConfig: buildState.menuConfig,
12614
+ collections: buildState.collections,
12615
+ refDocuments: buildState.refDocuments,
12616
+ });
12617
+ if (!resolved) {
12618
+ sendJson(res, 404, { error: 'Page JSON not found' });
12619
+ return;
12620
+ }
12621
+ sendJson(res, 200, resolved.page);
12622
+ })().catch((error) => {
12623
+ sendJson(res, 500, { error: error?.message || 'Page JSON resolution failed' });
12624
+ });
11198
12625
  return;
11199
12626
  }
11200
12627
  if (req.method === 'GET' && req.url === '/api/list-assets') {
@@ -11213,12 +12640,23 @@ export default defineConfig({
11213
12640
  if (!projectState || typeof slug !== 'string') { sendJson(res, 400, { error: 'Missing projectState or slug' }); return; }
11214
12641
  if (!fs.existsSync(DATA_CONFIG_DIR)) fs.mkdirSync(DATA_CONFIG_DIR, { recursive: true });
11215
12642
  if (!fs.existsSync(DATA_PAGES_DIR)) fs.mkdirSync(DATA_PAGES_DIR, { recursive: true });
12643
+ if (!fs.existsSync(DATA_COLLECTIONS_DIR)) fs.mkdirSync(DATA_COLLECTIONS_DIR, { recursive: true });
11216
12644
  if (projectState.site != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'site.json'), JSON.stringify(projectState.site, null, 2), 'utf8');
11217
12645
  if (projectState.menu != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'menu.json'), JSON.stringify(projectState.menu, null, 2), 'utf8');
11218
12646
  if (projectState.theme != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'theme.json'), JSON.stringify(projectState.theme, null, 2), 'utf8');
11219
12647
  if (projectState.page != null) {
11220
- const safeSlug = (slug.replace(/[^a-zA-Z0-9-_]/g, '_') || 'page');
11221
- fs.writeFileSync(path.join(DATA_PAGES_DIR, `${safeSlug}.json`), JSON.stringify(projectState.page, null, 2), 'utf8');
12648
+ const pagePath = safeDataSlugPath(DATA_PAGES_DIR, slug, 'page');
12649
+ fs.mkdirSync(path.dirname(pagePath), { recursive: true });
12650
+ fs.writeFileSync(pagePath, JSON.stringify(projectState.page, null, 2), 'utf8');
12651
+ }
12652
+ if (projectState.collections && typeof projectState.collections === 'object') {
12653
+ for (const [source, collection] of Object.entries(projectState.collections)) {
12654
+ const sourceSlug = String(source).replace(/[^a-zA-Z0-9-_]/g, '_');
12655
+ if (!sourceSlug) continue;
12656
+ const collectionDir = path.join(DATA_COLLECTIONS_DIR, sourceSlug);
12657
+ fs.mkdirSync(collectionDir, { recursive: true });
12658
+ fs.writeFileSync(path.join(collectionDir, `${sourceSlug}.json`), JSON.stringify(collection, null, 2), 'utf8');
12659
+ }
11222
12660
  }
11223
12661
  sendJson(res, 200, { ok: true });
11224
12662
  } catch (e) { sendJson(res, 500, { error: e?.message || 'Save to file failed' }); }