TIP
This page mirrors packages/seo/README.md from the repository. Edit it at the source, not here.
@saasweave/seo
SEO helpers for TanStack Start route head() objects.
This package is intentionally small. It does not fetch data, inspect routes, or depend on packages/env. You pass it your site configuration and route-specific values, and it returns a TanStack Start-compatible head object.
Public API
import {
generateTanStackStartSeo,
type GenerateTanStackStartSeoParams,
type TanStackStartSeoAlternates,
type TanStackStartSeoSite
} from "@saasweave/seo";What It Generates
generateTanStackStartSeo(...) can generate:
titledescription- Open Graph tags
- Twitter card tags
- canonical links
- alternate locale links
robotsandgooglebotmeta tags- optional
application/ld+jsonscript tags - optional root-only document meta such as
charsetandviewport
Basic Setup
Define a typed site config in the consuming app:
import { type TanStackStartSeoSite } from "@saasweave/seo";
const site = {
applicationName: "Example App",
baseUrl: "https://example.com",
defaultDescription: "An example application using route-level SEO helpers.",
defaultImages: [
{
alt: "Example App",
height: 630,
type: "image/png",
url: "https://example.com/og/index.png",
width: 1200
}
],
defaultTitle: "Example App",
siteName: "Example App",
titleTemplate: "%s | Example App"
} satisfies TanStackStartSeoSite;Then wrap the package in app-specific helpers if you want to inject locale defaults:
import {
type GenerateTanStackStartSeoParams,
generateTanStackStartSeo,
type TanStackStartSeoAlternates
} from "@saasweave/seo";
type AppSeoOptions = Omit<GenerateTanStackStartSeoParams, "alternates" | "site"> & {
alternates?: Omit<TanStackStartSeoAlternates, "baseLocale" | "locales">;
};
export function generateAppSeo({ alternates, ...options }: AppSeoOptions) {
return generateTanStackStartSeo({
...options,
alternates: alternates
? {
...alternates,
baseLocale: appConfig.i18n.baseLocale,
locales: appConfig.i18n.locales
}
: undefined,
site
});
}Common Use Cases
Root Route
Use includeDocumentMeta: true only on the root route:
head: () =>
generateAppSeo({
includeDocumentMeta: true
});This adds charset, viewport, application-name, and x-dns-prefetch-control in addition to the normal SEO tags.
Static Page
head: ({ params }) =>
generateAppSeo({
alternates: {
canonicalPath: "/privacy-policy",
locale: params.locale
},
description: "Read the privacy policy for Example App.",
title: "Privacy Policy"
});Dynamic Page
head: ({ loaderData, params }) =>
generateAppSeo({
alternates: {
canonicalPath: `/articles/${params.slug}`,
locale: params.locale
},
description: loaderData.article.summary ?? `Read ${loaderData.article.title} on Example App.`,
images: loaderData.article.coverImage
? [
{
alt: loaderData.article.title,
url: loaderData.article.coverImage
}
]
: undefined,
openGraphType: "article",
title: loaderData.article.title
});Noindex Routes
Use this for auth flows, internal pages, dashboards, and search pages:
head: ({ params }) =>
generateAppSeo({
alternates: {
canonicalPath: "/account",
locale: params.locale
},
robots: {
follow: false,
index: false
},
title: "Account"
});Passing JSON-LD Scripts
The package does not generate schema objects for you, but it will include prebuilt JSON-LD script tags:
head: () =>
generateAppSeo({
scripts: [
{
type: "application/ld+json",
children: JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
name: "Example App"
})
}
]
});Title Template Behavior
titleTemplate applies to:
- document title
og:titletwitter:title
If you pass title: "Articles" and titleTemplate: "%s | Example App", the output title fields become Articles | Example App.
If you omit title, the package uses site.defaultTitle.
Limitations
- This package only builds route
head()objects. It does not configuresitemap.xml,robots.txt, prerendering, or SSR. - It does not inspect route params, locale state, loader data, or env. The consuming app must pass those in.
- It does not add favicon, sitemap, stylesheet, preload, or other non-SEO links for you. Keep those in the app shell or route.
includeDocumentMetashould only be used where you truly want root document tags. TanStack Start does not have Next.js-style metadata inheritance.- JSON-LD support is pass-through only. You must provide the final serialized payload yourself.
- Canonical and alternate links are generated from
canonicalPath,baseUrl, and locale config. If those inputs are wrong, the output URLs will also be wrong. - The package assumes one site-level SEO config per consuming app. If you have multiple brands or hosts, build that branching in the app wrapper.
Recommended Integration Pattern
In the consuming app, keep:
- one typed
siteobject in a shared SEO module - one
generateAppSeo(...)wrapper that injects locale defaults - root-only
includeDocumentMeta: truein the root route - route-specific
title,description,robots, andcanonicalPathin each route