Skip to content

Add page-level SEO metadata support to @weaverse/hydrogen and @weaverse/schema#453

Merged
hta218 merged 3 commits into
mainfrom
feat/seo-page-metadata
May 22, 2026
Merged

Add page-level SEO metadata support to @weaverse/hydrogen and @weaverse/schema#453
hta218 merged 3 commits into
mainfrom
feat/seo-page-metadata

Conversation

@hieuht09
Copy link
Copy Markdown
Contributor

What

Adds page-level SEO support so themes can render meta tags from SEO data published by Weaverse Builder (the PageSeo sidecar on the public content payload).

Changes

@weaverse/schema — type-only contract

  • src/page-seo.ts (new): PageSEOData interface mirroring the builder's nested DTO, plus OpenGraphType and TwitterCardType unions.
  • Type-only mirror of builder/app/schemas/page-seo.ts (pageSeoSchema). The SDK trusts the builder payload and does not re-validate at runtime. Field names must stay in sync.
  • Re-exported from src/index.ts.

@weaverse/hydrogen — runtime

  • src/seo.ts (new):
    • formatMetaDescriptors(seo) — pure, synchronous, tree-shakeable. Converts PageSEOData → React Router MetaDescriptor[]. Maps title, description, keywords, canonicalUrl, openGraph.* (og:*), twitter.* (twitter:*). Empty/missing fields are filtered out so they don't override earlier SEO sources (e.g. Hydrogen getSeoMeta). robots is always emitted (defaults index,follow) for a deterministic last word on indexing.
    • getWeaverseSeoMeta(data) — null-safe accessor that pulls page.seo out of WeaverseLoaderData / HydrogenPageData and returns MetaDescriptor[].
  • src/types.ts: HydrogenPageData gains optional seo?: PageSEOData | null. null = page has no PageSeo row; undefined = pre-feature revision snapshot. Themes treat both as "no Weaverse override".
  • src/index.ts: re-exports ./seo.

Usage

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return getWeaverseSeoMeta(data?.weaverseData);
};

Notes

- Additive only  no breaking changes. +178 lines across 5 files.
- seo? is optional, so existing themes are unaffected until they opt in.

@claude
Copy link
Copy Markdown

claude Bot commented May 22, 2026

Claude finished @hieuht09's task in 2m 22s —— View job


Code Review: PR #453 — Page-level SEO metadata support

  • Gather context
  • Read changed files
  • Analyze for bugs, security, and best practices
  • Post review findings

Overall the implementation is clean, additive-only, and correct. No breaking changes, the logic is sound, and the JSDoc is useful. A few design choices are worth discussing.


packages/schema/src/page-seo.ts

Non-standard og:type value

OpenGraphType includes 'product' (line 38), which is not in the Open Graph Protocol spec. Facebook and LinkedIn will silently downgrade it to 'website' for rich previews. Since this mirrors the builder's schema it may be intentional, but a JSDoc note would help theme authors understand the limitation.

// Note: 'product' is a Shopify convention, not a standard OG type.
// Social scrapers may treat it as 'website'.
export type OpenGraphType = 'website' | 'article' | 'product' | ...

packages/hydrogen/src/seo.ts

1. robots is always emitted even when only title (or any field) is set

seo.ts:81-86 — when seo is present but seo.robots is undefined, the function still emits robots: index,follow. In a stacked meta setup like:

return [...getSeoMeta(shopifySeo), ...getWeaverseSeoMeta(weaverseData)]

The Weaverse robots tag appears last and overrides any noindex set by Shopify's SEO (e.g. on password pages or draft products). The PR intentionally describes this as "deterministic last word on indexing" which is a valid design choice — but themes that combine both sources need to be aware of the ordering contract. Consider adding a warning in the function JSDoc.

2. Missing og:url mapping

seo.ts:42-48canonicalUrl is mapped to <link rel="canonical"> but not to og:url. These are conventionally set to the same value and og:url is used by Facebook/LinkedIn to deduplicate shares pointing to the same page. This is a simple omission:

if (seo.canonicalUrl) {
  descriptors.push({ tagName: 'link', rel: 'canonical', href: seo.canonicalUrl })
  descriptors.push({ property: 'og:url', content: seo.canonicalUrl }) // add this
}

Fix this →

3. og:image requires an absolute URL

seo.ts:58-60 — Social crawlers (Facebook, Twitter, Slack) require og:image to be an absolute URL. A builder-provided relative URL would silently fail previews. Since the builder is the trusted source this is acceptable by design, but a comment would prevent confusion:

// og:image must be an absolute URL; builder is expected to provide one.
if (og.image) {
  descriptors.push({ property: 'og:image', content: og.image })
}

4. Fragile structural discriminant in getWeaverseSeoMeta

seo.ts:113-116 — the runtime check 'items' in data && 'id' in data to distinguish a bare HydrogenPageData from a WeaverseLoaderData wrapper works for current types, but it's relying on structural coincidence. If any future WeaverseLoaderData property gains items or id at the top level, this silently breaks. A named discriminant or a dedicated type guard on WeaverseLoaderData would be more robust:

// More explicit:
function isHydrogenPageData(data: unknown): data is HydrogenPageData {
  return typeof (data as any)?.items !== 'undefined' && typeof (data as any)?.id === 'string'
}

This is a low-risk concern today but worth tracking.


packages/hydrogen/src/types.ts

The seo?: PageSEOData | null addition to HydrogenPageData (line 223) is clean. The null / undefined distinction is clearly documented and sensible.


packages/schema/src/index.ts & packages/hydrogen/src/index.ts

Both re-exports are correct and in the right position. The type-only import in schema/index.ts means zero runtime overhead.


Summary

Severity Issue
Medium robots always emitted when any SEO object is present — could silently override noindex in stacked meta setups
Minor og:url not emitted from canonicalUrl
Minor 'product' is non-standard og:type — no JSDoc warning
Minor og:image absolute URL requirement is implicit / undocumented
Nit getWeaverseSeoMeta structural discriminant is fragile

The robots-always-wins behavior is the one most likely to surprise theme authors; the rest are low-impact quality improvements. The code is otherwise well-structured, the JSDoc is useful, and the additive-only approach is correct.

@hta218 hta218 merged commit 792de68 into main May 22, 2026
9 of 11 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.

2 participants