From c52973b81cc74b7bf9e1930f7decac9a0afa42f3 Mon Sep 17 00:00:00 2001 From: viet456 Date: Sun, 20 Sep 2026 16:49:49 -0700 Subject: [PATCH] fix(seo): Overhaul metadata, crawl directives, and LLM discoverability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improves site seo. - Add metadataBase to root layout to resolve relative canonical/OG URLs to absolute https://www.cardledger.io paths, fixing incomplete hreflang and social preview links reported by crawlers - Add generateMetadata to /cards/[cardId] with dynamic title, description, canonical URL, OpenGraph, and Twitter card tags so each card page has unique, crawlable metadata instead of falling back to the root default - Add canonical alternates to /sets/[setId] generateMetadata to prevent duplicate content issues across paginated or parameterized set URLs - Fix sitemap baseUrl from cardledger.io to www.cardledger.io to match the canonical domain used everywhere else, avoiding redirect hops during sitemap fetch - Migrate public/robots.txt to src/app/robots.ts using the Next.js MetadataRoute.Robots convention — keeps crawl rules typed, colocated with other metadata files, and version-controlled alongside the app - Add public/llms.txt following the llmstxt.org standard to give AI agents a structured overview of the site's purpose, key routes, available data, and technical context --- public/llms.txt | 29 +++++++++++++++++++++ public/robots.txt | 24 ------------------ src/app/cards/[cardId]/page.tsx | 45 ++++++++++++++++++++++++++++++--- src/app/layout.tsx | 1 + src/app/robots.ts | 39 ++++++++++++++++++++++++++++ src/app/sets/[setId]/page.tsx | 5 +++- src/app/sitemap.ts | 2 +- 7 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 public/llms.txt delete mode 100644 public/robots.txt create mode 100644 src/app/robots.ts diff --git a/public/llms.txt b/public/llms.txt new file mode 100644 index 0000000..d06ec9f --- /dev/null +++ b/public/llms.txt @@ -0,0 +1,29 @@ +# CardLedger + +> A local-first, high-performance Pokémon TCG catalog and portfolio manager. Browse every English-language Pokémon card, track market prices, and build a personal collection portfolio — all with instant, client-side search and offline support. + +## Key pages + +- [Home](https://www.cardledger.io/): Search across the full Pokémon TCG catalog with instant fuzzy results +- [Sets](https://www.cardledger.io/sets): Browse all Pokémon TCG sets with release dates and card counts +- [Cards](https://www.cardledger.io/cards): Individual card detail pages with pricing history, market stats, and set info +- [About](https://www.cardledger.io/about): Information about the project, data sources, and features + +## Data available + +- Complete English Pokémon TCG card catalog with images +- TCG set listings with release dates, symbols, and card counts +- Market pricing data sourced from TCGPlayer +- Historical price charts per card +- Card details: HP, types, attacks, abilities, weaknesses, resistances, retreat cost, rarity, and illustrator + +## API + +All routes under `/api/` are internal and not intended for public consumption. + +## Technical notes + +- Built with Next.js (App Router), React, Prisma, and PostgreSQL +- Uses a service worker for offline-first caching of visited pages +- Card search powered by client-side fuzzy matching for instant results +- Images served from a dedicated CDN at assets.cardledger.io \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index 352ac88..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,24 +0,0 @@ -User-agent: * -Disallow: /api/ - -# Block known aggressive commercial scrapers -User-agent: AhrefsBot -Disallow: / - -User-agent: SemrushBot -Disallow: / - -User-agent: MJ12bot -Disallow: / - -User-agent: DotBot -Disallow: / - -User-agent: PetalBot -Disallow: / - -User-agent: barkrowler -Disallow: / - -# Point search engines directly to dynamic catalog -Sitemap: https://cardledger.io/sitemap.xml \ No newline at end of file diff --git a/src/app/cards/[cardId]/page.tsx b/src/app/cards/[cardId]/page.tsx index 6c3442e..8710d3a 100644 --- a/src/app/cards/[cardId]/page.tsx +++ b/src/app/cards/[cardId]/page.tsx @@ -5,11 +5,48 @@ import { CardBreadcrumbs } from './CardBreadcrumbs'; import { ClientCachedBreadcrumbFallback } from './ClientCachedBreadcrumbFallback'; import { ClientCachedDetailsFallback } from './ClientCachedDetailsFallback'; import { PriceHero } from '@/src/components/cards/PriceHero'; +import { Metadata } from 'next'; +import { getCachedCardData } from './data'; -// src/app/cards/[cardId]/page.tsx -// Thin server shell — no data fetching here. -// All card data comes from client-side zustand stores (hydrated from IndexedDB/R2) -// for instant navigation with zero loading skeletons. +export async function generateMetadata({ + params +}: { + params: Promise<{ cardId: string }>; +}): Promise { + const { cardId } = await params; + const card = await getCachedCardData(cardId); + + if (!card) { + return { + title: 'Card Not Found | CardLedger', + description: 'The requested card could not be found.' + }; + } + + const cardName = card.n; + const cardNumber = card.num; + const setName = card.set.name; + const title = `${cardName} #${cardNumber} (${setName}) | CardLedger`; + const description = `View ${cardName} #${cardNumber} from the ${setName} set. Track prices, check market trends, and add to your Pokémon TCG collection on CardLedger.`; + + return { + title, + description, + alternates: { + canonical: `/cards/${cardId}` + }, + openGraph: { + title, + description, + type: 'website' + }, + twitter: { + card: 'summary', + title, + description + } + }; +} export default async function SingleCardPage({ params, searchParams }: { params: Promise<{ cardId: string }>; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5f894c0..6f92b71 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -25,6 +25,7 @@ const APP_TITLE_TEMPLATE = '%s | CardLedger'; const APP_DESCRIPTION = 'A local-first, high-performance Pokémon TCG catalog and portfolio manager.'; export const metadata: Metadata = { + metadataBase: new URL('https://www.cardledger.io'), applicationName: APP_NAME, title: { default: APP_DEFAULT_TITLE, diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 0000000..d22345a --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,39 @@ +import type { MetadataRoute } from 'next' + +export default function robots(): MetadataRoute.Robots { + return { + rules: [ + { + userAgent: '*', + allow: '/', + disallow: '/api/', + }, + // Block known aggressive commercial scrapers + { + userAgent: 'AhrefsBot', + disallow: '/', + }, + { + userAgent: 'SemrushBot', + disallow: '/', + }, + { + userAgent: 'MJ12bot', + disallow: '/', + }, + { + userAgent: 'DotBot', + disallow: '/', + }, + { + userAgent: 'PetalBot', + disallow: '/', + }, + { + userAgent: 'barkrowler', + disallow: '/', + }, + ], + sitemap: 'https://www.cardledger.io/sitemap.xml', + } +} \ No newline at end of file diff --git a/src/app/sets/[setId]/page.tsx b/src/app/sets/[setId]/page.tsx index 0dc16d5..b3f9af6 100644 --- a/src/app/sets/[setId]/page.tsx +++ b/src/app/sets/[setId]/page.tsx @@ -30,7 +30,10 @@ export async function generateMetadata({ return { title: `${set.name} | CardLedger`, - description: `Browse all ${set.printedTotal} cards from the ${set.name} set.` + description: `Browse all ${set.printedTotal} cards from the ${set.name} set.`, + alternates: { + canonical: `/sets/${setId}` + } }; } diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 2d00d9a..e944aca 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -2,7 +2,7 @@ import { MetadataRoute } from 'next' import { prisma } from '@/src/lib/prisma'; export default async function sitemap(): Promise { - const baseUrl = 'https://cardledger.io' + const baseUrl = 'https://www.cardledger.io' const cards = await prisma.card.findMany({ select: { id: true, releaseDate: true }