An Astro integration for SEO meta tags and structured data. Wraps astro-seo internally and provides a simple, configuration-driven API.
- Automatic SEO tags — generates meta tags for Open Graph, Twitter, and more
- JSON-LD structured data — schema.org support for events, organizations, articles, and more
- Virtual module configuration — centralized setup via
seo.config.ts - Flexible image resolution — customize how images are transformed for meta tags
- Per-entry transforms — override SEO fields by content type
- Extra meta/link tags — add custom tags built from the resolved SEO data
npm install @thenewdynamic/astro-seoastro.config.mjs
import tndSeo from '@thenewdynamic/astro-seo'
export default defineConfig({
site: 'https://example.com',
integrations: [tndSeo()]
})seo.config.ts (in your project root)
import type { SeoUserConfig } from '@thenewdynamic/astro-seo'
export default {
defaults: {
image: '/og-image.png', // Required — default OG image
title: 'My Site',
description: 'A brief site description',
},
// Optional — customize image resolution (e.g., Sanity). May be sync or async.
resolveImage: async (image) => await getAsyncImageUrl(image),
// Optional — override SEO fields per content type
transformEntry: (entry) => ({ /* ... */ }),
// Optional — add custom meta/link tags from the resolved SEO data
extend: (entry, data) => ({ /* ... */ }),
// Optional — detect production mode (defaults to import.meta.env.PROD)
isProd: () => process.env.NODE_ENV === 'production',
} satisfies SeoUserConfigIn your layout
---
import SEO from '@thenewdynamic/astro-seo/SEO.astro'
---
<SEO entry={entry} />The package works with flexible entry shapes — you don't need all fields. Here's what matters:
{
title?: string // Page title
description?: string // Meta description
image?: string // OG image URL or object
url?: string // Canonical URL
}If these are missing, the package falls back to defaults config.
Rich structured data requires content-type-specific fields:
Events need:
{
timeStart: string // ISO date (e.g., "2025-06-15T10:00:00Z")
timeEnd?: string // ISO date
venue?: object // Venue details
}Articles need:
{
_updatedAt?: string // ISO date for modified time
authors?: Array<{ name: string; url?: string }>
}Organizations need:
{
// Set via defaults or transformEntry
}If your content model uses different field names, use transformEntry to map:
// seo.config.ts
export default {
defaults: { image: '/og-image.png' },
transformEntry: (entry) => {
// For events
if (entry._type === 'event') {
return {
title: entry.name, // Map 'name' → 'title'
description: entry.summary, // Map 'summary' → 'description'
timeStart: entry.startDate, // Map 'startDate' → 'timeStart'
timeEnd: entry.endDate, // Map 'endDate' → 'timeEnd'
image: entry.poster, // Map 'poster' → 'image'
}
}
// For articles
if (entry._type === 'article') {
return {
title: entry.headline,
description: entry.excerpt,
authors: entry.contributors?.map(c => ({ name: c.name, url: c.bio_url })),
}
}
return {}
}
}The package merges transformEntry output with your entry data, so you only need to override fields that differ.
transformEntry runs before SEO fields are resolved, so it can't see computed values like ogTitle or the final resolved image URL. If you need to add extra <meta> or <link> tags built from those resolved values, use extend instead. It's called with two arguments, and its return value is passed straight through to astro-seo's extend prop:
entry— the raw entry as passed to<SEO entry={entry} />, untouched by any resolution ortransformEntrymappingdata— the fully resolvedSeoData(same shape asgetData(entry)returns):ogTitle, resolvedimage,canonical, etc.
// seo.config.ts
export default {
defaults: { image: '/og-image.png' },
extend: (entry, data) => ({
meta: [
{ name: 'my:title', content: data.ogTitle },
],
link: [
{ rel: 'alternate', type: 'application/rss+xml', href: '/rss.xml' },
],
}),
}Any entry can include a seo object to override resolved SEO values at the entry level — useful for custom titles, canonical URLs, or marking a page private:
{
title: 'My Event',
// ...
seo: {
title: 'Custom page title', // overrides resolved title
description: 'Custom description',
image: { src: '/custom-og.png' },
canonical: 'https://example.com/canonical-url',
private: true, // exclude from search engines
}
}These values take precedence over both entry fields and defaults.
See ARCHITECTURE.md for detailed configuration options, API reference, and architecture notes.
- Make sure
mainis clean and up to date, and everything you want to release is merged. - Bump the version — this updates
package.jsonand creates a commit + git tag (vX.Y.Z):npm version patch # bug fixes, non-breaking changes npm version minor # new features or breaking changes (pre-1.0) npm version major # breaking changes (post-1.0)
- Push the commit and the tag:
git push --follow-tags
- Publish to npm (runs
prepublishOnly/tscautomatically):npm publish
- Create the GitHub release from the pushed tag, after the npm publish succeeds:
gh release create vX.Y.Z --generate-notes
Publish to npm before creating the GitHub release — that way a public release is never announced for a version that failed to publish.
MIT