diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 2a28963a..74c976c9 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -53,3 +53,5 @@ {"id":"int-db886ffc","kind":"field_change","created_at":"2026-05-30T17:58:07.630601725Z","actor":"Stackwright Bot","issue_id":"stackwright-rqj","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} {"id":"int-34488cdd","kind":"field_change","created_at":"2026-05-31T01:52:40.569772239Z","actor":"Stackwright Bot","issue_id":"stackwright-b2w","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} {"id":"int-13dc0a3a","kind":"field_change","created_at":"2026-05-31T13:04:03.328783799Z","actor":"Stackwright Bot","issue_id":"stackwright-70q","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-e4836528","kind":"field_change","created_at":"2026-05-31T14:56:36.319940229Z","actor":"Stackwright Bot","issue_id":"stackwright-nw6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-7d9d52ed","kind":"field_change","created_at":"2026-05-31T23:33:00.059300042Z","actor":"Stackwright Bot","issue_id":"stackwright-11p","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 41d9229f..8e95e77a 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -5,11 +5,12 @@ {"_type":"issue","id":"stackwright-wao","title":"checkForPlaintextSecret: add high-entropy detection for real tokens (bi-directional check)","description":"The checkForPlaintextSecret function in packages/types/src/types/secret-detection.ts currently warns when entropy is low (\u003c 3.8) — catching human-readable plaintext passwords stored in integration auth YAML fields.\n\nHowever, there are two distinct threat classes that one threshold can't catch:\n- Low entropy (\u003c 3.8): human-readable passwords like \"password123\"\n- High entropy (\u003e 4.5): real cryptographic tokens like JWTs, bearer tokens, API keys stored directly in YAML\n\nThe current check only catches the first class. A JWT stored as `token: \"eyJhbGciOiJSUzI1NiJ9...\"` passes through silently.\n\nSuggested fix: change the condition to `entropy \u003c 3.8 || entropy \u003e 4.5` to catch both classes. Update the warning message to distinguish which case triggered.\n\nAdditional context: checkForPlaintextSecret is currently exported but never called (dead code). This issue should also cover wiring it into the prebuild pipeline at the integration auth processing point in packages/build-scripts/src/prebuild.ts. Consider adding a minimum length guard (e.g., skip values \u003c 32 chars for the high-entropy check) to reduce false positives from short but random-looking strings.","notes":"Tags: security, types, build-scripts","status":"closed","priority":1,"issue_type":"task","assignee":"planning-agent-2840e9","owner":"bot@per-aspera.dev","created_at":"2026-05-19T01:40:56Z","created_by":"Stackwright Bot","updated_at":"2026-05-20T16:41:06Z","started_at":"2026-05-20T16:32:10Z","closed_at":"2026-05-20T16:41:06Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-a1g","title":"feat: Otter Agents AI-assisted site generation pipeline — complete remaining work","description":"Four-otter pipeline (Foreman, Brand, Theme, Page) core architecture is complete. Remaining work: end-to-end testing of full pipeline, create 3-5 example sites generated by the otter raft, refine handoff protocol between otters, design Collection Otter for Phase 2. Affects packages/otters. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/236","notes":"Session progress: Refined handoff protocol across all 4 otter JSONs (session_id passing, phase-skip logic, dev server awareness, error recovery). Expanded Brand Otter discovery (14 questions, 5 phases), enriched BRAND_BRIEF.md template. Theme Otter now includes nav config and dark mode guidance. Page Otter now covers all 18 content types + navSidebar. Added Collection Otter Phase 2 design spec (packages/otters/src/stackwright-collection-otter-spec.md). Added E2E pipeline validation checklist (packages/otters/test/e2e-pipeline-checklist.md). Remaining: create 3-5 example sites by running the otter pipeline against real projects in stackwright-tests.","status":"closed","priority":1,"issue_type":"feature","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:48Z","created_by":"Stackwright Bot","updated_at":"2026-05-25T19:23:07Z","started_at":"2026-05-25T17:45:24Z","closed_at":"2026-05-25T19:23:07Z","close_reason":"Created 3 reference example sites demonstrating complete otter pipeline output: examples/law-firm-example/ (4 pages, navy/gold), examples/saas-example/ (5 pages + pricing, indigo/amber), examples/restaurant-example/ (3 pages, terracotta/cream). Updated e2e-pipeline-checklist.md marking law firm, SaaS, and restaurant as tested. Updated packages/otters/README.md with Example Sites section. Remaining: Portfolio/Agency and B2B services examples for Phase 2.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-ufs","title":"feat(types,core): migrate to explicit type field discrimination in content renderer","description":"Content renderer currently uses Object.entries(item)[0] to discriminate content types — relies on JS object insertion order (not guaranteed), prevents TypeScript discriminated unions, produces poor error messages. Migrate to explicit type field on every content item (z.object({ type: z.literal('...'), ... })). Breaking change — coordinate with next major version bump. Acceptance: update Zod schemas, content renderer, TypeScript unions, all YAML files, tests, JSON schemas, AGENTS.md tables. GitHub: https://github.com/Per-Aspera-LLC/stackwright/issues/344","status":"closed","priority":1,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-18T22:33:44Z","created_by":"Stackwright Bot","updated_at":"2026-05-19T00:22:32Z","closed_at":"2026-05-19T00:22:32Z","close_reason":"Already implemented: contentRenderer.tsx uses item.type for discrimination, content.ts uses z.literal('type') on all schemas. Shipped before this triage run.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-8bu","title":"Investigate cross-repo version manifest (OSS catalog as king)","description":"## Context\n\nstackwright-pro currently uses a fragile `sync-versions.mjs` script with:\n- Hardcoded `OSS_VERSION_FALLBACKS` that go stale\n- Regex-based lockfile parsing (known first-match bugs)\n- No formal contract between OSS and Pro for shared dep versions\n\nNow that OSS has a pnpm catalog (PR #478), we have a single source of truth for architectural deps (zod, js-yaml, tsup, vitest, typescript, @types/node, @types/js-yaml).\n\n## Goal\n\nEstablish OSS as the version authority that Pro and customer projects consume.\n\n## Options to Investigate\n\n1. **Shared manifest file**: OSS build emits `versions-manifest.json` (generated from catalog + package versions). Pro reads at build time.\n2. **Published constraints package**: `@stackwright/versions` npm package that exports the version map. Pro imports it in sync-versions.mjs.\n3. **Sibling-path read (dev only)**: Pro's sync-versions.mjs reads `../stackwright/pnpm-workspace.yaml` catalog directly.\n4. **CI sync bot**: GH Action in Pro reads OSS catalog, proposes alignment PRs.\n5. **pnpm named catalogs** (future): Cross-workspace catalog sharing when pnpm supports it.\n\n## Acceptance Criteria\n\n- [ ] Decide on approach (recommend A+E hybrid graduating to B)\n- [ ] Eliminate hardcoded `OSS_VERSION_FALLBACKS` from Pro\n- [ ] Pro's shared deps (zod, js-yaml) are validated against OSS catalog\n- [ ] Customer scaffolded projects inherit correct version floors automatically\n\n## References\n\n- OSS catalog: `pnpm-workspace.yaml` (PR #478)\n- Pro sync script: `stackwright-pro/scripts/sync-versions.mjs`\n- Pro fallbacks: `OSS_VERSION_FALLBACKS` object in sync-versions.mjs","status":"open","priority":2,"issue_type":"task","owner":"bot@per-aspera.dev","created_at":"2026-05-31T22:46:36Z","created_by":"Stackwright Bot","updated_at":"2026-05-31T22:46:36Z","labels":["architecture","cross-repo","dependencies"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-ri2","title":"Performance: Image optimization pipeline with sharp in prebuild","description":"## Problem\nThe image co-location pipeline (prebuild.ts) copies images as-is. No resizing, no format conversion, no blur placeholder generation. NextStackwrightImage accepts placeholder and blurDataURL props but nothing generates them. sharp is already in pnpm.onlyBuiltDependencies but never actually used.\n\n## Proposed Solution\nDuring prebuild, run co-located images through sharp:\n- Generate WebP/AVIF variants alongside the original\n- Create tiny (10px wide) blur placeholder as base64 for blurDataURL\n- Emit _image-manifest.json mapping original paths → optimized paths + metadata\n- NextStackwrightImage reads manifest and auto-provides placeholder=blur + blurDataURL\n- Add imageOptimization config to stackwright.yml:\n formats: [webp, avif]\n quality: 80\n maxWidth: 1920\n blur: true\n\n## Impact\nDirectly affects Core Web Vitals (LCP) for every Stackwright site.","acceptance_criteria":"- [ ] sharp processes all co-located images during prebuild\n- [ ] WebP variants generated alongside originals\n- [ ] Blur placeholders generated as base64 data URIs\n- [ ] _image-manifest.json emitted with image metadata\n- [ ] NextStackwrightImage auto-provides blurDataURL when available\n- [ ] imageOptimization config in stackwright.yml schema\n- [ ] Opt-out via --no-image-optimization flag\n- [ ] Performance regression test (prebuild time budget)\n- [ ] Unit tests for image processing","status":"open","priority":2,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-31T13:30:26Z","created_by":"Stackwright Bot","updated_at":"2026-05-31T13:30:26Z","labels":["feature","performance","prebuild"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-7dv","title":"Feature: Markdown rendering in TextBlocks via micromark","description":"## Problem\nTextBlock only supports plain text strings. Content authors and AI agents cannot express bold, italic, links, or lists without resorting to separate content types. This forces verbose YAML for simple prose paragraphs.\n\n## Proposed Solution\n- Integrate micromark (~4KB gzipped, zero deps, CommonMark compliant) for inline markdown rendering\n- Add optional format: plain | markdown field to TextBlock (default: plain for backward compat)\n- At render time, sanitize HTML output (no raw HTML passthrough — maintain safe by construction guarantee)\n- Narrow scope: inline markdown only, no arbitrary HTML\n\n## Why micromark over remark\nStreaming tokenizer with no AST overhead. Zero external dependencies. Tiny bundle. Perfect for a framework that values constrained output.","acceptance_criteria":"- [ ] TextBlock schema extended with optional format field\n- [ ] micromark integrated with sanitized output (no raw HTML passthrough)\n- [ ] Bold, italic, links, inline code render correctly\n- [ ] Lists and headings within markdown blocks render correctly\n- [ ] Default format: plain maintains 100% backward compatibility\n- [ ] XSS vectors are blocked (no script injection via markdown)\n- [ ] Unit tests for markdown rendering + sanitization\n- [ ] JSON schema regenerated\n- [ ] AGENTS.md content type table updated","status":"closed","priority":2,"issue_type":"feature","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-31T13:30:13Z","created_by":"Stackwright Bot","updated_at":"2026-05-31T14:18:23Z","started_at":"2026-05-31T14:12:32Z","closed_at":"2026-05-31T14:18:23Z","close_reason":"Closed","labels":["content-types","dx","feature"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-qb9","title":"A11y: Add aria-live regions for dynamic state changes (Form, Search, Carousel)","description":"## Problem\nZero uses of aria-live, role=alert, or role=status anywhere in packages/core/src (confirmed via grep). Components with dynamic state changes:\n- Form.tsx: loading → success/error with no announcements\n- SearchModal.tsx: result count changes not announced\n- Carousel.tsx: slide changes not communicated\n- ContentItemErrorBoundary: error UI with no live region\n\nThis violates WCAG 4.1.3 (Status Messages).\n\n## Proposed Solution\n- Add AriaLiveRegion utility component to core\n- Form.tsx: announce Submitting.../success/error messages\n- SearchModal.tsx: announce result count (3 results found / No results)\n- Carousel: announce slide position (Slide 2 of 5)\n- ContentItemErrorBoundary: wrap error message in role=alert","acceptance_criteria":"- [ ] Form announces submission state changes to screen readers\n- [ ] SearchModal announces result count changes\n- [ ] Carousel announces current slide position on change\n- [ ] ContentItemErrorBoundary uses role=alert\n- [ ] E2E tests verify aria-live announcements\n- [ ] No false positives in axe-core a11y audit","status":"closed","priority":2,"issue_type":"feature","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-31T13:30:00Z","created_by":"Stackwright Bot","updated_at":"2026-05-31T14:30:05Z","started_at":"2026-05-31T14:21:12Z","closed_at":"2026-05-31T14:30:05Z","close_reason":"Closed","labels":["a11y","quality","wcag"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-nw6","title":"A11y: Add prefers-reduced-motion support across all animated components","description":"## Problem\nZero instances of prefers-reduced-motion in the entire codebase (confirmed via grep). Yet multiple components have animations:\n- DynamicPage.tsx injects CSS keyframes (sf-shimmer, sf-drift, sf-float) for background animations\n- Carousel.tsx has autoplay with timed transitions\n- TopAppBar uses transition: background-color 0.2s\n\nUsers with vestibular disorders get no relief. This is a WCAG 2.3.3 failure.\n\n## Proposed Solution\n- Add a useReducedMotion() hook (matchMedia wrapper)\n- Wrap all @keyframes injections in @media (prefers-reduced-motion: no-preference) guard\n- Carousel: disable autoplay + instant slide changes when reduced motion preferred\n- Expose theme-level config: motionPreference: respect | always | never","acceptance_criteria":"- [ ] useReducedMotion() hook added to packages/core/src/hooks/\n- [ ] All CSS keyframe animations wrapped in prefers-reduced-motion media query\n- [ ] Carousel autoplay disabled when reduced motion is preferred\n- [ ] All transitions reduced to 0ms or removed when reduced motion preferred\n- [ ] E2E test verifying animations are suppressed with emulateMedia\n- [ ] Unit test for the hook","status":"closed","priority":2,"issue_type":"feature","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-31T13:29:49Z","created_by":"Stackwright Bot","updated_at":"2026-05-31T14:56:36Z","started_at":"2026-05-31T14:52:01Z","closed_at":"2026-05-31T14:56:36Z","close_reason":"Closed","labels":["a11y","quality","wcag"],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"stackwright-11p","title":"SEO Autopilot: Auto-generate sitemap.xml, robots.txt, and JSON-LD in prebuild","description":"## Problem\nDespite being a content-first static site framework, Stackwright has zero SEO automation. No sitemap.xml, no robots.txt, no structured data. The prebuild already walks all pages and knows their slugs, titles, and content types.\n\n## Proposed Solution\n- Auto-generate sitemap.xml in public/ during prebuild (all page slugs + locale variants are already known)\n- Auto-generate robots.txt with sensible defaults\n- Auto-generate JSON-LD for content types that map naturally to schema.org:\n - faq → FAQPage schema\n - pricing_table → Product/Offer schemas\n - Collection entries with dates → Article/BlogPosting\n- Add optional meta field to page YAML for og:image, description, canonicalUrl\n\n## Why This Fits Stackwright\nThe constrained schema is uniquely suited — content types are enumerable and validated, so JSON-LD generation can be deterministic and correct by construction.","acceptance_criteria":"- [ ] prebuild generates public/sitemap.xml with all page slugs including locale variants\n- [ ] prebuild generates public/robots.txt referencing the sitemap\n- [ ] FAQ content items auto-generate FAQPage JSON-LD in page head\n- [ ] pricing_table content items auto-generate Product/Offer JSON-LD\n- [ ] Collection entries with date fields generate Article JSON-LD\n- [ ] All generated structured data passes Google Rich Results Test\n- [ ] Unit tests for sitemap and JSON-LD generation","status":"open","priority":2,"issue_type":"feature","owner":"bot@per-aspera.dev","created_at":"2026-05-31T13:29:30Z","created_by":"Stackwright Bot","updated_at":"2026-05-31T13:29:30Z","labels":["feature","prebuild","seo"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"stackwright-11p","title":"SEO Autopilot: Auto-generate sitemap.xml, robots.txt, and JSON-LD in prebuild","description":"## Problem\nDespite being a content-first static site framework, Stackwright has zero SEO automation. No sitemap.xml, no robots.txt, no structured data. The prebuild already walks all pages and knows their slugs, titles, and content types.\n\n## Proposed Solution\n- Auto-generate sitemap.xml in public/ during prebuild (all page slugs + locale variants are already known)\n- Auto-generate robots.txt with sensible defaults\n- Auto-generate JSON-LD for content types that map naturally to schema.org:\n - faq → FAQPage schema\n - pricing_table → Product/Offer schemas\n - Collection entries with dates → Article/BlogPosting\n- Add optional meta field to page YAML for og:image, description, canonicalUrl\n\n## Why This Fits Stackwright\nThe constrained schema is uniquely suited — content types are enumerable and validated, so JSON-LD generation can be deterministic and correct by construction.","acceptance_criteria":"- [ ] prebuild generates public/sitemap.xml with all page slugs including locale variants\n- [ ] prebuild generates public/robots.txt referencing the sitemap\n- [ ] FAQ content items auto-generate FAQPage JSON-LD in page head\n- [ ] pricing_table content items auto-generate Product/Offer JSON-LD\n- [ ] Collection entries with date fields generate Article JSON-LD\n- [ ] All generated structured data passes Google Rich Results Test\n- [ ] Unit tests for sitemap and JSON-LD generation","status":"closed","priority":2,"issue_type":"feature","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-31T13:29:30Z","created_by":"Stackwright Bot","updated_at":"2026-05-31T23:33:00Z","started_at":"2026-05-31T23:17:06Z","closed_at":"2026-05-31T23:33:00Z","close_reason":"Closed","labels":["feature","prebuild","seo"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-pzr","title":"fix(e2e): update SSR test for App Router static export behavior","description":"The 'JavaScript disabled still shows content (SSR)' test in tests/edge-cases/error-scenarios.spec.ts:466 fails because App Router static export (output: 'export') produces client-rendered pages — the body is empty when JS is disabled. This is expected behavior for static export but the test was written for Pages Router SSR. Options: (1) update test to expect empty body for static export mode, (2) skip test when output=export, (3) remove test. Discovered during stackwright-rqj CI validation.","status":"closed","priority":2,"issue_type":"bug","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-30T22:20:01Z","created_by":"Stackwright Bot","updated_at":"2026-05-31T13:59:12Z","started_at":"2026-05-31T13:57:03Z","closed_at":"2026-05-31T13:59:12Z","close_reason":"Closed","labels":["app-router","e2e"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-b2w","title":"perf: tree-shake MapLibre GL from stackwright-docs bundle","description":"MapLibre GL JS (36KB gzip / 118KB raw) is included in every page of stackwright-docs despite zero pages using the map content type. Discovered during App Router migration validation (stackwright-rqj). The entire @stackwright/maplibre package is pulled into the shared chunk by Turbopack because @stackwright/core imports or re-exports it unconditionally. Fix: ensure MapLibre is only loaded via React.lazy() or dynamic import when a page actually contains a map content_item. Removing this dead weight would reduce first-load JS from ~448KB to ~412KB gzip.","status":"closed","priority":2,"issue_type":"task","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-30T17:57:44Z","created_by":"Stackwright Bot","updated_at":"2026-05-31T01:52:41Z","started_at":"2026-05-31T01:49:08Z","closed_at":"2026-05-31T01:52:41Z","close_reason":"Closed","labels":["bundle-size","performance"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"stackwright-70q","title":"perf(core): bundle optimization phase 3 — lazy content types + dep cleanup","description":"## Context\nAfter the App Router migration (June 2026), React.lazy() code splitting now works correctly. The ADR (docs/adr/bundle-architecture.md) targets ≤250KB gzip first-load. Current interim budget is 350KB. This bead covers the remaining optimizations to reach the target.\n\n## Tasks\n\n### Code Splitting (biggest wins)\n- [ ] Lazy-load CodeBlock + PrismJS (~12-15KB gzip savings)\n - Add React.lazy() wrapper in componentRegistry.ts for code_block\n - Add src/components/base/CodeBlock.tsx as separate tsup entry in packages/core/tsup.config.ts\n - prismHighlighter.ts moves into the lazy chunk automatically\n- [ ] Lazy-load FAQ + @radix-ui/react-accordion (~5KB gzip savings)\n - Add React.lazy() wrapper in componentRegistry.ts for faq\n - Add Faq.tsx as separate tsup entry\n\n### Dependency Cleanup\n- [ ] Remove fuse.js from @stackwright/core dependencies (it's dynamically imported in SearchModal — move to peerDependencies or optionalDependencies)\n- [ ] Remove fuse.js from examples/stackwright-docs direct dependencies if unnecessary\n- [ ] Test removing transpilePackages from examples/stackwright-docs/next.config.js (App Router may not need it for workspace packages)\n\n### Budget Tightening\n- [ ] After above changes land, measure actual first-load size\n- [ ] Update performance-budgets.json: target 250KB max / 200KB warn\n- [ ] Update ADR Decision 5 to reflect final achieved targets\n\n## Acceptance\n- First-load JS ≤ 250KB gzip\n- CodeBlock and FAQ load as separate async chunks (visible in out/_next/static/chunks/)\n- All E2E tests pass\n- Performance benchmarks pass with tightened budgets\n\n## References\n- docs/adr/bundle-architecture.md (Decisions 2, 3, 5, 6)\n- packages/e2e/tests/performance/performance-budgets.json\n- packages/core/tsup.config.ts (splitting: true already enabled)","status":"closed","priority":2,"issue_type":"task","assignee":"Stackwright Bot","owner":"bot@per-aspera.dev","created_at":"2026-05-30T17:35:47Z","created_by":"Stackwright Bot","updated_at":"2026-05-31T13:04:03Z","started_at":"2026-05-31T13:01:23Z","closed_at":"2026-05-31T13:04:03Z","close_reason":"Closed","labels":["bundle-size","code-splitting","performance"],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/.changeset/seo-autopilot.md b/.changeset/seo-autopilot.md new file mode 100644 index 00000000..35fa5ca1 --- /dev/null +++ b/.changeset/seo-autopilot.md @@ -0,0 +1,16 @@ +--- +"@stackwright/build-scripts": minor +"@stackwright/core": minor +--- + +feat: SEO Autopilot — auto-generate sitemap.xml, robots.txt, and JSON-LD structured data + +Prebuild now generates `sitemap.xml` and `robots.txt` in `public/` when `meta.base_url` is set in `stackwright.yml`. Pages with `noindex: true` are excluded from the sitemap. Locale variants get `xhtml:link` alternate entries. + +Content types with natural schema.org mappings now emit `` injection. + * Renders nothing when the data array is empty. + */ +export function JsonLdScript({ data }: JsonLdScriptProps) { + if (data.length === 0) return null; + + return ( + <> + {data.map((item, index) => ( +