Skip to content

fix(schema-org): missing Google rich result fields - #920

Merged
harlan-zw merged 5 commits into
mainfrom
feat/google-rich-result-fields
Jul 30, 2026
Merged

fix(schema-org): missing Google rich result fields#920
harlan-zw merged 5 commits into
mainfrom
feat/google-rich-result-fields

Conversation

@harlan-zw

@harlan-zw harlan-zw commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

🔗 Linked issue

Follow-up to #919 and #917.

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

Closes the property gaps found while checking the existing Schema.org helpers against Google's current structured data documentation. This adds typed fields and nested resolution for Dataset, Event, Image, JobPosting, LocalBusiness, Organization, Product and merchant listings, Q&A and ProfilePage, Recipe, Review, Video, paywalls, and speakable content.

The docs now state the package boundary directly: first-class types follow Google Search rich result fields. Other Schema.org properties remain valid as pass-through JSON-LD.

⚠️ Breaking changes

Google-required fields and alternatives are now represented in the public types. Existing incomplete markup may fail typecheck.

📝 Migration

Add the fields reported by TypeScript. Tightened existing helpers include Course.description, Event.name, DataDownload.contentUrl, LocalBusiness.address, SoftwareApp.name with aggregateRating or review, an AggregateRating count, and applicantLocationRequirements for remote jobs.

✅ Verification

  • pnpm lint
  • pnpm typecheck
  • pnpm build
  • TZ=UTC pnpm exec vitest run, 1,924 passed
  • pnpm lint:docs
  • pnpm --filter @unhead/schema-org test:attw

Summary by CodeRabbit

  • New Features
    • Expanded Schema.org/Google rich-result support across datasets, jobs (telecommute constraints), organizations/local businesses, products, recipes, images, webpages (profile + speakable), and videos (enhancements).
    • Added identity-aware linking for authors and introduced additional typed Schema.org helpers and resolvers.
  • Documentation
    • Added the “Google Search Fields” guide and refreshed multiple Schema.org/Google coverage sections.
  • Bug Fixes
    • Improved URL and date normalization (including licensing/media URLs) and paywalled section mapping.
    • Tightened structured-data typing for several schemas (e.g., Course, Image, Dataset download links).
  • Tests
    • Expanded runtime and compile-time coverage, including async test setup.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR expands Schema.org Google Search support with typed fields, conditional contracts, identity-aware relation resolution, composed feature resolvers, URL/date normalization, documentation, public exports, and compile-time/runtime coverage across multiple schema nodes.

Changes

Schema.org Google Search support

Layer / File(s) Summary
Google Search guidance and typed contracts
docs/schema-org/...
Adds the Google Search Fields guide and updates documentation for composed features, rich-result requirements, and expanded Schema.org input types.
Shared types and relation resolution
packages/schema-org/src/core/*, packages/schema-org/src/types.ts
Adds identity-aware relation resolution and shared Schema.org contracts for identities, properties, geography, interactions, actions, and digital-source values.
Composed feature and media resolution
packages/schema-org/src/nodes/{Article,Question,Recipe,Image,Video,WebPage}/*
Adds typed relations and resolvers for identities, speakable content, paywalled sections, answers, recipe instructions, image licensing, and video enhancements.
Structured-data node expansion and validation
packages/schema-org/src/nodes/*, packages/schema-org/test/*, test/exports/*
Expands dataset, job, local-business, organization, product, offer, event, and supporting node contracts, with resolver tests, public type assertions, and resolver export coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • unjs/unhead#852: Extends the Schema.org package’s public type export surface.
  • unjs/unhead#868: Overlaps in shared resolver, type, and Schema.org node changes.
  • unjs/unhead#919: Updates DiscussionForumPosting author resolution to use shared identity relation logic and common interaction-counter types.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise, conventional, and matches the main schema-org rich result field changes.
Description check ✅ Passed The description covers linked issue, change type, detailed summary, breaking changes, migration, and verification.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/google-rich-result-fields

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size

⚠️ 1 bundle grew · net +1.6 kB gz

Bundle Gzipped Δ
Schema.org (Minimal) 10.1 kB → 11.7 kB 🔴 +1.6 kB (+15.7%)
All bundles (14)
Bundle Gzipped Brotli Raw
Core
Client (Minimal) 5.4 kB 4.9 kB 13.6 kB
Client (Full) 9.5 kB 8.7 kB 24.9 kB
Client (Self-Contained) 5.7 kB 5.1 kB 14.3 kB
Server (Minimal) 4.9 kB 4.4 kB 12.3 kB
Server (Self-Contained) 5.2 kB 4.7 kB 13 kB
Vue
Vue Client (Minimal) 5.9 kB 5.3 kB 14.6 kB
Vue Client (Full) 10.5 kB 9.6 kB 27.7 kB
Vue Server (Minimal) 5.4 kB 4.9 kB 13.4 kB
React
React Client (Minimal) 5.8 kB 5.3 kB 14.7 kB
React Client (Full) 10.6 kB 9.6 kB 28.1 kB
React Server (Minimal) 5.2 kB 4.7 kB 13 kB
Schema.org
Schema.org (Minimal) 11.7 kB 10.5 kB 34.1 kB 🔴
Schema.org Imports 0.1 kB 0.1 kB 0.1 kB
Schema.org Vue Meta 0.5 kB 0.4 kB 1 kB

📦 Runtime Dependencies

No runtime dependency changes

All packages (10)
Package External deps Install size Largest dependency Skipped optional
@unhead/angular 10 773.6 kB @jridgewell/trace-mapping 146.7 kB 0
@unhead/bundler 17 5.5 MB @oxc-parser/binding-linux-arm64-gnu 2.2 MB 19
@unhead/cli 22 5.7 MB @oxc-parser/binding-linux-arm64-gnu 2.2 MB 19
@unhead/eslint-plugin 9 683.2 kB @jridgewell/trace-mapping 146.7 kB 0
@unhead/react 18 5.5 MB @oxc-parser/binding-linux-arm64-gnu 2.2 MB 19
@unhead/schema-org 9 683.2 kB @jridgewell/trace-mapping 146.7 kB 0
@unhead/solid-js 18 5.5 MB @oxc-parser/binding-linux-arm64-gnu 2.2 MB 19
@unhead/svelte 18 5.5 MB @oxc-parser/binding-linux-arm64-gnu 2.2 MB 19
@unhead/vue 18 5.5 MB @oxc-parser/binding-linux-arm64-gnu 2.2 MB 19
unhead 9 683.2 kB @jridgewell/trace-mapping 146.7 kB 0
Skipped optional dependencies (114)
  • @unhead/bundler: oxc-parser -> @oxc-parser/binding-android-arm-eabi, oxc-parser -> @oxc-parser/binding-android-arm64, oxc-parser -> @oxc-parser/binding-darwin-arm64, oxc-parser -> @oxc-parser/binding-darwin-x64, oxc-parser -> @oxc-parser/binding-freebsd-x64, oxc-parser -> @oxc-parser/binding-linux-arm-gnueabihf, oxc-parser -> @oxc-parser/binding-linux-arm-musleabihf, oxc-parser -> @oxc-parser/binding-linux-arm64-musl, oxc-parser -> @oxc-parser/binding-linux-ppc64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-musl, oxc-parser -> @oxc-parser/binding-linux-s390x-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-musl, oxc-parser -> @oxc-parser/binding-openharmony-arm64, oxc-parser -> @oxc-parser/binding-wasm32-wasi, oxc-parser -> @oxc-parser/binding-win32-arm64-msvc, oxc-parser -> @oxc-parser/binding-win32-ia32-msvc, oxc-parser -> @oxc-parser/binding-win32-x64-msvc
  • @unhead/cli: oxc-parser -> @oxc-parser/binding-android-arm-eabi, oxc-parser -> @oxc-parser/binding-android-arm64, oxc-parser -> @oxc-parser/binding-darwin-arm64, oxc-parser -> @oxc-parser/binding-darwin-x64, oxc-parser -> @oxc-parser/binding-freebsd-x64, oxc-parser -> @oxc-parser/binding-linux-arm-gnueabihf, oxc-parser -> @oxc-parser/binding-linux-arm-musleabihf, oxc-parser -> @oxc-parser/binding-linux-arm64-musl, oxc-parser -> @oxc-parser/binding-linux-ppc64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-musl, oxc-parser -> @oxc-parser/binding-linux-s390x-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-musl, oxc-parser -> @oxc-parser/binding-openharmony-arm64, oxc-parser -> @oxc-parser/binding-wasm32-wasi, oxc-parser -> @oxc-parser/binding-win32-arm64-msvc, oxc-parser -> @oxc-parser/binding-win32-ia32-msvc, oxc-parser -> @oxc-parser/binding-win32-x64-msvc
  • @unhead/react: oxc-parser -> @oxc-parser/binding-android-arm-eabi, oxc-parser -> @oxc-parser/binding-android-arm64, oxc-parser -> @oxc-parser/binding-darwin-arm64, oxc-parser -> @oxc-parser/binding-darwin-x64, oxc-parser -> @oxc-parser/binding-freebsd-x64, oxc-parser -> @oxc-parser/binding-linux-arm-gnueabihf, oxc-parser -> @oxc-parser/binding-linux-arm-musleabihf, oxc-parser -> @oxc-parser/binding-linux-arm64-musl, oxc-parser -> @oxc-parser/binding-linux-ppc64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-musl, oxc-parser -> @oxc-parser/binding-linux-s390x-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-musl, oxc-parser -> @oxc-parser/binding-openharmony-arm64, oxc-parser -> @oxc-parser/binding-wasm32-wasi, oxc-parser -> @oxc-parser/binding-win32-arm64-msvc, oxc-parser -> @oxc-parser/binding-win32-ia32-msvc, oxc-parser -> @oxc-parser/binding-win32-x64-msvc
  • @unhead/solid-js: oxc-parser -> @oxc-parser/binding-android-arm-eabi, oxc-parser -> @oxc-parser/binding-android-arm64, oxc-parser -> @oxc-parser/binding-darwin-arm64, oxc-parser -> @oxc-parser/binding-darwin-x64, oxc-parser -> @oxc-parser/binding-freebsd-x64, oxc-parser -> @oxc-parser/binding-linux-arm-gnueabihf, oxc-parser -> @oxc-parser/binding-linux-arm-musleabihf, oxc-parser -> @oxc-parser/binding-linux-arm64-musl, oxc-parser -> @oxc-parser/binding-linux-ppc64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-musl, oxc-parser -> @oxc-parser/binding-linux-s390x-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-musl, oxc-parser -> @oxc-parser/binding-openharmony-arm64, oxc-parser -> @oxc-parser/binding-wasm32-wasi, oxc-parser -> @oxc-parser/binding-win32-arm64-msvc, oxc-parser -> @oxc-parser/binding-win32-ia32-msvc, oxc-parser -> @oxc-parser/binding-win32-x64-msvc
  • @unhead/svelte: oxc-parser -> @oxc-parser/binding-android-arm-eabi, oxc-parser -> @oxc-parser/binding-android-arm64, oxc-parser -> @oxc-parser/binding-darwin-arm64, oxc-parser -> @oxc-parser/binding-darwin-x64, oxc-parser -> @oxc-parser/binding-freebsd-x64, oxc-parser -> @oxc-parser/binding-linux-arm-gnueabihf, oxc-parser -> @oxc-parser/binding-linux-arm-musleabihf, oxc-parser -> @oxc-parser/binding-linux-arm64-musl, oxc-parser -> @oxc-parser/binding-linux-ppc64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-musl, oxc-parser -> @oxc-parser/binding-linux-s390x-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-musl, oxc-parser -> @oxc-parser/binding-openharmony-arm64, oxc-parser -> @oxc-parser/binding-wasm32-wasi, oxc-parser -> @oxc-parser/binding-win32-arm64-msvc, oxc-parser -> @oxc-parser/binding-win32-ia32-msvc, oxc-parser -> @oxc-parser/binding-win32-x64-msvc
  • @unhead/vue: oxc-parser -> @oxc-parser/binding-android-arm-eabi, oxc-parser -> @oxc-parser/binding-android-arm64, oxc-parser -> @oxc-parser/binding-darwin-arm64, oxc-parser -> @oxc-parser/binding-darwin-x64, oxc-parser -> @oxc-parser/binding-freebsd-x64, oxc-parser -> @oxc-parser/binding-linux-arm-gnueabihf, oxc-parser -> @oxc-parser/binding-linux-arm-musleabihf, oxc-parser -> @oxc-parser/binding-linux-arm64-musl, oxc-parser -> @oxc-parser/binding-linux-ppc64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-gnu, oxc-parser -> @oxc-parser/binding-linux-riscv64-musl, oxc-parser -> @oxc-parser/binding-linux-s390x-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-gnu, oxc-parser -> @oxc-parser/binding-linux-x64-musl, oxc-parser -> @oxc-parser/binding-openharmony-arm64, oxc-parser -> @oxc-parser/binding-wasm32-wasi, oxc-parser -> @oxc-parser/binding-win32-arm64-msvc, oxc-parser -> @oxc-parser/binding-win32-ia32-msvc, oxc-parser -> @oxc-parser/binding-win32-x64-msvc

Production dependencies only. Peer dependencies and Unhead workspace packages are excluded. Skipped optional dependencies are unavailable on the CI platform.


⚡ Performance (directional)

⚠️ 1 slower · past the per-metric noise gate

Benchmark base → PR Δ
Schema.org cached allocated / render 133.7 KiB → 138.9 KiB 🔴 +5.2 KiB (+3.9%)
All benchmarks (25)
Benchmark PR Δ RME
SSR render (CPU) 0.364 ms ~ noise ±9.5%
SSR render (wall) 0.265 ms ~ noise ±4.7%
SSR allocated / render 242.7 KiB ~ noise ±5.7%
Schema.org cached render (CPU) 0.356 ms ~ noise ±6.4%
Schema.org cached render (wall) 0.252 ms ~ noise ±2.8%
Schema.org cached allocated / render 138.9 KiB 🔴 +5.2 KiB (+3.9%) ±0.9%
Streaming wrapStream drain (CPU) 0.245 ms ~ noise ±4.0%
Streaming wrapStream drain (wall) 0.154 ms ~ noise ±4.0%
Streaming allocated / drain 148.9 KiB ~ noise ±0.7%
Streaming suspense chunk (CPU) 0.013 ms ~ noise ±8.5%
Streaming allocated / suspense chunk 5.1 KiB ~ noise ±0.6%
CSR DOM mutations / nav 38 ~ noise
CSR re-render (CPU) 0.829 ms ~ noise ±5.5%
CSR re-render (wall) 0.468 ms ~ noise ±2.2%
Bundler: transformInclude mixed ids 0.385 ms ~ noise ±0.7%
Bundler: useSeoMetaTransform static calls 3.761 ms ~ noise ±4.2%
Bundler: minifyTransform inline script/style 0.581 ms ~ noise ±2.7%
Bundler: treeshakeServerComposables many calls 3.012 ms ~ noise ±5.1%
Bundler: treeshakeServerComposables skip unrelated code 0.002 ms ~ noise ±1.2%
Bundler: ssrStaticReplace many head.ssr reads 1.852 ms ~ noise ±6.5%
Bundler: ssrStaticReplace skip unrelated code 0.002 ms ~ noise ±1.1%
Bundler: createHeadTransform many createHead calls 0.662 ms ~ noise ±4.5%
Bundler: react streaming skip JSX without head calls 0.003 ms ~ noise ±1.2%
Bundler: react streaming transform JSX with head calls 1.688 ms ~ noise ±6.5%
Bundler: solid streaming skip JSX without head calls 0.003 ms ~ noise ±1.1%

Baseline: main @ 6b2c5b9 · 2026-07-29 · gzipped is the headline size metric · perf is directional (shared-runner, gated)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/schema-org/src/nodes/Image/index.ts (1)

10-19: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Orphaned JSDoc comment for the old url/contentUrl duplication note.

This comment block (documenting "contentUrl and url properties are intentionally duplicated") no longer documents anything — those fields were moved out into the new ImageLocation union below, leaving this comment floating directly above the unrelated caption doc block.

🧹 Suggested cleanup
 interface ImageBase extends Thing {
-  /**
-   * The fully-qualified, absolute URL of the image file (e.g., https://www.example.com/images/cat.jpg).
-   * Note: The contentUrl and url properties are intentionally duplicated.
-   */
   /**
    * A text string describing the image.
    * - Fall back to the image alt attribute if no specific caption field exists or is defined.
    */
   caption?: string
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/schema-org/src/nodes/Image/index.ts` around lines 10 - 19, Remove
the orphaned JSDoc block in the ImageBase interface that mentions duplicated
contentUrl and url properties, leaving the caption documentation directly above
caption. Do not alter the caption description or the ImageLocation definitions.
🧹 Nitpick comments (4)
packages/schema-org/src/nodes/Video/index.ts (1)

135-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

interactionCounterResolver is private to this file despite InteractionCounter being a shared type.

InteractionCounter is declared once in types.ts and is also used by Person/Organization, but this resolver (which sets the default '@type': 'InteractionCounter') is a non-exported const, so other node modules can't reuse it and must either duplicate it or (as currently happens in Person) skip resolving it entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/schema-org/src/nodes/Video/index.ts` around lines 135 - 139, Export
interactionCounterResolver so other schema node modules can reuse the shared
InteractionCounter default resolver. Keep its existing defineSchemaOrgResolver
configuration and default '`@type`' unchanged, and update consumers such as Person
or Organization to import and use the exported resolver instead of duplicating
or skipping resolution.
packages/schema-org/src/nodes/JobPosting/index.ts (1)

58-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the orphaned jobLocation doc block.

The job-location properties moved into the JobLocation union, but the JSDoc describing them stayed behind and now sits directly above directApply's own JSDoc (where TS ignores it).

🧹 Proposed cleanup
-  /**
-   * A description of the job location (e.g. TELECOMMUTE for telecommute jobs).
-   */
   /**
    * Indicates whether the URL that's associated with this job posting enables direct application for the job.
    */
   directApply?: boolean
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/schema-org/src/nodes/JobPosting/index.ts` around lines 58 - 64,
Remove the orphaned job-location JSDoc block immediately before the directApply
property in the JobPosting definition, preserving directApply’s own
documentation and declaration unchanged.
packages/schema-org/src/nodes/Question/index.ts (1)

60-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

video here rejects string URLs, unlike AnswerSimple.video.

videoResolver casts a bare string into { url }, and Answer types this as NodeRelations<VideoObject | string>; aligning the two avoids a surprising asymmetry between question and answer inputs.

♻️ Proposed alignment
-  video?: NodeRelations<VideoObject>
+  video?: NodeRelations<VideoObject | string>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/schema-org/src/nodes/Question/index.ts` at line 60, Update the
Question node’s video property to accept both VideoObject and string values,
matching Answer’s video type and the videoResolver behavior that converts bare
URLs into { url } objects.
packages/schema-org/src/nodes/Product/index.ts (1)

227-244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the exported ratingResolver instead of a duplicate certificationRatingResolver.

certificationRatingResolver duplicates ratingResolver (Rating/index.ts) but drops its cast (numeric shorthand) and bestRating/worstRating defaults, so Certification.certificationRating output is inconsistent with every other Rating usage in the codebase (e.g. AggregateRating, Review).

♻️ Proposed fix
-const certificationRatingResolver = defineSchemaOrgResolver<Rating>({
-  defaults: {
-    '`@type`': 'Rating',
-  },
-})
-
 const certificationResolver = defineSchemaOrgResolver<Certification>({
   defaults: {
     '`@type`': 'Certification',
   },
   resolve(node, ctx) {
-    node.certificationRating = resolveRelation(node.certificationRating, ctx, certificationRatingResolver)
+    node.certificationRating = resolveRelation(node.certificationRating, ctx, ratingResolver)
     node.issuedBy = resolveRelation(node.issuedBy, ctx, organizationResolver)
     if (node.url)
       node.url = resolveWithBase(ctx.meta.host, node.url)
     return node
   },
 })

Add import { ratingResolver } from '../Rating' alongside the other resolver imports at the top of the file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/schema-org/src/nodes/Product/index.ts` around lines 227 - 244,
Remove the duplicate certificationRatingResolver and update
certificationResolver to use the exported ratingResolver from ../Rating for
node.certificationRating. Add the corresponding import alongside the existing
resolver imports, preserving the organization and URL resolution behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/schema-org/5.api/9.schema/organization.md`:
- Line 169: Update the Recommended Properties entry for logo to use
NodeRelations<ImageObject | string>, matching the documented logo field type and
allowing multiple logos.

In `@docs/schema-org/5.api/9.schema/product.md`:
- Line 168: Update the documented `category` type to include
`NodeRelation<CategoryCode>` alongside `string` within the arrayable type,
matching the exported API and allowing valid `@id` references.
- Line 60: Update the Product node definition in its resolver implementation to
resolve hasCertification, isVariantOf, and subjectOf into their typed schema
nodes, matching the documented nested-relation behavior; alternatively remove
those three relations from the documentation claim if they are intentionally
unsupported.

In `@docs/schema-org/5.api/9.schema/recipe.md`:
- Around line 94-97: Update the `recipeInstructions` documentation comment to
describe all permitted values represented by `NodeRelations<HowToSection |
HowToStep | string>`: singular or related values, strings, `HowToSection`, and
`HowToStep`, rather than only an array of `HowToStep` objects.

In `@packages/schema-org/src/nodes/HowTo/HowToStep/index.ts`:
- Around line 42-50: Update the HowTo section/step discriminator in the Recipe
node to use an explicit type or resolver marker instead of treating every object
with itemListElement and no text as HowToSection, preserving text-less HowToStep
values as HowToStep. Add a regression test covering a text-less HowToStep and
its emitted `@type`.

In `@packages/schema-org/src/nodes/JobPosting/index.ts`:
- Around line 106-118: The JobLocation union still permits TELECOMMUTE without
applicantLocationRequirements through its first branch. Update the first
JobLocation branch to forbid jobLocationType with an appropriate never type,
while preserving its existing jobLocation requirement and optional
applicantLocationRequirements; keep TELECOMMUTE cases restricted to the second
branch.

In `@packages/schema-org/src/nodes/Organization/index.ts`:
- Around line 252-253: The Organization resolver must route all newly typed
relations through their appropriate nested-node resolvers. In
packages/schema-org/src/nodes/Organization/index.ts lines 252-253, pass
memberProgramResolver for isTierOf and use monetaryAmountResolver, an exported
unitPriceSpecificationResolver, or CreditCard for hasTierRequirement; in the
organizationResolver.resolve block at lines 222-233, resolve identifier with a
PropertyValue resolver and agentInteractionStatistic and interactionStatistic
with interactionCounterResolver.

In `@packages/schema-org/src/nodes/Person/index.ts`:
- Around line 23-30: Update personResolver.resolve to process Person’s
relation-like fields identifier, agentInteractionStatistic, and
interactionStatistic, rather than resolving only node.url. Reuse the established
relation resolvers and apply the appropriate default types for object values:
PropertyValue for identifier and InteractionCounter for both statistic fields,
while preserving explicitly provided `@type` values.

In `@packages/schema-org/src/nodes/Recipe/index.ts`:
- Around line 140-161: Update recipeResolver near the existing
recipeInstructions handling to explicitly resolve node.image with imageResolver
for array or object image relations, while preserving the generic graph pass for
string images. Ensure every RecipeSimple.image entry receives the same
URL/host/license/@id normalization as string images without overriding the
existing generic handling.

---

Outside diff comments:
In `@packages/schema-org/src/nodes/Image/index.ts`:
- Around line 10-19: Remove the orphaned JSDoc block in the ImageBase interface
that mentions duplicated contentUrl and url properties, leaving the caption
documentation directly above caption. Do not alter the caption description or
the ImageLocation definitions.

---

Nitpick comments:
In `@packages/schema-org/src/nodes/JobPosting/index.ts`:
- Around line 58-64: Remove the orphaned job-location JSDoc block immediately
before the directApply property in the JobPosting definition, preserving
directApply’s own documentation and declaration unchanged.

In `@packages/schema-org/src/nodes/Product/index.ts`:
- Around line 227-244: Remove the duplicate certificationRatingResolver and
update certificationResolver to use the exported ratingResolver from ../Rating
for node.certificationRating. Add the corresponding import alongside the
existing resolver imports, preserving the organization and URL resolution
behavior.

In `@packages/schema-org/src/nodes/Question/index.ts`:
- Line 60: Update the Question node’s video property to accept both VideoObject
and string values, matching Answer’s video type and the videoResolver behavior
that converts bare URLs into { url } objects.

In `@packages/schema-org/src/nodes/Video/index.ts`:
- Around line 135-139: Export interactionCounterResolver so other schema node
modules can reuse the shared InteractionCounter default resolver. Keep its
existing defineSchemaOrgResolver configuration and default '`@type`' unchanged,
and update consumers such as Person or Organization to import and use the
exported resolver instead of duplicating or skipping resolution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d4317cce-eac4-4d1a-96ba-7b0b8265f047

📥 Commits

Reviewing files that changed from the base of the PR and between 6b2c5b9 and 8eaa4f7.

📒 Files selected for processing (60)
  • docs/head/1.guides/build-plugins/3.minify-transform.md
  • docs/schema-org/2.guides/1.core-concepts/2.nodes.md
  • docs/schema-org/2.guides/1.core-concepts/3.google-search-fields.md
  • docs/schema-org/5.api/9.schema/course.md
  • docs/schema-org/5.api/9.schema/dataset.md
  • docs/schema-org/5.api/9.schema/image.md
  • docs/schema-org/5.api/9.schema/job-posting.md
  • docs/schema-org/5.api/9.schema/local-business.md
  • docs/schema-org/5.api/9.schema/organization.md
  • docs/schema-org/5.api/9.schema/product.md
  • docs/schema-org/5.api/9.schema/recipe.md
  • docs/schema-org/5.api/9.schema/software-app.md
  • docs/schema-org/5.api/9.schema/video.md
  • docs/schema-org/5.api/9.schema/webpage.md
  • packages/schema-org/src/core/identity.ts
  • packages/schema-org/src/core/index.ts
  • packages/schema-org/src/index.ts
  • packages/schema-org/src/nodes/AggregateRating/index.ts
  • packages/schema-org/src/nodes/Article/index.ts
  • packages/schema-org/src/nodes/Comment/index.ts
  • packages/schema-org/src/nodes/Course/index.ts
  • packages/schema-org/src/nodes/Dataset/index.test.ts
  • packages/schema-org/src/nodes/Dataset/index.ts
  • packages/schema-org/src/nodes/DiscussionForumPosting/index.ts
  • packages/schema-org/src/nodes/Event/index.test.ts
  • packages/schema-org/src/nodes/Event/index.ts
  • packages/schema-org/src/nodes/FoodEstablishment/index.test.ts
  • packages/schema-org/src/nodes/HowTo/HowToStep/index.ts
  • packages/schema-org/src/nodes/HowTo/index.ts
  • packages/schema-org/src/nodes/Image/index.test.ts
  • packages/schema-org/src/nodes/Image/index.ts
  • packages/schema-org/src/nodes/JobPosting/index.test.ts
  • packages/schema-org/src/nodes/JobPosting/index.ts
  • packages/schema-org/src/nodes/ListItem/index.ts
  • packages/schema-org/src/nodes/LocalBusiness/index.test.ts
  • packages/schema-org/src/nodes/LocalBusiness/index.ts
  • packages/schema-org/src/nodes/MonetaryAmount/index.ts
  • packages/schema-org/src/nodes/Offer/index.ts
  • packages/schema-org/src/nodes/OpeningHours/index.ts
  • packages/schema-org/src/nodes/Organization/index.test.ts
  • packages/schema-org/src/nodes/Organization/index.ts
  • packages/schema-org/src/nodes/Person/index.ts
  • packages/schema-org/src/nodes/Product/index.test.ts
  • packages/schema-org/src/nodes/Product/index.ts
  • packages/schema-org/src/nodes/Question/Answer/index.ts
  • packages/schema-org/src/nodes/Question/index.test.ts
  • packages/schema-org/src/nodes/Question/index.ts
  • packages/schema-org/src/nodes/Rating/index.ts
  • packages/schema-org/src/nodes/Recipe/index.test.ts
  • packages/schema-org/src/nodes/Recipe/index.ts
  • packages/schema-org/src/nodes/Review/index.ts
  • packages/schema-org/src/nodes/SoftwareApp/index.ts
  • packages/schema-org/src/nodes/VacationRental/index.ts
  • packages/schema-org/src/nodes/Video/index.test.ts
  • packages/schema-org/src/nodes/Video/index.ts
  • packages/schema-org/src/nodes/WebPage/index.test.ts
  • packages/schema-org/src/nodes/WebPage/index.ts
  • packages/schema-org/src/types.ts
  • packages/schema-org/test/public-types.test.ts
  • test/exports/schema-org.yaml

Comment thread docs/schema-org/5.api/9.schema/organization.md
Comment thread docs/schema-org/5.api/9.schema/product.md
Comment thread docs/schema-org/5.api/9.schema/product.md Outdated
Comment thread docs/schema-org/5.api/9.schema/recipe.md
Comment thread packages/schema-org/src/nodes/HowTo/HowToStep/index.ts
Comment thread packages/schema-org/src/nodes/JobPosting/index.ts
Comment thread packages/schema-org/src/nodes/Organization/index.ts Outdated
Comment thread packages/schema-org/src/nodes/Person/index.ts Outdated
Comment thread packages/schema-org/src/nodes/Recipe/index.ts
@harlan-zw

Copy link
Copy Markdown
Collaborator Author

Review follow-up: all actionable comments are addressed in 8190bc1. I kept certificationRatingResolver intentionally because certification scales are domain-specific, while ratingResolver injects 1 to 5 defaults and numeric shorthand. Reusing it would change certification meaning. The reported Product nested relations were already covered by existing resolver calls.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/schema-org/src/nodes/Product/index.ts (1)

144-144: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use Brand’s resolver for the public brand union.

ProductSimple.brand accepts Brand | Organization, but productResolver resolves any object without exact @type: 'Brand' through organizationResolver, which applies @type: 'Organization'. An untyped Brand-shaped value is therefore emitted as organization JSON-LD; route Brand inputs through brandResolver or make the input type discriminated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/schema-org/src/nodes/Product/index.ts` at line 144, Update
ProductSimple.brand and the productResolver path to preserve Brand inputs: route
brand values through brandResolver when they represent Brand, rather than
sending all non-exact-Brand objects to organizationResolver. Ensure the public
Brand | Organization union is discriminated or otherwise correctly identifies
Brand-shaped values so emitted JSON-LD uses `@type`: 'Brand'.
🧹 Nitpick comments (1)
packages/schema-org/src/nodes/Recipe/index.test.ts (1)

131-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the preserved itemListElement content.

The test inputs itemListElement but only verifies @type. Add an assertion for the expected resolved step contents so the test fails if the nested data is dropped.

Also applies to: 147-149

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/schema-org/src/nodes/Recipe/index.test.ts` around lines 131 - 134,
Add an assertion in the Recipe test covering the HowToStep entries to verify
that itemListElement preserves the expected “Shape the loaf.” content after
resolution. Apply the same assertion to the additional case referenced around
the later lines, while retaining the existing `@type` checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/schema-org/src/nodes/Product/index.ts`:
- Line 144: Update ProductSimple.brand and the productResolver path to preserve
Brand inputs: route brand values through brandResolver when they represent
Brand, rather than sending all non-exact-Brand objects to organizationResolver.
Ensure the public Brand | Organization union is discriminated or otherwise
correctly identifies Brand-shaped values so emitted JSON-LD uses `@type`: 'Brand'.

---

Nitpick comments:
In `@packages/schema-org/src/nodes/Recipe/index.test.ts`:
- Around line 131-134: Add an assertion in the Recipe test covering the
HowToStep entries to verify that itemListElement preserves the expected “Shape
the loaf.” content after resolution. Apply the same assertion to the additional
case referenced around the later lines, while retaining the existing `@type`
checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 58d90814-f556-4c6e-89b9-f4c69e676150

📥 Commits

Reviewing files that changed from the base of the PR and between 8159d64 and 8190bc1.

📒 Files selected for processing (19)
  • docs/schema-org/5.api/9.schema/organization.md
  • docs/schema-org/5.api/9.schema/product.md
  • docs/schema-org/5.api/9.schema/recipe.md
  • packages/schema-org/src/core/common.ts
  • packages/schema-org/src/nodes/Image/index.ts
  • packages/schema-org/src/nodes/JobPosting/index.ts
  • packages/schema-org/src/nodes/Offer/index.ts
  • packages/schema-org/src/nodes/Organization/index.test.ts
  • packages/schema-org/src/nodes/Organization/index.ts
  • packages/schema-org/src/nodes/Person/index.ts
  • packages/schema-org/src/nodes/Product/index.ts
  • packages/schema-org/src/nodes/Question/index.test.ts
  • packages/schema-org/src/nodes/Question/index.ts
  • packages/schema-org/src/nodes/Recipe/index.test.ts
  • packages/schema-org/src/nodes/Recipe/index.ts
  • packages/schema-org/src/nodes/Video/index.ts
  • packages/schema-org/src/nodes/WebPage/index.test.ts
  • packages/schema-org/test/public-types.test.ts
  • test/exports/schema-org.yaml
💤 Files with no reviewable changes (1)
  • packages/schema-org/src/nodes/Image/index.ts
🚧 Files skipped from review as they are similar to previous changes (14)
  • packages/schema-org/src/nodes/Organization/index.test.ts
  • packages/schema-org/src/nodes/Question/index.test.ts
  • test/exports/schema-org.yaml
  • packages/schema-org/src/nodes/WebPage/index.test.ts
  • packages/schema-org/test/public-types.test.ts
  • docs/schema-org/5.api/9.schema/recipe.md
  • packages/schema-org/src/nodes/Video/index.ts
  • docs/schema-org/5.api/9.schema/organization.md
  • docs/schema-org/5.api/9.schema/product.md
  • packages/schema-org/src/nodes/Question/index.ts
  • packages/schema-org/src/nodes/Organization/index.ts
  • packages/schema-org/src/nodes/Recipe/index.ts
  • packages/schema-org/src/nodes/Offer/index.ts
  • packages/schema-org/src/nodes/JobPosting/index.ts

@harlan-zw

Copy link
Copy Markdown
Collaborator Author

Follow-up review addressed in 8a5593a. Untyped Product brand objects now use brandResolver; explicit @type Organization values still use organizationResolver. Recipe tests now assert preserved nested instruction content.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/schema-org/src/nodes/Product/index.test.ts (1)

342-389: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert resolved values, not only nested @types.

This test would still pass if several supplied properties were silently dropped. Assert values for suggestedGender, age bounds/unit, category fields, certification metadata, productGroupID, membershipPointsEarned, and size fields in addition to their generated types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/schema-org/src/nodes/Product/index.test.ts` around lines 342 - 389,
Strengthen the Product test assertion around the existing product fixture to
verify resolved property values, not just generated `@type` fields. Add
expectations for suggestedGender, suggestedAge bounds and unit, category fields,
certification metadata, productGroupID, membershipPointsEarned, and size fields
while preserving the current type assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/schema-org/src/nodes/Product/index.test.ts`:
- Around line 342-389: Strengthen the Product test assertion around the existing
product fixture to verify resolved property values, not just generated `@type`
fields. Add expectations for suggestedGender, suggestedAge bounds and unit,
category fields, certification metadata, productGroupID, membershipPointsEarned,
and size fields while preserving the current type assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9abb13cf-b00e-4d06-b3e6-8abe52aa9059

📥 Commits

Reviewing files that changed from the base of the PR and between 8190bc1 and 8a5593a.

📒 Files selected for processing (4)
  • docs/schema-org/5.api/9.schema/product.md
  • packages/schema-org/src/nodes/Product/index.test.ts
  • packages/schema-org/src/nodes/Product/index.ts
  • packages/schema-org/src/nodes/Recipe/index.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/schema-org/src/nodes/Recipe/index.test.ts
  • docs/schema-org/5.api/9.schema/product.md
  • packages/schema-org/src/nodes/Product/index.ts

@harlan-zw

Copy link
Copy Markdown
Collaborator Author

Final test coverage nit addressed in a91d1b0. The Product merchant and variant regression now asserts every supplied Google field value alongside generated nested types.

@harlan-zw harlan-zw changed the title feat(schema-org)!: add Google rich result field parity fix(schema-org): missing Google rich result fields Jul 30, 2026
@harlan-zw
harlan-zw merged commit a58d4bf into main Jul 30, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant