@olonjs/cli 3.0.159 → 3.0.161
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.
|
@@ -10,6 +10,8 @@ cat << 'END_OF_FILE_CONTENT' > "app/[[...slug]]/page.tsx"
|
|
|
10
10
|
import type { Metadata } from 'next';
|
|
11
11
|
import { notFound } from 'next/navigation';
|
|
12
12
|
import {
|
|
13
|
+
buildPageContractHref,
|
|
14
|
+
buildPageManifestHref,
|
|
13
15
|
resolveCollectionContext,
|
|
14
16
|
resolveRuntimeConfig,
|
|
15
17
|
type PageConfig,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
import { loadVisitorPage } from '@olonjs/next/server';
|
|
18
20
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
19
21
|
import { CollectionRegistry } from '@/lib/CollectionRegistry';
|
|
22
|
+
import { buildVisitorWebPageJsonLd } from '@/lib/buildVisitorWebPageJsonLd';
|
|
20
23
|
import { getFileCollections } from '@/lib/loaders/getFileCollections';
|
|
21
24
|
import { getFilePages } from '@/lib/loaders/getFilePages';
|
|
22
25
|
import { getFileSiteBundle } from '@/lib/loaders/getFileSiteConfig';
|
|
@@ -109,9 +112,23 @@ export default async function VisitorCatchAllPage({ params, searchParams }: Page
|
|
|
109
112
|
const pageNum = Number(firstSearchValue(query.page) ?? '1') || 1;
|
|
110
113
|
const shell = resolveVisitorShell(page, resolved.siteConfig ?? siteConfig);
|
|
111
114
|
const sectionExtras = { authorId, page: pageNum, pathname };
|
|
115
|
+
const title = page.meta?.title ?? result.registrySlug;
|
|
116
|
+
const description = typeof page.meta?.description === 'string' ? page.meta.description : '';
|
|
117
|
+
const jsonLd = buildVisitorWebPageJsonLd({
|
|
118
|
+
title,
|
|
119
|
+
description,
|
|
120
|
+
slug: requestSlug,
|
|
121
|
+
});
|
|
112
122
|
|
|
113
123
|
return (
|
|
114
124
|
<>
|
|
125
|
+
<link rel="mcp-manifest" href={buildPageManifestHref(requestSlug)} />
|
|
126
|
+
<link rel="olon-contract" href={buildPageContractHref(requestSlug)} />
|
|
127
|
+
<script
|
|
128
|
+
type="application/ld+json"
|
|
129
|
+
// eslint-disable-next-line react/no-danger -- Schema.org JSON-LD payload (parity alpha bake)
|
|
130
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
131
|
+
/>
|
|
115
132
|
{shell.header ? (
|
|
116
133
|
<VisitorSection section={shell.header} extras={sectionExtras} />
|
|
117
134
|
) : null}
|
|
@@ -479,22 +496,26 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
479
496
|
"version": "0.0.1",
|
|
480
497
|
"private": true,
|
|
481
498
|
"type": "module",
|
|
499
|
+
"olonjs": {
|
|
500
|
+
"runtime": "next"
|
|
501
|
+
},
|
|
482
502
|
"scripts": {
|
|
483
503
|
"dev": "next dev",
|
|
484
|
-
"prebuild": "node scripts/sync-pages-to-public.mjs",
|
|
504
|
+
"prebuild": "node scripts/sync-pages-to-public.mjs && node scripts/generate-llms-txt.mjs && node scripts/bake.mjs && node scripts/sitemap.mjs && node scripts/robots.mjs",
|
|
485
505
|
"build": "next build",
|
|
486
506
|
"start": "next start",
|
|
487
507
|
"lint": "next lint",
|
|
488
508
|
"test": "vitest run",
|
|
489
509
|
"typecheck": "tsc --noEmit",
|
|
510
|
+
"verify:webmcp": "node scripts/webmcp-feature-check.mjs",
|
|
490
511
|
"dist": "bash ./src2Code.sh --template next app src scripts templates middleware.ts next.config.ts postcss.config.mjs tsconfig.json package.json next-env.d.ts vitest.config.ts",
|
|
491
512
|
"dist:dna": "npm run dist"
|
|
492
513
|
},
|
|
493
514
|
"dependencies": {
|
|
494
|
-
"@olonjs/core": "^1.1.
|
|
495
|
-
"@olonjs/next": "^0.0.
|
|
496
|
-
"@olonjs/react": "^0.1.
|
|
497
|
-
"@olonjs/studio": "^0.1.
|
|
515
|
+
"@olonjs/core": "^1.1.29",
|
|
516
|
+
"@olonjs/next": "^0.0.9",
|
|
517
|
+
"@olonjs/react": "^0.1.12",
|
|
518
|
+
"@olonjs/studio": "^0.1.12",
|
|
498
519
|
"clsx": "^2.1.1",
|
|
499
520
|
"lucide-react": "^0.474.0",
|
|
500
521
|
"next": "^15.5.0",
|
|
@@ -510,6 +531,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
510
531
|
"@types/react": "^19.0.0",
|
|
511
532
|
"@types/react-dom": "^19.0.0",
|
|
512
533
|
"tailwindcss": "^4.0.0",
|
|
534
|
+
"tsx": "^4.20.5",
|
|
513
535
|
"typescript": "^5.7.3",
|
|
514
536
|
"vitest": "^3.0.0"
|
|
515
537
|
}
|
|
@@ -529,6 +551,245 @@ export default config;
|
|
|
529
551
|
|
|
530
552
|
END_OF_FILE_CONTENT
|
|
531
553
|
mkdir -p "scripts"
|
|
554
|
+
echo "Creating scripts/bake.mjs..."
|
|
555
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/bake.mjs"
|
|
556
|
+
/**
|
|
557
|
+
* Next bake entry — agentic WebMCP artifacts only (no Vite / no HTML SSG).
|
|
558
|
+
* Runs scripts/bake.ts via tsx so SECTION_SCHEMAS can be imported from the tenant.
|
|
559
|
+
*/
|
|
560
|
+
import { spawnSync } from 'node:child_process';
|
|
561
|
+
import path from 'node:path';
|
|
562
|
+
import { fileURLToPath } from 'node:url';
|
|
563
|
+
|
|
564
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
565
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
566
|
+
const bakeTs = path.join(__dirname, 'bake.ts');
|
|
567
|
+
|
|
568
|
+
const result = spawnSync(
|
|
569
|
+
process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
570
|
+
['tsx', '--tsconfig', 'tsconfig.json', bakeTs],
|
|
571
|
+
{
|
|
572
|
+
cwd: rootDir,
|
|
573
|
+
stdio: 'inherit',
|
|
574
|
+
env: process.env,
|
|
575
|
+
shell: process.platform === 'win32',
|
|
576
|
+
},
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
process.exit(result.status ?? 1);
|
|
580
|
+
|
|
581
|
+
END_OF_FILE_CONTENT
|
|
582
|
+
echo "Creating scripts/bake.ts..."
|
|
583
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/bake.ts"
|
|
584
|
+
/**
|
|
585
|
+
* Next bake — agentic WebMCP artifacts only (no Vite / no HTML SSG).
|
|
586
|
+
* Invoked by scripts/bake.mjs via tsx.
|
|
587
|
+
*/
|
|
588
|
+
import fs from 'node:fs/promises';
|
|
589
|
+
import path from 'node:path';
|
|
590
|
+
import { fileURLToPath } from 'node:url';
|
|
591
|
+
import {
|
|
592
|
+
resolvePageMatchFromRegistry,
|
|
593
|
+
resolvePublicPageDocument,
|
|
594
|
+
webmcp,
|
|
595
|
+
} from '@olonjs/core';
|
|
596
|
+
import { CollectionRegistry } from '../src/lib/CollectionRegistry';
|
|
597
|
+
import { SECTION_SCHEMAS, SECTION_SUBMISSION_SCHEMAS } from '../src/lib/schemas';
|
|
598
|
+
import { getFileCollections } from '../src/lib/loaders/getFileCollections';
|
|
599
|
+
import { getFilePages } from '../src/lib/loaders/getFilePages';
|
|
600
|
+
import { getFileSiteBundle } from '../src/lib/loaders/getFileSiteConfig';
|
|
601
|
+
|
|
602
|
+
const {
|
|
603
|
+
buildPageContract,
|
|
604
|
+
buildPageManifest,
|
|
605
|
+
buildPageManifestHref,
|
|
606
|
+
buildSiteManifest,
|
|
607
|
+
buildLlmsTxt,
|
|
608
|
+
} = webmcp;
|
|
609
|
+
|
|
610
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
611
|
+
const root = path.resolve(__dirname, '..');
|
|
612
|
+
const publicDir = path.join(root, 'public');
|
|
613
|
+
const pagesDir = path.join(root, 'src', 'data', 'pages');
|
|
614
|
+
const collectionsDir = path.join(root, 'src', 'data', 'collections');
|
|
615
|
+
|
|
616
|
+
async function writePublic(relativePath: string, content: string): Promise<void> {
|
|
617
|
+
const target = path.join(publicDir, relativePath);
|
|
618
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
619
|
+
await fs.writeFile(target, content, 'utf-8');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function writePublicJson(relativePath: string, value: unknown): Promise<void> {
|
|
623
|
+
await writePublic(relativePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function readJsonFile(filePath: string): Promise<unknown> {
|
|
627
|
+
return JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function listJsonFilesRecursive(dir: string): Promise<string[]> {
|
|
631
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
632
|
+
const files: string[] = [];
|
|
633
|
+
for (const item of items) {
|
|
634
|
+
const fullPath = path.join(dir, item.name);
|
|
635
|
+
if (item.isDirectory()) {
|
|
636
|
+
files.push(...(await listJsonFilesRecursive(fullPath)));
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
|
|
640
|
+
}
|
|
641
|
+
return files;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function toCanonicalSlug(relativeJsonPath: string): string {
|
|
645
|
+
const slug = relativeJsonPath.replace(/\\/g, '/').replace(/\.json$/i, '').replace(/^\/+|\/+$/g, '');
|
|
646
|
+
if (!slug) throw new Error('[bake] Invalid page slug: empty path segment');
|
|
647
|
+
return slug;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function expandCollectionTarget(slug: string, pageFilePath: string): Promise<string[]> {
|
|
651
|
+
let pageConfig: Record<string, unknown>;
|
|
652
|
+
try {
|
|
653
|
+
pageConfig = (await readJsonFile(pageFilePath)) as Record<string, unknown>;
|
|
654
|
+
} catch {
|
|
655
|
+
return [slug];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const binding = pageConfig?.collection as { source?: string; paramKey?: string } | undefined;
|
|
659
|
+
if (!binding || typeof binding.source !== 'string' || typeof binding.paramKey !== 'string') {
|
|
660
|
+
return [slug];
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const token = `[${binding.paramKey}]`;
|
|
664
|
+
const authoredSlug =
|
|
665
|
+
typeof pageConfig.slug === 'string'
|
|
666
|
+
? String(pageConfig.slug).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')
|
|
667
|
+
: '';
|
|
668
|
+
const routePattern =
|
|
669
|
+
authoredSlug.includes(token) ? authoredSlug : slug.includes(token) ? slug : '';
|
|
670
|
+
if (!routePattern) return [slug];
|
|
671
|
+
|
|
672
|
+
const collectionPath = path.resolve(collectionsDir, binding.source, `${binding.source}.json`);
|
|
673
|
+
let collection: Record<string, unknown>;
|
|
674
|
+
try {
|
|
675
|
+
collection = (await readJsonFile(collectionPath)) as Record<string, unknown>;
|
|
676
|
+
} catch {
|
|
677
|
+
return [slug];
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (!collection || typeof collection !== 'object' || Array.isArray(collection)) return [slug];
|
|
681
|
+
const itemIds = Object.keys(collection).sort((a, b) => a.localeCompare(b));
|
|
682
|
+
return itemIds.length > 0
|
|
683
|
+
? itemIds.map((itemId) => routePattern.replace(token, itemId))
|
|
684
|
+
: [slug];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function discoverSlugs(): Promise<string[]> {
|
|
688
|
+
let files: string[] = [];
|
|
689
|
+
try {
|
|
690
|
+
files = await listJsonFilesRecursive(pagesDir);
|
|
691
|
+
} catch {
|
|
692
|
+
files = [];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const rawSlugs = (
|
|
696
|
+
await Promise.all(
|
|
697
|
+
files.map(async (fullPath) => {
|
|
698
|
+
const slug = toCanonicalSlug(path.relative(pagesDir, fullPath));
|
|
699
|
+
return expandCollectionTarget(slug, fullPath);
|
|
700
|
+
}),
|
|
701
|
+
)
|
|
702
|
+
).flat();
|
|
703
|
+
|
|
704
|
+
return Array.from(new Set(rawSlugs)).sort((a, b) => a.localeCompare(b));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function main(): Promise<void> {
|
|
708
|
+
console.log('\n[bake] Next agentic artifacts (no Vite SSG)...');
|
|
709
|
+
|
|
710
|
+
const pages = getFilePages(root);
|
|
711
|
+
const collections = getFileCollections(root);
|
|
712
|
+
const { siteConfig, themeConfig, menuConfig } = getFileSiteBundle(root);
|
|
713
|
+
const collectionSchemas = CollectionRegistry as unknown as Record<string, unknown>;
|
|
714
|
+
const schemas = SECTION_SCHEMAS as unknown as Record<string, unknown>;
|
|
715
|
+
const submissionSchemas = SECTION_SUBMISSION_SCHEMAS as unknown as Record<string, unknown>;
|
|
716
|
+
const refDocuments = {
|
|
717
|
+
'menu.json': menuConfig,
|
|
718
|
+
'config/menu.json': menuConfig,
|
|
719
|
+
'src/data/config/menu.json': menuConfig,
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
const slugs = await discoverSlugs();
|
|
723
|
+
if (slugs.length === 0) {
|
|
724
|
+
throw new Error('[bake] No pages discovered under src/data/pages');
|
|
725
|
+
}
|
|
726
|
+
console.log(`[bake] Targets: ${slugs.join(', ')}`);
|
|
727
|
+
|
|
728
|
+
const pagesForManifest: Record<string, (typeof pages)[string]> = { ...pages };
|
|
729
|
+
|
|
730
|
+
for (const slug of slugs) {
|
|
731
|
+
const pageConfig = resolvePageMatchFromRegistry(pages, slug)?.page;
|
|
732
|
+
if (!pageConfig) continue;
|
|
733
|
+
|
|
734
|
+
const resolvedPageDocument = resolvePublicPageDocument({
|
|
735
|
+
slug,
|
|
736
|
+
pages,
|
|
737
|
+
siteConfig,
|
|
738
|
+
themeConfig,
|
|
739
|
+
menuConfig,
|
|
740
|
+
collections,
|
|
741
|
+
collectionSchemas: collectionSchemas as never,
|
|
742
|
+
refDocuments,
|
|
743
|
+
});
|
|
744
|
+
const publicPageConfig = resolvedPageDocument?.page ?? pageConfig;
|
|
745
|
+
pagesForManifest[slug] = publicPageConfig;
|
|
746
|
+
|
|
747
|
+
await writePublicJson(`pages/${slug}.json`, publicPageConfig);
|
|
748
|
+
|
|
749
|
+
const contract = buildPageContract({
|
|
750
|
+
slug,
|
|
751
|
+
pageConfig: publicPageConfig,
|
|
752
|
+
schemas: schemas as never,
|
|
753
|
+
submissionSchemas: submissionSchemas as never,
|
|
754
|
+
siteConfig,
|
|
755
|
+
});
|
|
756
|
+
await writePublicJson(`schemas/${slug}.schema.json`, contract);
|
|
757
|
+
|
|
758
|
+
const pageManifest = buildPageManifest({
|
|
759
|
+
slug,
|
|
760
|
+
pageConfig: publicPageConfig,
|
|
761
|
+
schemas: schemas as never,
|
|
762
|
+
siteConfig,
|
|
763
|
+
});
|
|
764
|
+
await writePublicJson(buildPageManifestHref(slug).replace(/^\//, ''), pageManifest);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
await writePublicJson('config/site.json', siteConfig);
|
|
768
|
+
|
|
769
|
+
const mcpManifest = buildSiteManifest({
|
|
770
|
+
pages: pagesForManifest,
|
|
771
|
+
schemas: schemas as never,
|
|
772
|
+
siteConfig,
|
|
773
|
+
});
|
|
774
|
+
await writePublicJson('mcp-manifest.json', mcpManifest);
|
|
775
|
+
|
|
776
|
+
const llmsTxtContent = buildLlmsTxt({
|
|
777
|
+
pages: pagesForManifest,
|
|
778
|
+
schemas: schemas as never,
|
|
779
|
+
siteConfig,
|
|
780
|
+
});
|
|
781
|
+
await writePublic('llms.txt', `${llmsTxtContent}\n`);
|
|
782
|
+
|
|
783
|
+
console.log('[bake] Wrote public/mcp-manifest.json, mcp-manifests/, schemas/, pages/, llms.txt, config/site.json');
|
|
784
|
+
console.log('[bake] OK\n');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
main().catch((error) => {
|
|
788
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
789
|
+
process.exit(1);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
END_OF_FILE_CONTENT
|
|
532
793
|
echo "Creating scripts/generate-SystemsArchitect-next.test.mjs..."
|
|
533
794
|
cat << 'END_OF_FILE_CONTENT' > "scripts/generate-SystemsArchitect-next.test.mjs"
|
|
534
795
|
/**
|
|
@@ -581,12 +842,15 @@ describe('generate_SystemsArchitect_next.sh harness gates', () => {
|
|
|
581
842
|
assert.match(src, /cd "\$\(cd "\$\(dirname "\$\{BASH_SOURCE\[0\]\}"\)\/\.\." && pwd\)"/);
|
|
582
843
|
});
|
|
583
844
|
|
|
584
|
-
it('must
|
|
845
|
+
it('must wipe tenant content surfaces without DNA denylist', () => {
|
|
585
846
|
const src = readScript();
|
|
586
|
-
assert.match(src, /
|
|
587
|
-
assert.match(src, /
|
|
847
|
+
assert.match(src, /Wiping tenant content surfaces/);
|
|
848
|
+
assert.match(src, /find src\/components -mindepth 1 -maxdepth 1 ! -name 'ui' ! -name 'admin'/);
|
|
849
|
+
assert.match(src, /rm -rf \\\s*\n\s*src\/collections/m);
|
|
850
|
+
assert.match(src, /rm -rf \\\s*\n[\s\S]*?src\/data\/pages/m);
|
|
588
851
|
assert.match(src, /cat > src\/lib\/VisitorSection\.tsx/);
|
|
589
852
|
assert.doesNotMatch(src, /from '@\/components\/books-list'/);
|
|
853
|
+
assert.doesNotMatch(src, /src\/components\/books-list/);
|
|
590
854
|
});
|
|
591
855
|
|
|
592
856
|
it('must install react-markdown deps used by post-detail', () => {
|
|
@@ -597,11 +861,10 @@ describe('generate_SystemsArchitect_next.sh harness gates', () => {
|
|
|
597
861
|
assert.match(src, /from 'rehype-sanitize'/);
|
|
598
862
|
});
|
|
599
863
|
|
|
600
|
-
it('must not generate empty-tenant registry wiring
|
|
864
|
+
it('must not generate empty-tenant registry wiring', () => {
|
|
601
865
|
const src = readScript();
|
|
602
866
|
assert.doesNotMatch(src, /@\/components\/empty-tenant/);
|
|
603
867
|
assert.doesNotMatch(src, /['"]empty-tenant['"]/);
|
|
604
|
-
assert.match(src, /src\/components\/empty-tenant/);
|
|
605
868
|
});
|
|
606
869
|
|
|
607
870
|
it('must force shadcn radix base non-interactively', () => {
|
|
@@ -664,15 +927,435 @@ describe('generate_inkwell_next.sh harness gates', () => {
|
|
|
664
927
|
assert.match(src, /cd "\$\(cd "\$\(dirname "\$\{BASH_SOURCE\[0\]\}"\)\/\.\." && pwd\)"/);
|
|
665
928
|
});
|
|
666
929
|
|
|
667
|
-
it('must
|
|
930
|
+
it('must wipe tenant content surfaces without DNA denylist', () => {
|
|
668
931
|
const src = readScript();
|
|
669
|
-
assert.match(src, /
|
|
670
|
-
assert.match(src, /
|
|
932
|
+
assert.match(src, /Wiping tenant content surfaces/);
|
|
933
|
+
assert.match(src, /find src\/components -mindepth 1 -maxdepth 1 ! -name 'ui' ! -name 'admin'/);
|
|
934
|
+
assert.match(src, /rm -rf \\\s*\n\s*src\/collections/m);
|
|
935
|
+
assert.match(src, /rm -rf \\\s*\n[\s\S]*?src\/data\/pages/m);
|
|
671
936
|
assert.match(src, /cat > src\/lib\/VisitorSection\.tsx/);
|
|
937
|
+
assert.match(src, /EmptyTenantView empty-branch/);
|
|
672
938
|
assert.doesNotMatch(src, /from '@\/components\/books-list'/);
|
|
939
|
+
assert.doesNotMatch(src, /src\/components\/books-list/);
|
|
673
940
|
});
|
|
674
941
|
});
|
|
675
942
|
|
|
943
|
+
END_OF_FILE_CONTENT
|
|
944
|
+
echo "Creating scripts/generate-llms-txt.mjs..."
|
|
945
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/generate-llms-txt.mjs"
|
|
946
|
+
import fs from 'fs';
|
|
947
|
+
import path from 'path';
|
|
948
|
+
import { fileURLToPath } from 'url';
|
|
949
|
+
import { webmcp } from '@olonjs/core';
|
|
950
|
+
|
|
951
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
952
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
953
|
+
const { buildLlmsTxt } = webmcp;
|
|
954
|
+
|
|
955
|
+
const pagesDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
956
|
+
const siteConfig = JSON.parse(fs.readFileSync(path.join(rootDir, 'src', 'data', 'config', 'site.json'), 'utf-8'));
|
|
957
|
+
|
|
958
|
+
function listJsonFilesRecursive(dir) {
|
|
959
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
960
|
+
const files = [];
|
|
961
|
+
for (const item of items) {
|
|
962
|
+
const fullPath = path.join(dir, item.name);
|
|
963
|
+
if (item.isDirectory()) {
|
|
964
|
+
files.push(...listJsonFilesRecursive(fullPath));
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
|
|
968
|
+
}
|
|
969
|
+
return files;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const pages = {};
|
|
973
|
+
for (const fullPath of listJsonFilesRecursive(pagesDir)) {
|
|
974
|
+
const slug = path.relative(pagesDir, fullPath).replace(/\\/g, '/').replace(/\.json$/i, '');
|
|
975
|
+
pages[slug] = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const llmsTxt = buildLlmsTxt({ pages, schemas: {}, siteConfig });
|
|
979
|
+
|
|
980
|
+
const outPath = path.join(rootDir, 'public', 'llms.txt');
|
|
981
|
+
fs.writeFileSync(outPath, llmsTxt, 'utf-8');
|
|
982
|
+
console.log('[generate-llms-txt] Written -> public/llms.txt');
|
|
983
|
+
|
|
984
|
+
END_OF_FILE_CONTENT
|
|
985
|
+
echo "Creating scripts/prebuild-bake.test.mjs..."
|
|
986
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/prebuild-bake.test.mjs"
|
|
987
|
+
/**
|
|
988
|
+
* Gates for bake.mjs (Task 4 — plan 001). Agentic artifacts only; no Vite SSG.
|
|
989
|
+
* Run: node --test scripts/prebuild-bake.test.mjs
|
|
990
|
+
*/
|
|
991
|
+
import assert from 'node:assert/strict';
|
|
992
|
+
import fs from 'node:fs';
|
|
993
|
+
import path from 'node:path';
|
|
994
|
+
import { spawnSync } from 'node:child_process';
|
|
995
|
+
import { describe, it } from 'node:test';
|
|
996
|
+
import { fileURLToPath } from 'node:url';
|
|
997
|
+
|
|
998
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
999
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
1000
|
+
const SCRIPT = path.join(__dirname, 'bake.mjs');
|
|
1001
|
+
|
|
1002
|
+
describe('next bake.mjs (agentic artifacts)', () => {
|
|
1003
|
+
it('exists and does not import vite / write HTML SSG', () => {
|
|
1004
|
+
assert.ok(fs.existsSync(SCRIPT), `missing ${SCRIPT}`);
|
|
1005
|
+
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
1006
|
+
assert.doesNotMatch(src, /from ['"]vite['"]/);
|
|
1007
|
+
assert.doesNotMatch(src, /vite\.build|entry-ssg/);
|
|
1008
|
+
// May spawn tsx helper — check companion if present
|
|
1009
|
+
const impl = path.join(__dirname, 'bake.ts');
|
|
1010
|
+
if (fs.existsSync(impl)) {
|
|
1011
|
+
const implSrc = fs.readFileSync(impl, 'utf8');
|
|
1012
|
+
assert.doesNotMatch(implSrc, /from ['"]vite['"]/);
|
|
1013
|
+
assert.doesNotMatch(implSrc, /entry-ssg/);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it('writes reachable public agentic artifacts', () => {
|
|
1018
|
+
const result = spawnSync(process.execPath, [SCRIPT], {
|
|
1019
|
+
cwd: ROOT,
|
|
1020
|
+
encoding: 'utf8',
|
|
1021
|
+
env: { ...process.env },
|
|
1022
|
+
});
|
|
1023
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
1024
|
+
|
|
1025
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public', 'mcp-manifest.json')));
|
|
1026
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public', 'llms.txt')));
|
|
1027
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public', 'schemas', 'home.schema.json')));
|
|
1028
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public', 'mcp-manifests', 'home.json')));
|
|
1029
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public', 'config', 'site.json')));
|
|
1030
|
+
|
|
1031
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'public', 'mcp-manifest.json'), 'utf8'));
|
|
1032
|
+
assert.equal(manifest.kind, 'olonjs-mcp-manifest-index');
|
|
1033
|
+
assert.ok(Array.isArray(manifest.pages));
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
END_OF_FILE_CONTENT
|
|
1038
|
+
echo "Creating scripts/prebuild-generate-llms-txt.test.mjs..."
|
|
1039
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/prebuild-generate-llms-txt.test.mjs"
|
|
1040
|
+
/**
|
|
1041
|
+
* Gates for generate-llms-txt.mjs (Task 2 — plan 001).
|
|
1042
|
+
* Run: node --test scripts/prebuild-generate-llms-txt.test.mjs
|
|
1043
|
+
*/
|
|
1044
|
+
import assert from 'node:assert/strict';
|
|
1045
|
+
import fs from 'node:fs';
|
|
1046
|
+
import path from 'node:path';
|
|
1047
|
+
import { spawnSync } from 'node:child_process';
|
|
1048
|
+
import { describe, it } from 'node:test';
|
|
1049
|
+
import { fileURLToPath } from 'node:url';
|
|
1050
|
+
|
|
1051
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1052
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
1053
|
+
const SCRIPT = path.join(__dirname, 'generate-llms-txt.mjs');
|
|
1054
|
+
|
|
1055
|
+
describe('next prebuild generate-llms-txt', () => {
|
|
1056
|
+
it('script exists and uses @olonjs/core webmcp', () => {
|
|
1057
|
+
assert.ok(fs.existsSync(SCRIPT), `missing ${SCRIPT}`);
|
|
1058
|
+
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
1059
|
+
assert.match(src, /@olonjs\/core/);
|
|
1060
|
+
assert.match(src, /buildLlmsTxt|webmcp/);
|
|
1061
|
+
assert.match(src, /public\/llms\.txt|llms\.txt/);
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
it('writes public/llms.txt when run', () => {
|
|
1065
|
+
const out = path.join(ROOT, 'public', 'llms.txt');
|
|
1066
|
+
if (fs.existsSync(out)) fs.unlinkSync(out);
|
|
1067
|
+
|
|
1068
|
+
const result = spawnSync(process.execPath, [SCRIPT], { cwd: ROOT, encoding: 'utf8' });
|
|
1069
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
1070
|
+
assert.ok(fs.existsSync(out));
|
|
1071
|
+
const body = fs.readFileSync(out, 'utf8');
|
|
1072
|
+
assert.ok(body.trim().length > 0);
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
END_OF_FILE_CONTENT
|
|
1077
|
+
echo "Creating scripts/prebuild-robots-sitemap.test.mjs..."
|
|
1078
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/prebuild-robots-sitemap.test.mjs"
|
|
1079
|
+
/**
|
|
1080
|
+
* Static + smoke gates for robots.mjs / sitemap.mjs (Task 1 — plan 001).
|
|
1081
|
+
* Run: node --test scripts/prebuild-robots-sitemap.test.mjs
|
|
1082
|
+
*/
|
|
1083
|
+
import assert from 'node:assert/strict';
|
|
1084
|
+
import fs from 'node:fs';
|
|
1085
|
+
import path from 'node:path';
|
|
1086
|
+
import { spawnSync } from 'node:child_process';
|
|
1087
|
+
import { describe, it } from 'node:test';
|
|
1088
|
+
import { fileURLToPath } from 'node:url';
|
|
1089
|
+
|
|
1090
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1091
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
1092
|
+
const ROBOTS = path.join(__dirname, 'robots.mjs');
|
|
1093
|
+
const SITEMAP = path.join(__dirname, 'sitemap.mjs');
|
|
1094
|
+
|
|
1095
|
+
describe('next prebuild robots + sitemap', () => {
|
|
1096
|
+
it('scripts exist', () => {
|
|
1097
|
+
assert.ok(fs.existsSync(ROBOTS), `missing ${ROBOTS}`);
|
|
1098
|
+
assert.ok(fs.existsSync(SITEMAP), `missing ${SITEMAP}`);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
it('defaults to Next localhost:3000 (not Vite 5173)', () => {
|
|
1102
|
+
const robots = fs.readFileSync(ROBOTS, 'utf8');
|
|
1103
|
+
const sitemap = fs.readFileSync(SITEMAP, 'utf8');
|
|
1104
|
+
assert.match(robots, /localhost:3000/);
|
|
1105
|
+
assert.match(sitemap, /localhost:3000/);
|
|
1106
|
+
assert.doesNotMatch(robots, /localhost:5173/);
|
|
1107
|
+
assert.doesNotMatch(sitemap, /localhost:5173/);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it('writes public/robots.txt and public/sitemap.xml when run', () => {
|
|
1111
|
+
const robotsOut = path.join(ROOT, 'public', 'robots.txt');
|
|
1112
|
+
const sitemapOut = path.join(ROOT, 'public', 'sitemap.xml');
|
|
1113
|
+
for (const p of [robotsOut, sitemapOut]) {
|
|
1114
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const r1 = spawnSync(process.execPath, [ROBOTS], { cwd: ROOT, encoding: 'utf8' });
|
|
1118
|
+
assert.equal(r1.status, 0, r1.stderr || r1.stdout);
|
|
1119
|
+
const r2 = spawnSync(process.execPath, [SITEMAP], { cwd: ROOT, encoding: 'utf8' });
|
|
1120
|
+
assert.equal(r2.status, 0, r2.stderr || r2.stdout);
|
|
1121
|
+
|
|
1122
|
+
assert.ok(fs.existsSync(robotsOut));
|
|
1123
|
+
assert.ok(fs.existsSync(sitemapOut));
|
|
1124
|
+
const robotsTxt = fs.readFileSync(robotsOut, 'utf8');
|
|
1125
|
+
const sitemapXml = fs.readFileSync(sitemapOut, 'utf8');
|
|
1126
|
+
assert.match(robotsTxt, /Sitemap: http:\/\/localhost:3000\/sitemap\.xml/);
|
|
1127
|
+
assert.match(sitemapXml, /<urlset/);
|
|
1128
|
+
assert.match(sitemapXml, /\/home\.json|PAGE: HOME|loc>http:\/\/localhost:3000\/</);
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
END_OF_FILE_CONTENT
|
|
1133
|
+
echo "Creating scripts/prebuild-webmcp-feature-check.test.mjs..."
|
|
1134
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/prebuild-webmcp-feature-check.test.mjs"
|
|
1135
|
+
/**
|
|
1136
|
+
* Gates for webmcp-feature-check.mjs (Task 3 — plan 001).
|
|
1137
|
+
* Run: node --test scripts/prebuild-webmcp-feature-check.test.mjs
|
|
1138
|
+
*/
|
|
1139
|
+
import assert from 'node:assert/strict';
|
|
1140
|
+
import fs from 'node:fs';
|
|
1141
|
+
import path from 'node:path';
|
|
1142
|
+
import { describe, it } from 'node:test';
|
|
1143
|
+
import { fileURLToPath } from 'node:url';
|
|
1144
|
+
|
|
1145
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1146
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
1147
|
+
const SCRIPT = path.join(__dirname, 'webmcp-feature-check.mjs');
|
|
1148
|
+
const PKG = path.join(ROOT, 'package.json');
|
|
1149
|
+
|
|
1150
|
+
describe('next verify:webmcp script', () => {
|
|
1151
|
+
it('exists and uses document.modelContextTesting only', () => {
|
|
1152
|
+
assert.ok(fs.existsSync(SCRIPT), `missing ${SCRIPT}`);
|
|
1153
|
+
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
1154
|
+
assert.match(src, /document\.modelContextTesting/);
|
|
1155
|
+
assert.doesNotMatch(src, /navigator\.modelContextTesting/);
|
|
1156
|
+
assert.doesNotMatch(src, /navigator\.modelContext(?!Testing)/);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it('is wired as verify:webmcp and not in prebuild', () => {
|
|
1160
|
+
const pkg = JSON.parse(fs.readFileSync(PKG, 'utf8'));
|
|
1161
|
+
assert.equal(pkg.scripts?.['verify:webmcp'], 'node scripts/webmcp-feature-check.mjs');
|
|
1162
|
+
assert.ok(pkg.scripts?.prebuild);
|
|
1163
|
+
assert.doesNotMatch(pkg.scripts.prebuild, /webmcp-feature-check/);
|
|
1164
|
+
});
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
END_OF_FILE_CONTENT
|
|
1168
|
+
echo "Creating scripts/prebuild-wire.test.mjs..."
|
|
1169
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/prebuild-wire.test.mjs"
|
|
1170
|
+
/**
|
|
1171
|
+
* Gates for package.json prebuild wiring (Task 5 — plan 001).
|
|
1172
|
+
* Run: node --test scripts/prebuild-wire.test.mjs
|
|
1173
|
+
*/
|
|
1174
|
+
import assert from 'node:assert/strict';
|
|
1175
|
+
import fs from 'node:fs';
|
|
1176
|
+
import path from 'node:path';
|
|
1177
|
+
import { describe, it } from 'node:test';
|
|
1178
|
+
import { fileURLToPath } from 'node:url';
|
|
1179
|
+
|
|
1180
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1181
|
+
const PKG = path.resolve(__dirname, '../package.json');
|
|
1182
|
+
|
|
1183
|
+
describe('next prebuild wiring', () => {
|
|
1184
|
+
it('runs sync → llms → bake → sitemap → robots (alpha order)', () => {
|
|
1185
|
+
const pkg = JSON.parse(fs.readFileSync(PKG, 'utf8'));
|
|
1186
|
+
const prebuild = pkg.scripts?.prebuild ?? '';
|
|
1187
|
+
assert.match(prebuild, /sync-pages-to-public\.mjs/);
|
|
1188
|
+
assert.match(prebuild, /generate-llms-txt\.mjs/);
|
|
1189
|
+
assert.match(prebuild, /bake\.mjs/);
|
|
1190
|
+
assert.match(prebuild, /sitemap\.mjs/);
|
|
1191
|
+
assert.match(prebuild, /robots\.mjs/);
|
|
1192
|
+
assert.doesNotMatch(prebuild, /webmcp-feature-check/);
|
|
1193
|
+
|
|
1194
|
+
const order = ['sync-pages-to-public', 'generate-llms-txt', 'bake', 'sitemap', 'robots'].map((name) =>
|
|
1195
|
+
prebuild.indexOf(name),
|
|
1196
|
+
);
|
|
1197
|
+
for (let i = 1; i < order.length; i += 1) {
|
|
1198
|
+
assert.ok(order[i] > order[i - 1], `expected ${i} after previous in: ${prebuild}`);
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
it('dist DNA includes scripts/', () => {
|
|
1203
|
+
const pkg = JSON.parse(fs.readFileSync(PKG, 'utf8'));
|
|
1204
|
+
assert.match(pkg.scripts?.dist ?? '', /\bscripts\b/);
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
END_OF_FILE_CONTENT
|
|
1209
|
+
echo "Creating scripts/robots.mjs..."
|
|
1210
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/robots.mjs"
|
|
1211
|
+
import fs from 'fs';
|
|
1212
|
+
import path from 'path';
|
|
1213
|
+
import { fileURLToPath } from 'url';
|
|
1214
|
+
|
|
1215
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1216
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
1217
|
+
|
|
1218
|
+
const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
1219
|
+
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
1220
|
+
: 'http://localhost:3000';
|
|
1221
|
+
|
|
1222
|
+
const robotsTxt = `User-agent: *
|
|
1223
|
+
Allow: /
|
|
1224
|
+
Disallow: /api/
|
|
1225
|
+
|
|
1226
|
+
User-agent: GPTBot
|
|
1227
|
+
User-agent: ChatGPT-User
|
|
1228
|
+
User-agent: ClaudeBot
|
|
1229
|
+
User-agent: Claude-Web
|
|
1230
|
+
User-agent: PerplexityBot
|
|
1231
|
+
User-agent: OAI-SearchBot
|
|
1232
|
+
Allow: /
|
|
1233
|
+
Allow: /*.json
|
|
1234
|
+
Allow: /schemas/
|
|
1235
|
+
Allow: /llms.txt
|
|
1236
|
+
Allow: /mcp-manifest.json
|
|
1237
|
+
Disallow: /api/
|
|
1238
|
+
|
|
1239
|
+
Sitemap: ${baseUrl}/sitemap.xml
|
|
1240
|
+
`;
|
|
1241
|
+
|
|
1242
|
+
const outPath = path.join(rootDir, 'public', 'robots.txt');
|
|
1243
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1244
|
+
fs.writeFileSync(outPath, robotsTxt, 'utf-8');
|
|
1245
|
+
console.log('[robots] Written -> public/robots.txt');
|
|
1246
|
+
|
|
1247
|
+
END_OF_FILE_CONTENT
|
|
1248
|
+
echo "Creating scripts/sitemap.mjs..."
|
|
1249
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/sitemap.mjs"
|
|
1250
|
+
import fs from 'fs';
|
|
1251
|
+
import path from 'path';
|
|
1252
|
+
import { fileURLToPath } from 'url';
|
|
1253
|
+
|
|
1254
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1255
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
1256
|
+
|
|
1257
|
+
const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
1258
|
+
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
|
1259
|
+
: 'http://localhost:3000';
|
|
1260
|
+
|
|
1261
|
+
function listJsonFilesRecursive(dir) {
|
|
1262
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
1263
|
+
const files = [];
|
|
1264
|
+
for (const item of items) {
|
|
1265
|
+
const fullPath = path.join(dir, item.name);
|
|
1266
|
+
if (item.isDirectory()) {
|
|
1267
|
+
files.push(...listJsonFilesRecursive(fullPath));
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
if (item.isFile() && item.name.toLowerCase().endsWith('.json')) files.push(fullPath);
|
|
1271
|
+
}
|
|
1272
|
+
return files;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function toW3CDate(date) {
|
|
1276
|
+
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function urlEntry({ loc, lastmod, changefreq, priority, comment }) {
|
|
1280
|
+
const lines = [];
|
|
1281
|
+
if (comment) lines.push(` <!-- ${comment} -->`);
|
|
1282
|
+
lines.push(` <url>`);
|
|
1283
|
+
lines.push(` <loc>${loc}</loc>`);
|
|
1284
|
+
lines.push(` <lastmod>${lastmod}</lastmod>`);
|
|
1285
|
+
lines.push(` <changefreq>${changefreq}</changefreq>`);
|
|
1286
|
+
lines.push(` <priority>${priority}</priority>`);
|
|
1287
|
+
lines.push(` </url>`);
|
|
1288
|
+
return lines.join('\n');
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function sectionComment(label) {
|
|
1292
|
+
const bar = '='.repeat(42);
|
|
1293
|
+
return [
|
|
1294
|
+
` <!-- ${bar} -->`,
|
|
1295
|
+
` <!-- ${label.padEnd(42)} -->`,
|
|
1296
|
+
` <!-- ${bar} -->`,
|
|
1297
|
+
].join('\n');
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const pagesDir = path.join(rootDir, 'src', 'data', 'pages');
|
|
1301
|
+
const buildTime = toW3CDate(new Date());
|
|
1302
|
+
|
|
1303
|
+
const pageFiles = listJsonFilesRecursive(pagesDir);
|
|
1304
|
+
const pages = pageFiles.map((fullPath) => {
|
|
1305
|
+
const slug = path
|
|
1306
|
+
.relative(pagesDir, fullPath)
|
|
1307
|
+
.replace(/\\/g, '/')
|
|
1308
|
+
.replace(/\.json$/i, '');
|
|
1309
|
+
const lastmod = toW3CDate(fs.statSync(fullPath).mtime);
|
|
1310
|
+
return { slug, lastmod };
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
const entries = [];
|
|
1314
|
+
|
|
1315
|
+
entries.push(sectionComment('GLOBAL AGENT DISCOVERY NODES'));
|
|
1316
|
+
entries.push(
|
|
1317
|
+
urlEntry({ loc: `${baseUrl}/llms.txt`, lastmod: buildTime, changefreq: 'weekly', priority: '1.0' }),
|
|
1318
|
+
);
|
|
1319
|
+
entries.push(
|
|
1320
|
+
urlEntry({ loc: `${baseUrl}/mcp-manifest.json`, lastmod: buildTime, changefreq: 'weekly', priority: '1.0' }),
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
for (const { slug, lastmod } of pages) {
|
|
1324
|
+
const humanPath = slug === 'home' ? '/' : `/${slug}`;
|
|
1325
|
+
const label = `PAGE: ${slug.toUpperCase()}`;
|
|
1326
|
+
|
|
1327
|
+
entries.push(sectionComment(label));
|
|
1328
|
+
entries.push(
|
|
1329
|
+
urlEntry({ loc: `${baseUrl}${humanPath}`, lastmod, changefreq: 'daily', priority: '0.9', comment: 'Human UI' }),
|
|
1330
|
+
);
|
|
1331
|
+
entries.push(
|
|
1332
|
+
urlEntry({ loc: `${baseUrl}/${slug}.json`, lastmod, changefreq: 'daily', priority: '0.9', comment: 'Machine Payload' }),
|
|
1333
|
+
);
|
|
1334
|
+
entries.push(
|
|
1335
|
+
urlEntry({
|
|
1336
|
+
loc: `${baseUrl}/schemas/${slug}.schema.json`,
|
|
1337
|
+
lastmod: buildTime,
|
|
1338
|
+
changefreq: 'weekly',
|
|
1339
|
+
priority: '0.8',
|
|
1340
|
+
comment: 'Machine Contract (Schema)',
|
|
1341
|
+
}),
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const xml = [
|
|
1346
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
1347
|
+
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`,
|
|
1348
|
+
``,
|
|
1349
|
+
entries.join('\n'),
|
|
1350
|
+
``,
|
|
1351
|
+
`</urlset>`,
|
|
1352
|
+
].join('\n');
|
|
1353
|
+
|
|
1354
|
+
const outPath = path.join(rootDir, 'public', 'sitemap.xml');
|
|
1355
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1356
|
+
fs.writeFileSync(outPath, xml, 'utf-8');
|
|
1357
|
+
console.log('[sitemap] Written -> public/sitemap.xml');
|
|
1358
|
+
|
|
676
1359
|
END_OF_FILE_CONTENT
|
|
677
1360
|
echo "Creating scripts/sync-pages-to-public.mjs..."
|
|
678
1361
|
cat << 'END_OF_FILE_CONTENT' > "scripts/sync-pages-to-public.mjs"
|
|
@@ -723,6 +1406,308 @@ if (fs.existsSync(sourceSiteConfigPath)) {
|
|
|
723
1406
|
|
|
724
1407
|
console.log('[sync-pages-to-public] Synced pages, collections, and site config to public/');
|
|
725
1408
|
|
|
1409
|
+
END_OF_FILE_CONTENT
|
|
1410
|
+
echo "Creating scripts/webmcp-feature-check.mjs..."
|
|
1411
|
+
cat << 'END_OF_FILE_CONTENT' > "scripts/webmcp-feature-check.mjs"
|
|
1412
|
+
import fs from 'fs/promises';
|
|
1413
|
+
import path from 'path';
|
|
1414
|
+
import { fileURLToPath } from 'url';
|
|
1415
|
+
import { createRequire } from 'module';
|
|
1416
|
+
|
|
1417
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1418
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
1419
|
+
const baseUrl = process.env.WEBMCP_BASE_URL ?? 'http://127.0.0.1:3000';
|
|
1420
|
+
|
|
1421
|
+
function pageFilePathFromSlug(slug) {
|
|
1422
|
+
return path.resolve(rootDir, 'src', 'data', 'pages', `${slug}.json`);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function adminUrlFromSlug(slug) {
|
|
1426
|
+
return `${baseUrl}/admin${slug === 'home' ? '' : `/${slug}`}`;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function isStringSchema(schema) {
|
|
1430
|
+
if (!schema || typeof schema !== 'object') return false;
|
|
1431
|
+
if (schema.type === 'string') return true;
|
|
1432
|
+
if (Array.isArray(schema.anyOf)) {
|
|
1433
|
+
return schema.anyOf.some((entry) => entry && typeof entry === 'object' && entry.type === 'string');
|
|
1434
|
+
}
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function findTopLevelStringField(sectionSchema) {
|
|
1439
|
+
const properties = sectionSchema?.properties;
|
|
1440
|
+
if (!properties || typeof properties !== 'object') return null;
|
|
1441
|
+
const preferred = ['title', 'sectionTitle', 'label', 'headline', 'name'];
|
|
1442
|
+
for (const key of preferred) {
|
|
1443
|
+
if (isStringSchema(properties[key])) return key;
|
|
1444
|
+
}
|
|
1445
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
1446
|
+
if (isStringSchema(value)) return key;
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async function loadPlaywright() {
|
|
1452
|
+
const require = createRequire(import.meta.url);
|
|
1453
|
+
try {
|
|
1454
|
+
return require('playwright');
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1457
|
+
throw new Error(
|
|
1458
|
+
`Playwright is required for WebMCP verification. Install it before running this script. Original error: ${message}`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
async function readPageJson(slug) {
|
|
1464
|
+
const pageFilePath = pageFilePathFromSlug(slug);
|
|
1465
|
+
const raw = await fs.readFile(pageFilePath, 'utf8');
|
|
1466
|
+
return { raw, json: JSON.parse(raw), pageFilePath };
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
async function waitFor(predicate, timeoutMs, label) {
|
|
1470
|
+
const startedAt = Date.now();
|
|
1471
|
+
for (;;) {
|
|
1472
|
+
const result = await predicate();
|
|
1473
|
+
if (result) return result;
|
|
1474
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
1475
|
+
throw new Error(`Timed out while waiting for ${label}.`);
|
|
1476
|
+
}
|
|
1477
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
async function waitForFileFieldValue(slug, sectionId, fieldKey, expectedValue) {
|
|
1482
|
+
await waitFor(async () => {
|
|
1483
|
+
const { json } = await readPageJson(slug);
|
|
1484
|
+
const section = Array.isArray(json.sections)
|
|
1485
|
+
? json.sections.find((item) => item?.id === sectionId)
|
|
1486
|
+
: null;
|
|
1487
|
+
return section?.data?.[fieldKey] === expectedValue;
|
|
1488
|
+
}, 8_000, `file field "${fieldKey}" = "${expectedValue}"`);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
async function ensureResponseOk(response, label) {
|
|
1492
|
+
if (!response.ok) {
|
|
1493
|
+
const text = await response.text();
|
|
1494
|
+
throw new Error(`${label} failed with ${response.status}: ${text}`);
|
|
1495
|
+
}
|
|
1496
|
+
return response;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
async function fetchJson(relativePath, label) {
|
|
1500
|
+
const response = await ensureResponseOk(await fetch(`${baseUrl}${relativePath}`), label);
|
|
1501
|
+
return response.json();
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async function selectTarget() {
|
|
1505
|
+
const siteIndex = await fetchJson('/mcp-manifest.json', 'Manifest index request');
|
|
1506
|
+
const requestedSlug = typeof process.env.WEBMCP_TARGET_SLUG === 'string' && process.env.WEBMCP_TARGET_SLUG.trim()
|
|
1507
|
+
? process.env.WEBMCP_TARGET_SLUG.trim()
|
|
1508
|
+
: null;
|
|
1509
|
+
|
|
1510
|
+
const candidatePages = requestedSlug
|
|
1511
|
+
? (siteIndex.pages ?? []).filter((page) => page?.slug === requestedSlug)
|
|
1512
|
+
: (siteIndex.pages ?? []);
|
|
1513
|
+
|
|
1514
|
+
for (const pageEntry of candidatePages) {
|
|
1515
|
+
if (!pageEntry?.slug || !pageEntry?.manifestHref || !pageEntry?.contractHref) continue;
|
|
1516
|
+
const pageManifest = await fetchJson(pageEntry.manifestHref, `Page manifest request for ${pageEntry.slug}`);
|
|
1517
|
+
const pageContract = await fetchJson(pageEntry.contractHref, `Page contract request for ${pageEntry.slug}`);
|
|
1518
|
+
const localInstances = Array.isArray(pageContract.sectionInstances)
|
|
1519
|
+
? pageContract.sectionInstances.filter((section) => section?.scope === 'local')
|
|
1520
|
+
: [];
|
|
1521
|
+
const tools = Array.isArray(pageManifest.tools) ? pageManifest.tools : [];
|
|
1522
|
+
|
|
1523
|
+
for (const tool of tools) {
|
|
1524
|
+
const sectionType = tool?.sectionType;
|
|
1525
|
+
if (typeof tool?.name !== 'string' || typeof sectionType !== 'string') continue;
|
|
1526
|
+
const targetInstance = localInstances.find((section) => section?.type === sectionType);
|
|
1527
|
+
if (!targetInstance?.id) continue;
|
|
1528
|
+
const targetFieldKey = findTopLevelStringField(pageContract.sectionSchemas?.[sectionType]);
|
|
1529
|
+
if (!targetFieldKey) continue;
|
|
1530
|
+
const pageState = await readPageJson(pageEntry.slug);
|
|
1531
|
+
const section = Array.isArray(pageState.json.sections)
|
|
1532
|
+
? pageState.json.sections.find((item) => item?.id === targetInstance.id)
|
|
1533
|
+
: null;
|
|
1534
|
+
const originalValue = section?.data?.[targetFieldKey];
|
|
1535
|
+
if (typeof originalValue !== 'string') continue;
|
|
1536
|
+
|
|
1537
|
+
return {
|
|
1538
|
+
slug: pageEntry.slug,
|
|
1539
|
+
manifestHref: pageEntry.manifestHref,
|
|
1540
|
+
contractHref: pageEntry.contractHref,
|
|
1541
|
+
toolName: tool.name,
|
|
1542
|
+
sectionId: targetInstance.id,
|
|
1543
|
+
fieldKey: targetFieldKey,
|
|
1544
|
+
originalValue,
|
|
1545
|
+
originalState: pageState,
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
throw new Error(
|
|
1551
|
+
requestedSlug
|
|
1552
|
+
? `No valid WebMCP verification target found for page "${requestedSlug}".`
|
|
1553
|
+
: 'No valid WebMCP verification target found in manifest index.'
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
async function main() {
|
|
1558
|
+
const { chromium } = await loadPlaywright();
|
|
1559
|
+
const target = await selectTarget();
|
|
1560
|
+
const nextValue = `${target.originalValue} WebMCP ${Date.now()}`;
|
|
1561
|
+
const browser = await chromium.launch({ headless: true });
|
|
1562
|
+
const page = await browser.newPage();
|
|
1563
|
+
const consoleEvents = [];
|
|
1564
|
+
let mutationApplied = false;
|
|
1565
|
+
|
|
1566
|
+
page.on('console', (message) => {
|
|
1567
|
+
if (message.type() === 'error' || message.type() === 'warning') {
|
|
1568
|
+
consoleEvents.push(`[console:${message.type()}] ${message.text()}`);
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
page.on('pageerror', (error) => {
|
|
1572
|
+
consoleEvents.push(`[pageerror] ${error.message}`);
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
const restoreOriginal = async () => {
|
|
1576
|
+
try {
|
|
1577
|
+
await page.evaluate(
|
|
1578
|
+
async ({ toolName, slug, sectionId, fieldKey, value }) => {
|
|
1579
|
+
const runtime = document.modelContextTesting;
|
|
1580
|
+
if (!runtime?.executeTool) return;
|
|
1581
|
+
await runtime.executeTool(
|
|
1582
|
+
toolName,
|
|
1583
|
+
JSON.stringify({
|
|
1584
|
+
slug,
|
|
1585
|
+
sectionId,
|
|
1586
|
+
fieldKey,
|
|
1587
|
+
value,
|
|
1588
|
+
})
|
|
1589
|
+
);
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
toolName: target.toolName,
|
|
1593
|
+
slug: target.slug,
|
|
1594
|
+
sectionId: target.sectionId,
|
|
1595
|
+
fieldKey: target.fieldKey,
|
|
1596
|
+
value: target.originalValue,
|
|
1597
|
+
}
|
|
1598
|
+
);
|
|
1599
|
+
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, target.originalValue);
|
|
1600
|
+
} catch {
|
|
1601
|
+
await fs.writeFile(target.originalState.pageFilePath, target.originalState.raw, 'utf8');
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
try {
|
|
1606
|
+
const pageManifest = await fetchJson(target.manifestHref, `Manifest request for ${target.slug}`);
|
|
1607
|
+
if (!Array.isArray(pageManifest.tools) || !pageManifest.tools.some((tool) => tool?.name === target.toolName)) {
|
|
1608
|
+
throw new Error(`Manifest does not expose ${target.toolName}.`);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const pageContract = await fetchJson(target.contractHref, `Page contract request for ${target.slug}`);
|
|
1612
|
+
if (!Array.isArray(pageContract.tools) || !pageContract.tools.some((tool) => tool?.name === target.toolName)) {
|
|
1613
|
+
throw new Error(`Page contract does not expose ${target.toolName}.`);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
await page.goto(adminUrlFromSlug(target.slug), { waitUntil: 'networkidle' });
|
|
1617
|
+
|
|
1618
|
+
try {
|
|
1619
|
+
await page.waitForFunction(
|
|
1620
|
+
({ manifestHref, contractHref }) => {
|
|
1621
|
+
const manifestLink = document.head.querySelector('link[rel="mcp-manifest"]');
|
|
1622
|
+
const contractLink = document.head.querySelector('link[rel="olon-contract"]');
|
|
1623
|
+
return manifestLink?.getAttribute('href') === manifestHref
|
|
1624
|
+
&& contractLink?.getAttribute('href') === contractHref;
|
|
1625
|
+
},
|
|
1626
|
+
{ manifestHref: target.manifestHref, contractHref: target.contractHref },
|
|
1627
|
+
{ timeout: 10_000 }
|
|
1628
|
+
);
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
const diagnostics = await page.evaluate(() => ({
|
|
1631
|
+
head: document.head.innerHTML,
|
|
1632
|
+
bodyText: document.body.innerText,
|
|
1633
|
+
}));
|
|
1634
|
+
throw new Error(
|
|
1635
|
+
[
|
|
1636
|
+
error instanceof Error ? error.message : String(error),
|
|
1637
|
+
`head=${diagnostics.head}`,
|
|
1638
|
+
`body=${diagnostics.bodyText}`,
|
|
1639
|
+
...consoleEvents,
|
|
1640
|
+
].join('\n')
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
const toolNames = await page.evaluate(() => {
|
|
1645
|
+
const runtime = document.modelContextTesting;
|
|
1646
|
+
return runtime?.listTools?.().map((tool) => tool.name) ?? [];
|
|
1647
|
+
});
|
|
1648
|
+
if (!toolNames.includes(target.toolName)) {
|
|
1649
|
+
throw new Error(`Runtime did not register ${target.toolName}. Found: ${toolNames.join(', ')}`);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const rawResult = await page.evaluate(
|
|
1653
|
+
async ({ toolName, slug, sectionId, fieldKey, value }) => {
|
|
1654
|
+
const runtime = document.modelContextTesting;
|
|
1655
|
+
if (!runtime?.executeTool) {
|
|
1656
|
+
throw new Error('document.modelContextTesting.executeTool is unavailable.');
|
|
1657
|
+
}
|
|
1658
|
+
return runtime.executeTool(
|
|
1659
|
+
toolName,
|
|
1660
|
+
JSON.stringify({
|
|
1661
|
+
slug,
|
|
1662
|
+
sectionId,
|
|
1663
|
+
fieldKey,
|
|
1664
|
+
value,
|
|
1665
|
+
})
|
|
1666
|
+
);
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
toolName: target.toolName,
|
|
1670
|
+
slug: target.slug,
|
|
1671
|
+
sectionId: target.sectionId,
|
|
1672
|
+
fieldKey: target.fieldKey,
|
|
1673
|
+
value: nextValue,
|
|
1674
|
+
}
|
|
1675
|
+
);
|
|
1676
|
+
|
|
1677
|
+
const parsedResult = JSON.parse(rawResult);
|
|
1678
|
+
if (parsedResult?.isError) {
|
|
1679
|
+
throw new Error(`WebMCP tool returned an error: ${rawResult}`);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
mutationApplied = true;
|
|
1683
|
+
await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, nextValue);
|
|
1684
|
+
await page.frameLocator('iframe').getByText(nextValue, { exact: true }).waitFor({ state: 'attached' });
|
|
1685
|
+
|
|
1686
|
+
console.log(
|
|
1687
|
+
JSON.stringify({
|
|
1688
|
+
ok: true,
|
|
1689
|
+
slug: target.slug,
|
|
1690
|
+
manifestHref: target.manifestHref,
|
|
1691
|
+
contractHref: target.contractHref,
|
|
1692
|
+
toolName: target.toolName,
|
|
1693
|
+
sectionId: target.sectionId,
|
|
1694
|
+
fieldKey: target.fieldKey,
|
|
1695
|
+
toolNames,
|
|
1696
|
+
})
|
|
1697
|
+
);
|
|
1698
|
+
} finally {
|
|
1699
|
+
if (mutationApplied) {
|
|
1700
|
+
await restoreOriginal();
|
|
1701
|
+
}
|
|
1702
|
+
await browser.close();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
main().catch((error) => {
|
|
1707
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
1708
|
+
process.exit(1);
|
|
1709
|
+
});
|
|
1710
|
+
|
|
726
1711
|
END_OF_FILE_CONTENT
|
|
727
1712
|
mkdir -p "src"
|
|
728
1713
|
# SKIP: src/App.tsx is binary and cannot be embedded as text.
|
|
@@ -3421,6 +4406,74 @@ export function useCloudSave({ apiUrl, apiKey }: UseCloudSaveOptions) {
|
|
|
3421
4406
|
return { cloudSaveUi, runCloudSave, closeCloudDrawer, retryCloudSave };
|
|
3422
4407
|
}
|
|
3423
4408
|
|
|
4409
|
+
END_OF_FILE_CONTENT
|
|
4410
|
+
echo "Creating src/lib/buildVisitorWebPageJsonLd.test.ts..."
|
|
4411
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/buildVisitorWebPageJsonLd.test.ts"
|
|
4412
|
+
import { describe, expect, it } from 'vitest';
|
|
4413
|
+
import { buildVisitorWebPageJsonLd } from './buildVisitorWebPageJsonLd';
|
|
4414
|
+
|
|
4415
|
+
describe('buildVisitorWebPageJsonLd', () => {
|
|
4416
|
+
it('builds Schema.org WebPage for home at /', () => {
|
|
4417
|
+
expect(
|
|
4418
|
+
buildVisitorWebPageJsonLd({
|
|
4419
|
+
title: 'Home',
|
|
4420
|
+
description: 'Welcome',
|
|
4421
|
+
slug: 'home',
|
|
4422
|
+
}),
|
|
4423
|
+
).toEqual({
|
|
4424
|
+
'@context': 'https://schema.org',
|
|
4425
|
+
'@type': 'WebPage',
|
|
4426
|
+
name: 'Home',
|
|
4427
|
+
description: 'Welcome',
|
|
4428
|
+
url: '/',
|
|
4429
|
+
});
|
|
4430
|
+
});
|
|
4431
|
+
|
|
4432
|
+
it('builds Schema.org WebPage for nested slug paths', () => {
|
|
4433
|
+
expect(
|
|
4434
|
+
buildVisitorWebPageJsonLd({
|
|
4435
|
+
title: 'Dune',
|
|
4436
|
+
description: 'Book',
|
|
4437
|
+
slug: 'libri/dune',
|
|
4438
|
+
}),
|
|
4439
|
+
).toEqual({
|
|
4440
|
+
'@context': 'https://schema.org',
|
|
4441
|
+
'@type': 'WebPage',
|
|
4442
|
+
name: 'Dune',
|
|
4443
|
+
description: 'Book',
|
|
4444
|
+
url: '/libri/dune',
|
|
4445
|
+
});
|
|
4446
|
+
});
|
|
4447
|
+
});
|
|
4448
|
+
|
|
4449
|
+
END_OF_FILE_CONTENT
|
|
4450
|
+
echo "Creating src/lib/buildVisitorWebPageJsonLd.ts..."
|
|
4451
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/buildVisitorWebPageJsonLd.ts"
|
|
4452
|
+
/**
|
|
4453
|
+
* Schema.org WebPage JSON-LD for Next visitor RSC (parity with tenant-alpha bake HTML injection).
|
|
4454
|
+
*/
|
|
4455
|
+
export type VisitorWebPageJsonLd = {
|
|
4456
|
+
'@context': 'https://schema.org';
|
|
4457
|
+
'@type': 'WebPage';
|
|
4458
|
+
name: string;
|
|
4459
|
+
description: string;
|
|
4460
|
+
url: string;
|
|
4461
|
+
};
|
|
4462
|
+
|
|
4463
|
+
export function buildVisitorWebPageJsonLd(input: {
|
|
4464
|
+
title: string;
|
|
4465
|
+
description?: string;
|
|
4466
|
+
slug: string;
|
|
4467
|
+
}): VisitorWebPageJsonLd {
|
|
4468
|
+
return {
|
|
4469
|
+
'@context': 'https://schema.org',
|
|
4470
|
+
'@type': 'WebPage',
|
|
4471
|
+
name: input.title,
|
|
4472
|
+
description: input.description ?? '',
|
|
4473
|
+
url: input.slug === 'home' ? '/' : `/${input.slug}`,
|
|
4474
|
+
};
|
|
4475
|
+
}
|
|
4476
|
+
|
|
3424
4477
|
END_OF_FILE_CONTENT
|
|
3425
4478
|
mkdir -p "src/lib/css"
|
|
3426
4479
|
echo "Creating src/lib/css/serializeThemeRootCss.test.ts..."
|
|
@@ -4465,38 +5518,26 @@ else
|
|
|
4465
5518
|
fi
|
|
4466
5519
|
|
|
4467
5520
|
# -----------------------------------------------------------------------------
|
|
4468
|
-
#
|
|
4469
|
-
#
|
|
5521
|
+
# WIPE tenant content — no DNA name denylist (orphans break the compiler).
|
|
5522
|
+
# Preserve: src/components/ui (shadcn), src/components/admin (studio).
|
|
5523
|
+
# Wipe includes overlap dirs (e.g. header) — generators rewrite them fresh.
|
|
4470
5524
|
# -----------------------------------------------------------------------------
|
|
4471
|
-
echo "--
|
|
5525
|
+
echo "-- Wiping tenant content surfaces (components/collections/pages/config)..."
|
|
5526
|
+
if [[ -d src/components ]]; then
|
|
5527
|
+
find src/components -mindepth 1 -maxdepth 1 ! -name 'ui' ! -name 'admin' -exec rm -rf {} +
|
|
5528
|
+
fi
|
|
4472
5529
|
rm -rf \
|
|
4473
|
-
src/
|
|
4474
|
-
src/
|
|
4475
|
-
src/
|
|
4476
|
-
src/components/form-demo \
|
|
4477
|
-
src/components/empty-tenant \
|
|
4478
|
-
src/collections/autori \
|
|
4479
|
-
src/collections/libri \
|
|
4480
|
-
src/data/collections/autori \
|
|
4481
|
-
src/data/collections/libri \
|
|
4482
|
-
src/data/pages/authors \
|
|
4483
|
-
src/data/pages/libri \
|
|
4484
|
-
src/data/pages/work \
|
|
4485
|
-
src/data/pages/blog \
|
|
5530
|
+
src/collections \
|
|
5531
|
+
src/data/collections \
|
|
5532
|
+
src/data/pages \
|
|
4486
5533
|
public/pages \
|
|
4487
5534
|
public/collections
|
|
4488
|
-
rm -f
|
|
4489
|
-
|
|
4490
|
-
src/data/
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
src/data/pages/work.json \
|
|
4495
|
-
src/data/pages/blog.json \
|
|
4496
|
-
src/data/pages/contact.json \
|
|
4497
|
-
public/config/site.json
|
|
4498
|
-
|
|
4499
|
-
# Drop books-list special-case so deleted capsules cannot break the visitor RSC path.
|
|
5535
|
+
rm -f public/config/site.json
|
|
5536
|
+
if [[ -d src/data/config ]]; then
|
|
5537
|
+
find src/data/config -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
5538
|
+
fi
|
|
5539
|
+
|
|
5540
|
+
# Drop DNA special-cases so wiped capsules cannot break the visitor RSC path.
|
|
4500
5541
|
echo "-- Resetting VisitorSection to registry-only..."
|
|
4501
5542
|
mkdir -p src/lib
|
|
4502
5543
|
cat > src/lib/VisitorSection.tsx << 'EOF'
|
|
@@ -7157,31 +8198,26 @@ else
|
|
|
7157
8198
|
fi
|
|
7158
8199
|
|
|
7159
8200
|
# -----------------------------------------------------------------------------
|
|
7160
|
-
#
|
|
7161
|
-
#
|
|
8201
|
+
# WIPE tenant content — no DNA name denylist (orphans break the compiler).
|
|
8202
|
+
# Preserve: src/components/ui (shadcn), src/components/admin (studio).
|
|
8203
|
+
# Wipe includes overlap dirs (e.g. header) — generators rewrite them fresh.
|
|
7162
8204
|
# -----------------------------------------------------------------------------
|
|
7163
|
-
echo "--
|
|
8205
|
+
echo "-- Wiping tenant content surfaces (components/collections/pages/config)..."
|
|
8206
|
+
if [[ -d src/components ]]; then
|
|
8207
|
+
find src/components -mindepth 1 -maxdepth 1 ! -name 'ui' ! -name 'admin' -exec rm -rf {} +
|
|
8208
|
+
fi
|
|
7164
8209
|
rm -rf \
|
|
7165
|
-
src/
|
|
7166
|
-
src/
|
|
7167
|
-
src/
|
|
7168
|
-
src/components/form-demo \
|
|
7169
|
-
src/collections/autori \
|
|
7170
|
-
src/collections/libri \
|
|
7171
|
-
src/data/collections/autori \
|
|
7172
|
-
src/data/collections/libri \
|
|
7173
|
-
src/data/pages/authors \
|
|
7174
|
-
src/data/pages/libri \
|
|
8210
|
+
src/collections \
|
|
8211
|
+
src/data/collections \
|
|
8212
|
+
src/data/pages \
|
|
7175
8213
|
public/pages \
|
|
7176
8214
|
public/collections
|
|
7177
|
-
rm -f
|
|
7178
|
-
|
|
7179
|
-
src/data/
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
# Drop books-list special-case so deleted capsules cannot break the visitor RSC path.
|
|
8215
|
+
rm -f public/config/site.json
|
|
8216
|
+
if [[ -d src/data/config ]]; then
|
|
8217
|
+
find src/data/config -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
8218
|
+
fi
|
|
8219
|
+
|
|
8220
|
+
# Drop DNA special-cases so wiped capsules cannot break the visitor RSC path.
|
|
7185
8221
|
echo "-- Resetting VisitorSection to registry-only..."
|
|
7186
8222
|
mkdir -p src/lib
|
|
7187
8223
|
cat > src/lib/VisitorSection.tsx << 'EOF'
|
|
@@ -7217,6 +8253,31 @@ export function VisitorSection({
|
|
|
7217
8253
|
}
|
|
7218
8254
|
EOF
|
|
7219
8255
|
|
|
8256
|
+
# DNA catch-all imports EmptyTenantView — drop it after wipe of that capsule.
|
|
8257
|
+
if [[ -f 'app/[[...slug]]/page.tsx' ]]; then
|
|
8258
|
+
echo "-- Patching app/[[...slug]]/page.tsx empty fallback..."
|
|
8259
|
+
python3 - <<'PY'
|
|
8260
|
+
from pathlib import Path
|
|
8261
|
+
p = Path("app/[[...slug]]/page.tsx")
|
|
8262
|
+
src = p.read_text()
|
|
8263
|
+
capsule = "empty" + "-tenant"
|
|
8264
|
+
src = src.replace(f"import {{ EmptyTenantView }} from '@/components/{capsule}';\n", "")
|
|
8265
|
+
old = " if (result.kind === 'empty') {\n return <EmptyTenantView />;\n }"
|
|
8266
|
+
new = """ if (result.kind === 'empty') {
|
|
8267
|
+
return (
|
|
8268
|
+
<main className="mx-auto max-w-3xl px-8 py-24">
|
|
8269
|
+
<h1 className="text-2xl font-bold">Your tenant is empty.</h1>
|
|
8270
|
+
<p className="mt-2 text-muted-foreground">Create your first page.</p>
|
|
8271
|
+
</main>
|
|
8272
|
+
);
|
|
8273
|
+
}"""
|
|
8274
|
+
if old not in src:
|
|
8275
|
+
raise SystemExit("EmptyTenantView empty-branch not found in page.tsx")
|
|
8276
|
+
p.write_text(src.replace(old, new))
|
|
8277
|
+
print(" page.tsx empty fallback inlined")
|
|
8278
|
+
PY
|
|
8279
|
+
fi
|
|
8280
|
+
|
|
7220
8281
|
# -----------------------------------------------------------------------------
|
|
7221
8282
|
# DIRECTORIES
|
|
7222
8283
|
# -----------------------------------------------------------------------------
|