Concepts

The SEO Object

The SEO type is the single source of truth after createSEO:

SEO = {
  meta: {
    title: string           // required after normalize
    description?: string
    canonical?: string
    robots?: string
    alternates?: { languages?: Record<string,string>, canonical?: string }
    pagination?: { prev?: string, next?: string }
    verification?: { google?: string, bing?: string, ... }
  }
  openGraph?: { title?, description?, images[], type?, url?, siteName?, locale? }
  twitter?: { card?, title?, description?, image?, site?, creator? }
  schema?: JSONLD[]         // structured data nodes
}

The Pipeline

Partial<SEO> + SEOConfig
  → applyRules (optional, needs route string)
  → createSEO (normalize + fallbacks)
  → run plugins (beforeMerge / afterMerge)
  → SEO (canonical document)
  → Adapter.render(seo) → framework metadata | Helmet props | TagDescriptor[]

Merge Strategy

mergeSEO(parent, child, config?):

Field Strategy
meta.title Child overrides
meta.description Child overrides
meta.alternates.languages Deep merge (incl. x-default)
openGraph.images Replace array (child wins)
schema Concatenate; optional dedupeByIdAndType
All other scalars Child overrides

JSON-LD Serialization

All structured data goes through serializeJSONLD — never ad-hoc string concat. This prevents </script> injection and U+2028/U+2029 breaks.

import { serializeJSONLD, article } from "@better-seo/core"

// Safe for embedding in <script type="application/ld+json">
const json = serializeJSONLD(article({ headline: title, url }))

Config & Context

SEOConfig controls defaults:

type SEOConfig = {
  defaultTitle: string
  description?: string
  baseUrl?: string
  titleTemplate?: string     // "%s | Site Name"
  defaultRobots?: string
  schemaMerge?: "concat" | "dedupeByIdAndType"
  rules?: SEORule[]
  plugins?: SEOPlugin[]
}

For Edge/Workers/multi-tenant: use createSEOContext(explicitConfig) per request — no filesystem reads.

Adapter Contract

Every adapter maps SEO → framework output:

interface SEOAdapter<TOutput> {
  id: string
  toFramework(seo: SEO): TOutput
}

Next