Files
ComfyUI_frontend/apps/website/package.json
Mobeen Abdullah 1815c7f7a4 feat(website): add JSON-LD structured data across the site (#13480)
## Summary

This PR adds schema.org **JSON-LD structured data across the whole
marketing site**, built from one shared, CMS-ready module and gated by a
small CI validator. It replaces the old global block (which had a stale
logo, wrong social links, disconnected nodes, and a head slot that
rendered three times) with a single connected `@graph` on every page.
Structured data only — there is no visual or runtime change for users.
Tracks Linear **FE-1170**.

The design goal was that structured data should be impossible to get
subtly wrong: one place builds it, honesty rules are enforced in code,
and a build-time validator fails the build if any page ships a broken
`@id` graph or a fabricated price/rating.

## Changes

- **One builder, one sink.** `utils/jsonLd.ts` holds pure, node-testable
builders; `components/common/JsonLdGraph.astro` is the single escaped
`<script type="application/ld+json">` sink (prevents `</script>`
breakout XSS).
- **The layout owns the page entity.** `BaseLayout` emits a baseline
`Organization` + `WebSite` + `WebPage` graph on every page from its own
`title`/`description`/canonical props. Enriched pages pass only what is
specific to them: `pageType`, `breadcrumbs`, `mainEntityId`, and
`extraJsonLd` nodes. This makes it impossible for a page's meta tags and
its structured data to drift apart.
- **Corrected site-wide entity.** Raster PNG logo (Google does not index
SVG logos), real `sameAs` handles sourced from the footer links,
`@id`-linked `Organization`/`WebSite`, and the triple-rendered head slot
fixed.
- **Honesty is enforced, not just intended.** No fabricated
`Review`/`AggregateRating`/`Offer`. Pricing offers are parsed only from
plain `$N` copy (a future "Contact us" drops the offer instead of
shipping a garbage price). Third-party node packs and listed models do
**not** claim Comfy Org as author/publisher. `noindex` pages (404,
payment) emit no structured data.
- **CI validator.** `pnpm --filter @comfyorg/website validate:jsonld`
runs over `dist/` in the website build workflow and fails on invalid
JSON, an unresolved `@id`, a fake rating, or an empty/non-numeric offer
price.
- **Breaking:** none.

## Coverage (also a manual QA checklist for the preview)

Every public page carries at least `Organization` + `WebSite` +
`WebPage`. The pages below add a page-specific primary entity, in
**English and zh-CN**:

| Page | Path (example) | Adds to the graph |
|---|---|---|
| Home | `/`, `/zh-CN` | `SoftwareApplication` (ComfyUI, free) +
`SoftwareSourceCode` |
| Download | `/download` | `SoftwareApplication` (ComfyUI desktop) |
| Pricing | `/cloud/pricing` | `Product` + 3 monthly `Offer`s
($20/$35/$100) + Breadcrumb |
| Models catalog | `/p/supported-models` | `CollectionPage` + `ItemList`
(313, lean) + Breadcrumb |
| Model detail | `/p/supported-models/4x-ultrasharp` |
`SoftwareApplication` + `FAQPage` + Breadcrumb |
| Nodes catalog | `/cloud/supported-nodes` | `CollectionPage` +
`ItemList` (58 packs) + Breadcrumb |
| Node-pack detail | `/cloud/supported-nodes/ComfyQR` |
`SoftwareApplication` (+ free `Offer`) + Breadcrumb |
| Demos | `/demos/community-workflows` | `LearningResource` + Breadcrumb
|
| About / Contact | `/about`, `/contact` | `AboutPage` / `ContactPage`
(Org as `mainEntity`) + Breadcrumb |
| Careers | `/careers` | `CollectionPage` + `ItemList` of open roles +
Breadcrumb |
| Affiliates | `/affiliates` | `FAQPage` + Breadcrumb |

Pages deliberately left at the baseline `WebPage` (generic landings,
legal, coming-soon) and pages with **no** structured data (`noindex`:
`/404`, `/payment/*`; redirect URLs) are intentional.

## Review Focus

- **Layout-owns-WebPage design.** `BaseLayout` builds the `WebPage`;
pages contribute only extra nodes. This is the main structural decision
and is what removes meta-vs-schema drift by construction.
- **Honesty guardrails.** Worth confirming: pricing offers, third-party
author omission on packs/models, and that `noindex` pages emit nothing.
- **`@id` and URL consistency.** All cross-page links and `@id`s resolve
to the canonical trailing-slash form; zh-CN breadcrumbs are rooted under
`/zh-CN`; the singleton `WebSite`/`#software` entities carry one
consistent definition across pages and locales.
- **The validator.** It is a bespoke ~100-line script scoped to the
website build job (not the prod deploy). Happy to make it non-blocking
or drop it if the team prefers.
- **Coordination with #13468.** Both branches add
`components/common/JsonLdGraph.astro`. Customer pages are intentionally
excluded here; #13468 can converge onto this shared builder.

## Verification

`astro check` 0 errors · 166 unit tests · `knip` 0 · `eslint` 0 · build
497 pages · validator passes across 500 pages · JSON-LD e2e specs 24/24.
(The 3 pre-existing demo e2e timeouts are an external Arcade-embed flake
on one slug, reproduced identically on `main`.)

## Screenshots

Not applicable — head-only structured data, no visual change. Validate
on the Vercel preview with the Rich Results Test and the schema.org
validator. Note: `@id`/URL values render as `comfy.org` (from
`astro.config` `site`) even on the preview host, which is correct.
2026-07-09 21:03:43 +00:00

57 lines
1.9 KiB
JSON

{
"name": "@comfyorg/website",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"dev:no-toolbar": "cross-env NO_TOOLBAR=1 astro dev",
"build": "astro build",
"preview": "astro preview",
"typecheck": "astro check",
"test:unit": "vitest run",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:local": "cross-env PLAYWRIGHT_LOCAL=1 playwright test",
"test:visual": "playwright test --project visual",
"test:visual:update": "playwright test --project visual --update-snapshots",
"ashby:refresh-snapshot": "tsx ./scripts/refresh-ashby-snapshot.ts",
"cloud-nodes:refresh-snapshot": "tsx ./scripts/refresh-cloud-nodes-snapshot.ts",
"generate:models": "tsx ./scripts/generate-models.ts",
"validate:jsonld": "tsx ./scripts/validate-jsonld.ts"
},
"dependencies": {
"@astrojs/sitemap": "catalog:",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/object-info-parser": "workspace:*",
"@comfyorg/shared-frontend-utils": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@lucide/vue": "catalog:",
"@vercel/analytics": "catalog:",
"@vueuse/core": "catalog:",
"class-variance-authority": "catalog:",
"cva": "catalog:",
"gsap": "catalog:",
"lenis": "catalog:",
"posthog-js": "catalog:",
"reka-ui": "catalog:",
"three": "catalog:",
"vue": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@astrojs/check": "catalog:",
"@astrojs/mdx": "catalog:",
"@astrojs/vue": "catalog:",
"@playwright/test": "catalog:",
"@tailwindcss/vite": "catalog:",
"astro": "catalog:",
"tailwindcss": "catalog:",
"tsx": "catalog:",
"tw-animate-css": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:",
"vue-component-type-helpers": "catalog:"
}
}