fix(schema-org): missing Google rich result fields - #920
Conversation
📝 WalkthroughWalkthroughThe 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. ChangesSchema.org Google Search support
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
📦 Bundle Size
All bundles (14)
📦 Runtime Dependencies✅ No runtime dependency changes All packages (10)
Skipped optional dependencies (114)
Production dependencies only. Peer dependencies and Unhead workspace packages are excluded. Skipped optional dependencies are unavailable on the CI platform. ⚡ Performance (directional)
All benchmarks (25)
Baseline: main @ 6b2c5b9 · 2026-07-29 · gzipped is the headline size metric · perf is directional (shared-runner, gated) |
There was a problem hiding this comment.
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 winOrphaned JSDoc comment for the old
url/contentUrlduplication note.This comment block (documenting "contentUrl and url properties are intentionally duplicated") no longer documents anything — those fields were moved out into the new
ImageLocationunion below, leaving this comment floating directly above the unrelatedcaptiondoc 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
interactionCounterResolveris private to this file despiteInteractionCounterbeing a shared type.
InteractionCounteris declared once intypes.tsand is also used byPerson/Organization, but this resolver (which sets the default'@type': 'InteractionCounter') is a non-exportedconst, so other node modules can't reuse it and must either duplicate it or (as currently happens inPerson) 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 valueRemove the orphaned
jobLocationdoc block.The job-location properties moved into the
JobLocationunion, but the JSDoc describing them stayed behind and now sits directly abovedirectApply'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
videohere rejects string URLs, unlikeAnswerSimple.video.
videoResolvercasts a bare string into{ url }, andAnswertypes this asNodeRelations<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 winReuse the exported
ratingResolverinstead of a duplicatecertificationRatingResolver.
certificationRatingResolverduplicatesratingResolver(Rating/index.ts) but drops itscast(numeric shorthand) andbestRating/worstRatingdefaults, soCertification.certificationRatingoutput is inconsistent with every otherRatingusage 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
📒 Files selected for processing (60)
docs/head/1.guides/build-plugins/3.minify-transform.mddocs/schema-org/2.guides/1.core-concepts/2.nodes.mddocs/schema-org/2.guides/1.core-concepts/3.google-search-fields.mddocs/schema-org/5.api/9.schema/course.mddocs/schema-org/5.api/9.schema/dataset.mddocs/schema-org/5.api/9.schema/image.mddocs/schema-org/5.api/9.schema/job-posting.mddocs/schema-org/5.api/9.schema/local-business.mddocs/schema-org/5.api/9.schema/organization.mddocs/schema-org/5.api/9.schema/product.mddocs/schema-org/5.api/9.schema/recipe.mddocs/schema-org/5.api/9.schema/software-app.mddocs/schema-org/5.api/9.schema/video.mddocs/schema-org/5.api/9.schema/webpage.mdpackages/schema-org/src/core/identity.tspackages/schema-org/src/core/index.tspackages/schema-org/src/index.tspackages/schema-org/src/nodes/AggregateRating/index.tspackages/schema-org/src/nodes/Article/index.tspackages/schema-org/src/nodes/Comment/index.tspackages/schema-org/src/nodes/Course/index.tspackages/schema-org/src/nodes/Dataset/index.test.tspackages/schema-org/src/nodes/Dataset/index.tspackages/schema-org/src/nodes/DiscussionForumPosting/index.tspackages/schema-org/src/nodes/Event/index.test.tspackages/schema-org/src/nodes/Event/index.tspackages/schema-org/src/nodes/FoodEstablishment/index.test.tspackages/schema-org/src/nodes/HowTo/HowToStep/index.tspackages/schema-org/src/nodes/HowTo/index.tspackages/schema-org/src/nodes/Image/index.test.tspackages/schema-org/src/nodes/Image/index.tspackages/schema-org/src/nodes/JobPosting/index.test.tspackages/schema-org/src/nodes/JobPosting/index.tspackages/schema-org/src/nodes/ListItem/index.tspackages/schema-org/src/nodes/LocalBusiness/index.test.tspackages/schema-org/src/nodes/LocalBusiness/index.tspackages/schema-org/src/nodes/MonetaryAmount/index.tspackages/schema-org/src/nodes/Offer/index.tspackages/schema-org/src/nodes/OpeningHours/index.tspackages/schema-org/src/nodes/Organization/index.test.tspackages/schema-org/src/nodes/Organization/index.tspackages/schema-org/src/nodes/Person/index.tspackages/schema-org/src/nodes/Product/index.test.tspackages/schema-org/src/nodes/Product/index.tspackages/schema-org/src/nodes/Question/Answer/index.tspackages/schema-org/src/nodes/Question/index.test.tspackages/schema-org/src/nodes/Question/index.tspackages/schema-org/src/nodes/Rating/index.tspackages/schema-org/src/nodes/Recipe/index.test.tspackages/schema-org/src/nodes/Recipe/index.tspackages/schema-org/src/nodes/Review/index.tspackages/schema-org/src/nodes/SoftwareApp/index.tspackages/schema-org/src/nodes/VacationRental/index.tspackages/schema-org/src/nodes/Video/index.test.tspackages/schema-org/src/nodes/Video/index.tspackages/schema-org/src/nodes/WebPage/index.test.tspackages/schema-org/src/nodes/WebPage/index.tspackages/schema-org/src/types.tspackages/schema-org/test/public-types.test.tstest/exports/schema-org.yaml
|
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. |
There was a problem hiding this comment.
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 winUse
Brand’s resolver for the public brand union.
ProductSimple.brandacceptsBrand | Organization, butproductResolverresolves any object without exact@type: 'Brand'throughorganizationResolver, which applies@type: 'Organization'. An untyped Brand-shaped value is therefore emitted as organization JSON-LD; routeBrandinputs throughbrandResolveror 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 winAssert the preserved
itemListElementcontent.The test inputs
itemListElementbut 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
📒 Files selected for processing (19)
docs/schema-org/5.api/9.schema/organization.mddocs/schema-org/5.api/9.schema/product.mddocs/schema-org/5.api/9.schema/recipe.mdpackages/schema-org/src/core/common.tspackages/schema-org/src/nodes/Image/index.tspackages/schema-org/src/nodes/JobPosting/index.tspackages/schema-org/src/nodes/Offer/index.tspackages/schema-org/src/nodes/Organization/index.test.tspackages/schema-org/src/nodes/Organization/index.tspackages/schema-org/src/nodes/Person/index.tspackages/schema-org/src/nodes/Product/index.tspackages/schema-org/src/nodes/Question/index.test.tspackages/schema-org/src/nodes/Question/index.tspackages/schema-org/src/nodes/Recipe/index.test.tspackages/schema-org/src/nodes/Recipe/index.tspackages/schema-org/src/nodes/Video/index.tspackages/schema-org/src/nodes/WebPage/index.test.tspackages/schema-org/test/public-types.test.tstest/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
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/schema-org/src/nodes/Product/index.test.ts (1)
342-389: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAssert 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
📒 Files selected for processing (4)
docs/schema-org/5.api/9.schema/product.mdpackages/schema-org/src/nodes/Product/index.test.tspackages/schema-org/src/nodes/Product/index.tspackages/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
|
Final test coverage nit addressed in a91d1b0. The Product merchant and variant regression now asserts every supplied Google field value alongside generated nested types. |
🔗 Linked issue
Follow-up to #919 and #917.
❓ Type of 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.
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.namewithaggregateRatingorreview, anAggregateRatingcount, andapplicantLocationRequirementsfor remote jobs.✅ Verification
pnpm lintpnpm typecheckpnpm buildTZ=UTC pnpm exec vitest run, 1,924 passedpnpm lint:docspnpm --filter @unhead/schema-org test:attwSummary by CodeRabbit