|
| 1 | +/** |
| 2 | + * SEO helpers for TanStack Router's `head` option. |
| 3 | + * |
| 4 | + * TanStack Router's public `meta` type is typed as HTML `<meta>` attributes, |
| 5 | + * but its runtime also recognizes `{ title }` (renders `<title>`) and |
| 6 | + * `{ "script:ld+json": payload }` (renders a JSON-LD `<script>`). The entries |
| 7 | + * here produce runtime-valid tags; the final array is cast so the `head` |
| 8 | + * option accepts it. |
| 9 | + * @link https://tanstack.com/router/latest/docs/framework/react/guide/document-head-management |
| 10 | + */ |
| 11 | +import type { JSX } from "react"; |
| 12 | + |
| 13 | +const SITE_NAME = "シフトリ"; |
| 14 | +const SITE_URL = "https://shiftori.app"; |
| 15 | + |
| 16 | +type MetaList = NonNullable<JSX.IntrinsicElements["meta"]>[]; |
| 17 | + |
| 18 | +type MetaEntry = |
| 19 | + | { title: string } |
| 20 | + | { name: string; content: string } |
| 21 | + | { property: string; content: string } |
| 22 | + | { "script:ld+json": Record<string, unknown> }; |
| 23 | + |
| 24 | +type BuildMetaOptions = { |
| 25 | + title: string; |
| 26 | + description?: string; |
| 27 | + /** Set true for pages that should not be indexed (magic-link pages, authed pages). */ |
| 28 | + noindex?: boolean; |
| 29 | + /** Canonical path (e.g. "/terms"). Adds `og:url`. */ |
| 30 | + canonical?: string; |
| 31 | +}; |
| 32 | + |
| 33 | +/** |
| 34 | + * Build route meta tags for TanStack Router's `head` option. |
| 35 | + * - Appends the site name to the title (except when the title is the site name itself) |
| 36 | + * - Mirrors description to `og:description` |
| 37 | + * - Adds `robots: noindex, nofollow` when `noindex` is set |
| 38 | + */ |
| 39 | +export const buildMeta = ({ title, description, noindex, canonical }: BuildMetaOptions): MetaList => { |
| 40 | + const fullTitle = title === SITE_NAME ? title : `${title} | ${SITE_NAME}`; |
| 41 | + const entries: MetaEntry[] = [{ title: fullTitle }, { property: "og:title", content: fullTitle }]; |
| 42 | + |
| 43 | + if (description) { |
| 44 | + entries.push({ name: "description", content: description }); |
| 45 | + entries.push({ property: "og:description", content: description }); |
| 46 | + } |
| 47 | + |
| 48 | + if (canonical) { |
| 49 | + entries.push({ property: "og:url", content: `${SITE_URL}${canonical}` }); |
| 50 | + } |
| 51 | + |
| 52 | + if (noindex) { |
| 53 | + entries.push({ name: "robots", content: "noindex, nofollow" }); |
| 54 | + } |
| 55 | + |
| 56 | + return entries as unknown as MetaList; |
| 57 | +}; |
| 58 | + |
| 59 | +/** |
| 60 | + * Wrap a JSON-LD payload as a `head.meta` entry. Rendered as |
| 61 | + * `<script type="application/ld+json">...</script>` by TanStack Router. |
| 62 | + */ |
| 63 | +export const jsonLdMeta = (payload: Record<string, unknown>): MetaList => |
| 64 | + [{ "script:ld+json": payload }] as unknown as MetaList; |
0 commit comments