tanstack-meta
v0.6.0
Published
A small library that helps you manage document heads type-safely in TanStack Router/Start
Readme
tanstack-meta
A small library that transforms structured, Next.js Metadata-like objects into metadata compatible with TanStack Router/Start, helping you manage document heads type-safely.
Why
TanStack Start is awesome, but there’s one thing I’m not fully satisfied with: its document head management, which I think still falls short of what Next.js offers.
Simply assigning strings to name or content is straightforward and easy to understand, but it lacks sufficient autocompletion and makes it hard to catch mistakes, which does not offer a good DX.
To improve this situation, I created a library that enables you to manage metadata as more structured objects and provides type-safe autocompletion.
Here's an example of how you can use tanstack-meta.
While it makes the code slightly longer in this case, it provides a more organized structure and enables richer IDE type completion:
| Without tanstack-meta | With tanstack-meta |
| ----------------------- | -------------------- |
|
|
|
Usage
Install tanstack-meta:
npm install tanstack-meta@latestImport generateMetadata from tanstack-meta and use it in your route's head function:
import { createFileRoute } from "@tanstack/react-router";
import { generateMetadata } from "tanstack-meta";
export const Route = createFileRoute("/")({
component: Home,
head: () => {
const { meta, links } = generateMetadata({
title: "TanStack Start App",
description: "An example app built with TanStack Start.",
});
return { meta, links };
},
});You can use it almost the same way as Next.js's generateMetadata function.
Title Template
If you want to use a title template like Next.js's title.template, use createMetadataGenerator to create a customized metadata generator:
import { createMetadataGenerator } from "tanstack-meta";
// Create a generator with title template
const generateMetadata = createMetadataGenerator({
titleTemplate: {
default: "Default Title", // Used when title is not provided
template: "%s | My Site", // %s is replaced with the page title
},
});
// In your routes:
generateMetadata({ title: "About" });
// Output: <title>About | My Site</title>
generateMetadata({ title: null });
// Output: <title>Default Title</title>
generateMetadata({});
// Output: <title>Default Title</title>You can also use a function for more complex title transformations:
const generateMetadata = createMetadataGenerator({
titleTemplate: {
default: "My Site",
template: (title) => `${title.toUpperCase()} — My Site`,
},
});
generateMetadata({ title: "about" });
// Output: <title>ABOUT — My Site</title>
// Conditional logic example
const generateMetadata = createMetadataGenerator({
titleTemplate: {
default: "My Site",
template: (title) =>
title.length > 50
? `${title.slice(0, 47)}... | My Site`
: `${title} | My Site`,
},
});To opt out of the title template on a specific page, use title.absolute:
generateMetadata({ title: { absolute: "Home" } });
// Output: <title>Home</title> (template is ignored)%s placeholders are all replaced. For example, template: "%s | %s | My Site" with title: "Docs" renders <title>Docs | Docs | My Site</title>.
Base URL
Similar to Next.js's metadataBase, you can use the baseUrl option to resolve relative URLs to absolute URLs for metadata fields like openGraph, twitter, and alternates:
import { createMetadataGenerator } from "tanstack-meta";
const generateMetadata = createMetadataGenerator({
baseUrl: "https://example.com",
});
// Relative URLs are resolved to absolute URLs
generateMetadata({
openGraph: {
images: "/og.png",
},
alternates: {
canonical: "/about",
},
});
// Output:
// <meta property="og:image" content="https://example.com/og.png" />
// <link rel="canonical" href="https://example.com/about" />You can also pass a URL object:
const generateMetadata = createMetadataGenerator({
baseUrl: new URL("https://example.com"),
});Absolute URLs are preserved unchanged:
generateMetadata({
openGraph: {
images: "https://cdn.example.com/og.png",
},
});
// Output: <meta property="og:image" content="https://cdn.example.com/og.png" />You can combine baseUrl with titleTemplate:
const generateMetadata = createMetadataGenerator({
titleTemplate: { default: "My Site", template: "%s | My Site" },
baseUrl: "https://example.com",
});
generateMetadata({
title: "About",
openGraph: {
images: "/og.png",
},
});
// Output:
// <title>About | My Site</title>
// <meta property="og:image" content="https://example.com/og.png" />Reference
generateMetadata
Generates the document metadata compatible with TanStack Router/Start's head function.
Parameters
An object containing the document metadata to be set.
Return Value
An object containing meta and links properties, which can be used as the return value of the head function.
createMetadataGenerator
Creates a customized metadata generator with options like title templates and base URL resolution.
Parameters
An options object with the following properties:
titleTemplate(optional): An object containing:default: The default title used when no title is providedtemplate: A template string where%sis replaced with the page title, or a function that receives the page title and returns the formatted title
baseUrl(optional): A string orURLobject used to resolve relative URLs to absolute URLs. Applies to:openGraph(images, audio, videos, url)twitter(images, players, app URLs)alternates(canonical, languages, media, types)
Return Value
A function that accepts metadata (with extended title support) and returns the same structure as generateMetadata.
Supported Metadata Fields
charSet- The character encoding of the document.
// Input { charSet: "utf-8" }<!-- Output --> <meta charset="utf-8" />
title- The document title.
// Input { title: "My Blog" }<!-- Output --> <title>My Blog</title>- When using
createMetadataGeneratorwith a title template, you can also use{ absolute: string }to bypass the template: // Input (with createMetadataGenerator) { title: { absolute: "Special Page" } }<!-- Output --> <title>Special Page</title>
description- The document description.
// Input { description: "My Blog Description" }<!-- Output --> <meta name="description" content="My Blog Description" />
applicationName- The application name.
// Input { applicationName: "My Blog" }<!-- Output --> <meta name="application-name" content="My Blog" />
authors- The authors of the document.
// Input { authors: [{ name: "TanStack Team", url: "https://tanstack.com" }] }<!-- Output --> <meta name="author" content="TanStack Team" /> <link rel="author" href="https://tanstack.com" />
generator- The generator used for the document.
// Input { generator: "TanStack Start" }<!-- Output --> <meta name="generator" content="TanStack Start" />
referrer- The referrer setting for the document.
// Input { referrer: "origin" }<!-- Output --> <meta name="referrer" content="origin" />
viewport- The viewport configuration for the document.
// Input { width: "device-width", initialScale: 1, themeColor: [ { media: "(prefers-color-scheme: dark)", color: "#000000" }, { media: "(prefers-color-scheme: light)", color: "#ffffff" } ], colorScheme: "dark" }<!-- Output --> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000" /> <meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" /> <meta name="color-scheme" content="dark" />
other- Arbitrary name/value pairs for additional metadata.
// Input { other: { custom: ["meta1", "meta2"] } }<!-- Output --> <meta name="custom" content="meta1" /> <meta name="custom" content="meta2" />
robots- The robots setting for the document.
// Input { robots: "index, follow" }<!-- Output --> <meta name="robots" content="index, follow" />// Input { robots: { index: true, follow: true } }<!-- Output --> <meta name="robots" content="index, follow" />
keywords- The keywords for the document.
// Input { keywords: "tanstack, react, blog" }<!-- Output --> <meta name="keywords" content="tanstack, react, blog" />// Input { keywords: ["react", "tanstack query"] }<!-- Output --> <meta name="keywords" content="react,tanstack query" />
pagination- The pagination link rel properties.
// Input { pagination: { previous: "https://example.com/items?page=1", next: "https://example.com/items?page=3" } }<!-- Output --> <link rel="prev" href="https://example.com/items?page=1" /> <link rel="next" href="https://example.com/items?page=3" />
openGraph- The Open Graph metadata for the document.
// Input { openGraph: { type: "website", url: "https://example.com", title: "My Website", description: "My Website Description", siteName: "My Website", images: [{ url: "https://example.com/og.png" }] } }<!-- Output --> <meta property="og:title" content="My Website" /> <meta property="og:description" content="My Website Description" /> <meta property="og:url" content="https://example.com" /> <meta property="og:site_name" content="My Website" /> <meta property="og:image" content="https://example.com/og.png" /> <meta property="og:type" content="website" />
twitter- The Twitter metadata for the document.
// Input { twitter: { card: "summary_large_image", site: "@site", creator: "@creator", images: "https://example.com/og.png" } }<!-- Output --> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:site" content="@site" /> <meta name="twitter:creator" content="@creator" /> <meta name="twitter:image" content="https://example.com/og.png" />
facebook- The Facebook metadata for the document.
// Input { facebook: { appId: "12345678" } }<!-- Output --> <meta property="fb:app_id" content="12345678" />// Input { facebook: { admins: ["12345678"] } }<!-- Output --> <meta property="fb:admins" content="12345678" />
pinterest- The Pinterest metadata for the document to choose whether opt out of rich pin data.
// Input { pinterest: { richPin: true } }<!-- Output --> <meta name="pinterest-rich-pin" content="true" />
manifest- The web application manifest for the document.
// Input { manifest: "https://example.com/manifest.json" }<!-- Output --> <link rel="manifest" href="https://example.com/manifest.json" />
icons- The icons for the document.
// Input { icons: "https://example.com/icon.png" }<!-- Output --> <link rel="icon" href="https://example.com/icon.png" />// Input { icons: { icon: "https://example.com/icon.png", apple: "https://example.com/apple-icon.png" } }<!-- Output --> <link rel="icon" href="https://example.com/icon.png" /> <link rel="apple-touch-icon" href="https://example.com/apple-icon.png" />
appleWebApp- The Apple web app metadata for the document.
// Input { appleWebApp: { capable: true, title: "My Website", statusBarStyle: "black-translucent" } }<!-- Output --> <meta name="mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-title" content="My Website" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
appLinks- The Facebook AppLinks metadata for the document.
// Input { appLinks: { ios: { appStoreId: "123456789", url: "https://example.com" }, android: { packageName: "com.example", url: "https://example.com" } } }<!-- Output --> <meta property="al:ios:app_store_id" content="123456789" /> <meta property="al:ios:url" content="https://example.com" /> <meta property="al:android:package" content="com.example" /> <meta property="al:android:url" content="https://example.com" />
itunes- The metadata for the iTunes App.
// Input { itunes: { app: { id: "123456789", affiliateData: "123456789", appArguments: "123456789" } } }<!-- Output --> <meta name="apple-itunes-app" content="app-id=123456789, affiliate-data=123456789, app-arguments=123456789" />
alternates- The canonical and alternate URLs for the document.
// Input { alternates: { canonical: "https://example.com", languages: { "en-US": "https://example.com/en-US" } } }<!-- Output --> <link rel="canonical" href="https://example.com" /> <link rel="alternate" hreflang="en-US" href="https://example.com/en-US" />
verification- The common verification tokens for the document.
// Input { verification: { google: "1234567890", yandex: "1234567890", "me": "1234567890" } }<!-- Output --> <meta name="google-site-verification" content="1234567890" /> <meta name="yandex-verification" content="1234567890" /> <meta name="me" content="1234567890" />
creator- The creator of the document.
// Input { creator: "TanStack Team" }<!-- Output --> <meta name="creator" content="TanStack Team" />
publisher- The publisher of the document.
// Input { publisher: "TanStack" }<!-- Output --> <meta name="publisher" content="TanStack" />
abstract- The brief description of the document.
// Input { abstract: "My Website Description" }<!-- Output --> <meta name="abstract" content="My Website Description" />
archives- The archives link rel property.
// Input { archives: "https://example.com/archives" }<!-- Output --> <link rel="archives" href="https://example.com/archives" />
assets- The assets link rel property.
// Input { assets: "https://example.com/assets" }<!-- Output --> <link rel="assets" href="https://example.com/assets" />
bookmarks- The bookmarks link rel property.
// Input { bookmarks: "https://example.com/bookmarks" }<!-- Output --> <link rel="bookmarks" href="https://example.com/bookmarks" />
category- The category meta name property.
// Input { category: "My Category" }<!-- Output --> <meta name="category" content="My Category" />
classification- The classification meta name property.
// Input { classification: "My Classification" }<!-- Output --> <meta name="classification" content="My Classification" />
formatDetection- Indicates whether devices should interpret certain formats (such as telephone numbers) as actionable links.
// Input { formatDetection: { telephone: false } }<!-- Output --> <meta name="format-detection" content="telephone=no" />
