Skip to content

morinokami/tanstack-meta

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

68 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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
Screenshot Screenshot

Usage

Install tanstack-meta:

npm install tanstack-meta@latest

Import 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 provided
    • template: A template string where %s is replaced with the page title, or a function that receives the page title and returns the formatted title
  • baseUrl (optional): A string or URL object 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 createMetadataGenerator with 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" />

Migration from Next.js

If you're migrating from Next.js, tanstack-meta provides a familiar API that closely mirrors Next.js's Metadata API. Most metadata objects can be used as-is with minimal changes.

Basic Migration

Next.js:

// app/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "My Page",
  description: "Page description",
  openGraph: {
    title: "My Page",
    images: ["/og.png"],
  },
};

export default function Page() {
  return <div>...</div>;
}

tanstack-meta:

// routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { generateMetadata } from "tanstack-meta";

export const Route = createFileRoute("/")({
  component: Page,
  head: () =>
    generateMetadata({
      title: "My Page",
      description: "Page description",
      openGraph: {
        title: "My Page",
        images: ["/og.png"],
      },
    }),
});

function Page() {
  return <div>...</div>;
}

Title Template Migration

In Next.js, title templates are defined in the metadata object. In tanstack-meta, use createMetadataGenerator to configure templates.

Next.js:

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    template: "%s | My Site",
    default: "My Site",
  },
};

// app/about/page.tsx
export const metadata: Metadata = {
  title: "About",
  // Output: <title>About | My Site</title>
};

tanstack-meta:

// src/meta.ts
import { createMetadataGenerator } from "tanstack-meta";

export const generateMetadata = createMetadataGenerator({
  titleTemplate: {
    template: "%s | My Site",
    default: "My Site",
  },
});

// routes/about.tsx
import { generateMetadata } from "../meta";

export const Route = createFileRoute("/about")({
  head: () => generateMetadata({ title: "About" }),
  // Output: <title>About | My Site</title>
});

metadataBase Migration

Next.js's metadataBase is replaced with baseUrl in createMetadataGenerator.

Next.js:

// app/layout.tsx
export const metadata: Metadata = {
  metadataBase: new URL("https://example.com"),
  openGraph: {
    images: "/og.png", // Resolved to https://example.com/og.png
  },
};

tanstack-meta:

// src/meta.ts
import { createMetadataGenerator } from "tanstack-meta";

export const generateMetadata = createMetadataGenerator({
  baseUrl: "https://example.com",
});

// routes/index.tsx
generateMetadata({
  openGraph: {
    images: "/og.png", // Resolved to https://example.com/og.png
  },
});

Key Differences

Next.js tanstack-meta
Definition location metadata export in page.tsx/layout.tsx head function in route definition
Title template title.template in metadata object titleTemplate in createMetadataGenerator
Base URL metadataBase in metadata object baseUrl in createMetadataGenerator
Output format Automatically injected into <head> Returns { meta, links } for TanStack Router
Inheritance Automatic parent-child merging Manual composition via shared generator