Labels.io
Agent skills

Add SEO

Adds Open Graph tags, Twitter cards, a sitemap, robots.txt, and favicons. Runs once; updates apply to every route.

This kit already ships SEO

app/lib/seo.ts, the per-page meta/OG/Twitter/JSON-LD, and the robots.txt / sitemap.xml / llms.txt generator (scripts/generate-seo.mjs) are already wired. In most cases you only edit SITE_URL in app/lib/routes-manifest.ts and BRAND / BRAND_DESCRIPTION in app/lib/brand.ts, then the three files regenerate on pnpm dev / pnpm build (or pnpm gen:seo). The sections below explain each piece and how to extend it — see SEO & LLMO for the everyday workflow.

1. Open Graph + Twitter meta (shared helper)

The kit ships app/lib/seo.ts. A from-scratch version looks like:

type SeoInput = {
  title: string
  description: string
  path?: string
  image?: string
}

const SITE_NAME = 'Your site name'
const SITE_URL = import.meta.env.VITE_SITE_URL ?? 'http://127.0.0.1:3000'

export function seo({ title, description, path = '/', image = '/og-image.png' }: SeoInput) {
  const url = `${SITE_URL}${path}`
  return [
    { title: `${title} — ${SITE_NAME}` },
    { name: 'description', content: description },
    { property: 'og:title', content: title },
    { property: 'og:description', content: description },
    { property: 'og:url', content: url },
    { property: 'og:type', content: 'website' },
    { property: 'og:image', content: image },
    { property: 'og:site_name', content: SITE_NAME },
    { name: 'twitter:card', content: 'summary_large_image' },
    { name: 'twitter:title', content: title },
    { name: 'twitter:description', content: description },
    { name: 'twitter:image', content: image },
  ] as const
}

Update each route's head() to use it:

import { seo } from '~/lib/seo'

head: () => ({
  meta: seo({
    title: 'About',
    description: 'Learn who is behind this and what we set out to do.',
    path: '/about',
  }),
}),

2. robots.txt, sitemap.xml, llms.txt (already generated)

You do not hand-write these — the kit generates all three as real static files in public/ via scripts/generate-seo.mjs, from the publicRoutes list and SITE_URL in app/lib/routes-manifest.ts (plus BRAND / BRAND_DESCRIPTION for llms.txt). The dev and build scripts run the generator first, so the files stay in sync; run it on demand with:

pnpm gen:seo

To change what's crawlable, edit publicRoutes in app/lib/routes-manifest.ts and regenerate — never edit public/robots.txt / public/sitemap.xml / public/llms.txt by hand (they're overwritten on the next run). Set the real domain by changing SITE_URL in the same file.

4. Favicons

Add to public/:

  • favicon.ico (32x32)
  • apple-touch-icon.png (180x180)
  • icon.svg (optional, for modern browsers)

Add to <head> via links in app/routes/__root.tsx:

links: [
  { rel: 'stylesheet', href: appCss },
  { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
  { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' },
],

5. Canonical + lang

  • <html lang="en"> already set in __root.tsx; change to the correct language if needed.
  • Add a canonical link per route: { rel: 'canonical', href: url } inside seo().

6. Verify

  • Open each route. View source: titles, meta, OG tags present.
  • Run Lighthouse SEO audit in Chrome devtools: should score 95+.
  • Twitter card validator and Facebook debugger: paste URLs to confirm OG renders.

On this page