SEO Utilities
SEO Utilities
Two helpers in src/lib/seo.ts centralize meta tags and structured data so every
public page emits consistent SEO markup without hand-rolling tag arrays.
getSeoMeta(params?)
Returns a TanStack Router head.meta array: title, description, Open Graph
(og:title, og:description, og:url, og:type, optional og:image), and Twitter
card tags. Every field falls back to config.seo defaults (defaultTitle,
defaultDescription, defaultOgImage, twitterHandle) and config.domainName, so a
bare getSeoMeta() is always valid.
import { getSeoMeta } from '@/lib/seo';
export const Route = createFileRoute('/_landing/pricing')({
head: () => ({
meta: getSeoMeta({
title: 'Pricing',
description: 'Simple plans that scale with you.',
url: `https://${config.domainName}/pricing`
})
}),
component: PricingPage
});
Params: title, description, url, image, type ('website' | 'article'),
noindex, all optional. og:image/twitter:image and twitter:site are emitted
only when a value exists, so unset config never produces empty tags.
Pass noindex: true for pages with no unique indexable content (a login/signup
form, for example), this keeps the page crawlable (noindex, follow, so internal
links are still discovered) but out of search results, instead of padding thin
functional UI with copy just to satisfy a word-count check. See
src/routes/_auth.login.tsx for the reference usage.
getJsonLd(params)
Returns a schema.org JSON-LD object for Article, WebSite, Organization, or
SoftwareApplication. Article adds datePublished + author (as a Person);
SoftwareApplication adds the category/OS/offer boilerplate Google expects for app
listings. Render it into a script tag in the route head using stringifyJsonLd, not
raw JSON.stringify, JSON.stringify doesn't escape <, so a value containing
</script> (e.g. a future blog post title) would prematurely close the script tag:
scripts: [
{
type: 'application/ld+json',
children: stringifyJsonLd(
getJsonLd({ type: 'Article', name: post.title, datePublished: post.date })
)
}
];
Where it's used
All _landing.* routes (index, pricing, features, blog index + $slug, tos,
privacy, changelog) and the compare/* pages. Blog post routes are the reference
for the Article JSON-LD + type: 'article' meta combination
(src/routes/_landing.blog.$slug.tsx).
Adding SEO to a new public page
- Add
head: () => ({ meta: getSeoMeta({ ... }) })to the route definition. - Pass an absolute
url(prefix withhttps://${config.domainName}). - Only pages that should rank need JSON-LD; skip it for utility pages.
- Defaults live in
config.tsunderseo:, change branding there, not per page.