From c52f807963fe8deb54874471140422caffa2de7f Mon Sep 17 00:00:00 2001 From: voidptr <1711810+taraxvoid@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:09:49 -0500 Subject: [PATCH] update jsonLD def for my gender stuff and add check for the issue that broke it --- package.json | 1 + src/components/HeadMeta.astro | 12 ++-- test/structured-data.test.ts | 100 ++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 test/structured-data.test.ts diff --git a/package.json b/package.json index 409d5e5..3004e42 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test:data": "vitest run test/data.test.ts", "test:ical": "vitest run test/ical.test.ts", "test:build": "bun run build && vitest run test/build.test.ts", + "test:structured-data": "bun run build && vitest run test/structured-data.test.ts", "test:e2e": "bun run build && playwright test --project=mobile-chrome", "test:e2e:only": "playwright test --project=mobile-chrome", "test:e2e:desktop": "bun run build && playwright test --project=desktop-chrome", diff --git a/src/components/HeadMeta.astro b/src/components/HeadMeta.astro index 2bee659..ec6f46e 100644 --- a/src/components/HeadMeta.astro +++ b/src/components/HeadMeta.astro @@ -51,12 +51,12 @@ const { title, description, canonicalUrl, refresh } = Astro.props url: Astro.site?.href ?? canonicalUrl, description: "Community-maintained directory of queer groups, spaces, and resources in Omaha, Nebraska.", "administrator": { - "@type": "Person", - "name": "Tara X Void", - "gender": "https://en.pronouns.page/are/xe&she", - "description": "Initial Creator of queeromaha.net", - "sameAs": "https://github.com/taraxvoid" - } + "@type": "Person", + "name": "Tara X Void", + "gender": "https://en.pronouns.page/are/xe%26she", + "description": "Initial Creator of queeromaha.net", + "sameAs": "https://github.com/taraxvoid" + } })}> including inline Astro output. +const LDJSON_RE = + /]*type=['"]application\/ld\+json['"][^>]*>([\s\S]*?)<\/script>/gi + +function* ldJsonBlocks( + html: string, +): Generator<{ raw: string; data: unknown }> { + // Reset index + LDJSON_RE.lastIndex = 0 + for (let m = LDJSON_RE.exec(html); m !== null; m = LDJSON_RE.exec(html)) { + const raw = m[1].trim() + try { + yield { raw, data: JSON.parse(raw) } + } catch (e) { + yield { + raw, + data: new SyntaxError( + `invalid JSON-LD: ${(e as Error).message}`, + ), + } + } + } +} + +function walkHtml(dir: string): string[] { + const out: string[] = [] + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, entry.name) + if (entry.isDirectory()) out.push(...walkHtml(p)) + else if (entry.isFile() && entry.name.endsWith('.html')) out.push(p) + } + return out +} + +describe('structured data (JSON-LD)', () => { + // Mirrors test/build.test.ts convention: this suite only makes sense + // against built output, so fail fast with an actionable message. + test('produces dist/ (run `bun run build` before this test)', () => { + expect( + existsSync(DIST), + 'dist/ not found — run `bun run build` before this test', + ).toBe(true) + }) + + if (!existsSync(DIST)) { + // dist/ is absent: stop registering per-page tests; the guard above + // already explains what to do. (Vitest skips the rest of this file.) + return + } + + const pages = walkHtml(DIST) + test('dist has built HTML pages to inspect', () => { + expect(pages.length).toBeGreaterThan(0) + }) + + for (const file of pages) { + // eslint-disable-next-line no-loop-func + test(`JSON-LD in ${file.replace(DIST, '') || 'index.html'} is well-formed`, () => { + const rel = file.replace(DIST, '') || 'index.html' + const html = readFileSync(file, 'utf8') + const blocks: string[] = [] + for (const b of ldJsonBlocks(html)) { + blocks.push(b.raw) + // Must parse as JSON + expect( + b.data, + `JSON-LD block failed to parse:\n${b.raw}`, + ).not.toBeInstanceOf(SyntaxError) + // Must carry the required schema.org markers + if ( + b.data && + typeof b.data === 'object' && + !(b.data instanceof SyntaxError) + ) { + expect( + b.data as Record, + rel, + ).toHaveProperty('@context') + expect( + b.data as Record, + rel, + ).toHaveProperty('@type') + } + } + // Sanity: the homepage at least emits structured data + if (rel === '/index.html' || rel === 'index.html') { + expect(blocks.length).toBeGreaterThan(0) + } + }) + } +})