From 048cba3ee748fa698a8715af6d6a2957e711558e Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Mon, 15 Jun 2026 16:49:54 +1000 Subject: [PATCH] Share one manifest contract between Standard.site writer and reader Export StandardSiteManifestSchema, the StandardSiteManifest type, and a new serializeStandardSiteManifest from src/lib/standard-site.ts so the sync script serialises through the same schema it validates on read. The script drops its duplicate PUBLICATION_URI constant and inline manifest builder in favour of standardSitePublicationUri and the shared serializer. --- apps/web/scripts/sync-standard-site.ts | 24 +++++------ apps/web/src/lib/standard-site.test.ts | 55 ++++++++++++++++++++++++++ apps/web/src/lib/standard-site.ts | 16 +++++++- 3 files changed, 78 insertions(+), 17 deletions(-) diff --git a/apps/web/scripts/sync-standard-site.ts b/apps/web/scripts/sync-standard-site.ts index 751fc19..8754007 100644 --- a/apps/web/scripts/sync-standard-site.ts +++ b/apps/web/scripts/sync-standard-site.ts @@ -3,6 +3,11 @@ import { basename } from 'node:path'; import { pathToFileURL } from 'node:url'; import * as z from 'zod'; +import { + serializeStandardSiteManifest, + standardSitePublicationUri, +} from '../src/lib/standard-site.ts'; + type PublishedPost = { portableContent: string; publishedAt: string; @@ -22,7 +27,7 @@ type DocumentRecord = { description: string; path: `/posts/${string}`; publishedAt: string; - site: typeof PUBLICATION_URI; + site: typeof standardSitePublicationUri; textContent: string; title: string; }; @@ -70,8 +75,6 @@ const POSTS_DIR = new URL('content/posts/', APP_DIR); const GENERATED_DIR = new URL('src/generated/', APP_DIR); const MANIFEST_URL = new URL('src/generated/standard-site.json', APP_DIR); const COLLECTION = 'site.standard.document'; -const PUBLICATION_URI = - 'at://did:plc:3z5ja7l2rhnmtr2bni5dyfe7/site.standard.publication/3mnqwgvxn372f'; const DEFAULT_IDENTIFIER = 'lukebennett.dev'; export function extractPortableContent( @@ -184,7 +187,7 @@ export function planReconciliation({ for (const record of existingRecords) { const value = record.value ?? {}; if ( - value.site !== PUBLICATION_URI || + value.site !== standardSitePublicationUri || typeof value.path !== 'string' || !value.path.startsWith('/posts/') ) { @@ -253,7 +256,7 @@ export function buildDocumentRecord(post: PublishedPost): DocumentRecord { description: truncateDescription(textContent), path: `/posts/${post.slug}`, publishedAt: new Date(`${post.publishedAt}T00:00:00.000Z`).toISOString(), - site: PUBLICATION_URI, + site: standardSitePublicationUri, textContent, title: post.title, }; @@ -269,16 +272,7 @@ async function writeManifest( await mkdir(GENERATED_DIR, { recursive: true }); await writeFile( MANIFEST_URL, - `${JSON.stringify( - { - documentsBySlug: Object.fromEntries( - [...documentsBySlug.entries()].sort(([a], [b]) => a.localeCompare(b)), - ), - publicationUri: PUBLICATION_URI, - }, - null, - 2, - )}\n`, + serializeStandardSiteManifest(documentsBySlug), 'utf8', ); } diff --git a/apps/web/src/lib/standard-site.test.ts b/apps/web/src/lib/standard-site.test.ts index a08ba9d..f89b1b2 100644 --- a/apps/web/src/lib/standard-site.test.ts +++ b/apps/web/src/lib/standard-site.test.ts @@ -7,6 +7,7 @@ import test from 'node:test'; import { getStandardSiteDocumentUri, loadStandardSiteManifest, + serializeStandardSiteManifest, standardSitePublicationUri, } from './standard-site.ts'; @@ -92,3 +93,57 @@ test('reads document URIs from valid manifest', async () => { await rm(dir, { force: true, recursive: true }); } }); + +test('serializeStandardSiteManifest round-trips through loadStandardSiteManifest', async () => { + const dir = await mkdtemp(join(tmpdir(), 'standard-site-')); + const manifestPath = join(dir, 'standard-site.json'); + const documentsBySlug = new Map([ + [ + 'hello-world', + 'at://did:plc:3z5ja7l2rhnmtr2bni5dyfe7/site.standard.document/hello-world', + ], + [ + 'another-post', + 'at://did:plc:3z5ja7l2rhnmtr2bni5dyfe7/site.standard.document/another-post', + ], + ]); + + try { + await writeFile( + manifestPath, + serializeStandardSiteManifest(documentsBySlug), + 'utf8', + ); + + const manifest = loadStandardSiteManifest({ + manifestPath, + requireManifest: false, + }); + + assert.deepEqual( + manifest.documentsBySlug, + Object.fromEntries(documentsBySlug), + ); + assert.equal(manifest.publicationUri, standardSitePublicationUri); + } finally { + await rm(dir, { force: true, recursive: true }); + } +}); + +test('serializeStandardSiteManifest sorts slugs and ends with a trailing newline', () => { + const serialized = serializeStandardSiteManifest( + new Map([ + [ + 'zebra', + 'at://did:plc:3z5ja7l2rhnmtr2bni5dyfe7/site.standard.document/zebra', + ], + [ + 'apple', + 'at://did:plc:3z5ja7l2rhnmtr2bni5dyfe7/site.standard.document/apple', + ], + ]), + ); + + assert.ok(serialized.endsWith('\n')); + assert.ok(serialized.indexOf('"apple"') < serialized.indexOf('"zebra"')); +}); diff --git a/apps/web/src/lib/standard-site.ts b/apps/web/src/lib/standard-site.ts index 80e885a..873e24f 100644 --- a/apps/web/src/lib/standard-site.ts +++ b/apps/web/src/lib/standard-site.ts @@ -5,14 +5,26 @@ import * as z from 'zod'; export const standardSitePublicationUri = 'at://did:plc:3z5ja7l2rhnmtr2bni5dyfe7/site.standard.publication/3mnqwgvxn372f'; -const StandardSiteManifestSchema = z +export const StandardSiteManifestSchema = z .object({ documentsBySlug: z.record(z.string(), z.string().startsWith('at://')), publicationUri: z.literal(standardSitePublicationUri), }) .readonly(); -type StandardSiteManifest = z.infer; +export type StandardSiteManifest = z.infer; + +export function serializeStandardSiteManifest( + documentsBySlug: Map, +): string { + const manifest = StandardSiteManifestSchema.parse({ + documentsBySlug: Object.fromEntries( + [...documentsBySlug.entries()].sort(([a], [b]) => a.localeCompare(b)), + ), + publicationUri: standardSitePublicationUri, + }); + return `${JSON.stringify(manifest, null, 2)}\n`; +} type ManifestOptions = { manifestPath?: string;