SEO Head Tags
SEO Head Tags
Plugin Developer Agent Kit — documentation written for AI agents, readable by humans.
Components inject <title>, <meta>, <link> tags into the HTML <head> during SSR by returning headTags from phaseOutput().
Basic Usage
return phaseOutput(
{ title: product.name, price: product.price },
{ productId: product.id },
{
headTags: [
{ tag: 'title', children: `${product.name} | My Store` },
{ tag: 'meta', attrs: { name: 'description', content: product.description } },
{ tag: 'meta', attrs: { property: 'og:title', content: product.name } },
{ tag: 'meta', attrs: { property: 'og:description', content: product.description } },
{ tag: 'meta', attrs: { property: 'og:image', content: product.imageUrl } },
{ tag: 'link', attrs: { rel: 'canonical', href: canonicalUrl } },
],
},
);
HeadTag Type
interface HeadTag {
tag: string; // 'title', 'meta', 'link', etc.
attrs?: Record<string, string>; // HTML attributes
children?: string; // Text content (for <title>, <script>, etc.)
}
Common SEO Tags
// Page title
{ tag: 'title', children: 'Product Name | Store' }
// Meta description
{ tag: 'meta', attrs: { name: 'description', content: 'Product description here' } }
// Open Graph
{ tag: 'meta', attrs: { property: 'og:title', content: 'Product Name' } }
{ tag: 'meta', attrs: { property: 'og:description', content: 'Description' } }
{ tag: 'meta', attrs: { property: 'og:image', content: 'https://...' } }
{ tag: 'meta', attrs: { property: 'og:type', content: 'product' } }
// Twitter Card
{ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' } }
{ tag: 'meta', attrs: { name: 'twitter:title', content: 'Product Name' } }
// Canonical URL
{ tag: 'link', attrs: { rel: 'canonical', href: 'https://example.com/products/slug' } }
// JSON-LD structured data
{ tag: 'script', attrs: { type: 'application/ld+json' }, children: JSON.stringify(structuredData) }
Mapping Generic SEO Data
If the data source provides a generic structure (array of tags with type/props/children), map it:
const headTags = seoData.tags.map((tag) => ({
tag: tag.type,
attrs: Object.fromEntries(tag.props.map((p) => [p.key, p.value])),
children: tag.children,
}));
return phaseOutput(viewState, carryForward, { headTags });
Declaring Head Tags in plugin.yaml
If your component provides head tags dynamically via phaseOutput, declare them in plugin.yaml so the SEO validator knows not to warn about missing title/description on pages using your component:
name: my-plugin
contracts:
- name: product-page
contract: product-page.jay-contract
component: productPage
headTags:
- title
- meta:description
- link:canonical
Values: title, meta:<name> (e.g., meta:description, meta:og:title), link:<rel> (e.g., link:canonical).
Priority: Template Wins
When both the jay-html template and a component provide the same head tag, the template wins:
- Component
phaseOutput({ headTags })— defaults - Jay-html
<head>tags — highest priority, overrides component
This lets designers customize head content in the template while components provide sensible defaults.
The jay-html <head> supports {binding} syntax for dynamic values:
<head>
<title>{productPage.name} | My Store</title>
<meta name="description" content="{productPage.description}" />
<link rel="canonical" href="https://mystore.com/products/{productPage.slug}" />
</head>
Bindings are resolved against the merged ViewState at SSR time.
Phase Rules
- Return headTags from slow phase for build-time SEO data (product name, description)
- Return headTags from fast phase for per-request data (pricing, availability)
- Fast phase headTags replace slow phase entirely (no merge)
- Template head tags override both (if present)
- No interactive phase — head tags are SSR-only
Collision Rules
<title>— singleton, last-write-wins<meta name="X">— keyed byname, last-write-wins<meta property="X">— keyed byproperty, last-write-wins<link rel="canonical">— singleton, last-write-wins- Other tags — always included (no dedup)
- A warning is logged on collision between different components
Restrictions
- Head tags from components inside
forEachare ignored - The framework handles HTML escaping automatically
- Canonical URLs must be absolute (
https://...)
About this document
This page is part of the Jay Stack Agent Kit — documentation generated from the framework source and written primarily for AI agents. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation. Learn more about the Agent Kit →