diff --git a/src/app/(map)/map/layout.tsx b/src/app/(map)/map/layout.tsx index 044338b..7b2e190 100644 --- a/src/app/(map)/map/layout.tsx +++ b/src/app/(map)/map/layout.tsx @@ -1,3 +1,12 @@ +import type { Metadata } from "next"; + +// The map is a client component and syncs state (week/year) into the URL query +// string, so set an explicit canonical here to consolidate those variants onto +// the clean /map URL. +export const metadata: Metadata = { + alternates: { canonical: "https://packedplaces.com/map" }, +}; + export default function MapLayout({ children, }: Readonly<{ diff --git a/src/app/(marketing)/best-time/page.tsx b/src/app/(marketing)/best-time/page.tsx index 1fc6ff9..b543eca 100644 --- a/src/app/(marketing)/best-time/page.tsx +++ b/src/app/(marketing)/best-time/page.tsx @@ -12,6 +12,7 @@ export async function generateMetadata(): Promise { return { title: t("metaTitle"), description: t("metaDescription"), + alternates: { canonical: "https://packedplaces.com/best-time" }, }; } diff --git a/src/app/(marketing)/features/categories/page.tsx b/src/app/(marketing)/features/categories/page.tsx index 0d5a31e..5366744 100644 --- a/src/app/(marketing)/features/categories/page.tsx +++ b/src/app/(marketing)/features/categories/page.tsx @@ -3,6 +3,7 @@ import { getTranslations } from "next-intl/server"; import { FeaturePageShell } from "@/components/features/FeaturePageShell"; export const metadata: Metadata = { + alternates: { canonical: "https://packedplaces.com/features/categories" }, title: "11 Destination Categories — PackedPlaces.com", description: "Ski, beach, city, island, safari, desert, tropical, lake, nature, cultural, and mountain — each category has tuned peak and off-peak capacity.", diff --git a/src/app/(marketing)/features/crowdedness/page.tsx b/src/app/(marketing)/features/crowdedness/page.tsx index a0fe7cd..630072e 100644 --- a/src/app/(marketing)/features/crowdedness/page.tsx +++ b/src/app/(marketing)/features/crowdedness/page.tsx @@ -4,6 +4,7 @@ import { FeaturePageShell } from "@/components/features/FeaturePageShell"; import { crowdColor, crowdCssGradient } from "@/lib/crowd-palette"; export const metadata: Metadata = { + alternates: { canonical: "https://packedplaces.com/features/crowdedness" }, title: "Crowdedness, Not Traffic — PackedPlaces.com", description: "PackedPlaces measures congestion relative to capacity, not raw visitor numbers. A packed ski village scores higher than a busy metropolis.", diff --git a/src/app/(marketing)/features/events/page.tsx b/src/app/(marketing)/features/events/page.tsx index 6d576a3..e7f9205 100644 --- a/src/app/(marketing)/features/events/page.tsx +++ b/src/app/(marketing)/features/events/page.tsx @@ -4,6 +4,7 @@ import { FeaturePageShell } from "@/components/features/FeaturePageShell"; import { crowdColor } from "@/lib/crowd-palette"; export const metadata: Metadata = { + alternates: { canonical: "https://packedplaces.com/features/events" }, title: "50+ Major Events — PackedPlaces.com", description: "Olympics, FIFA World Cup, Oktoberfest, Carnival, Grand Slams, and more — major events that drive millions of visitors are tracked and shown on the map.", diff --git a/src/app/(marketing)/features/holiday-boost/page.tsx b/src/app/(marketing)/features/holiday-boost/page.tsx index 0f790f2..a3f8d70 100644 --- a/src/app/(marketing)/features/holiday-boost/page.tsx +++ b/src/app/(marketing)/features/holiday-boost/page.tsx @@ -4,6 +4,7 @@ import { FeaturePageShell } from "@/components/features/FeaturePageShell"; import { crowdColor } from "@/lib/crowd-palette"; export const metadata: Metadata = { + alternates: { canonical: "https://packedplaces.com/features/holiday-boost" }, title: "Holiday Boost Effect — PackedPlaces.com", description: "A 1.5x multiplier during school breaks captures the surge in travel demand when families hit the road.", diff --git a/src/app/(marketing)/features/holiday-providers/page.tsx b/src/app/(marketing)/features/holiday-providers/page.tsx index a9911cb..c08a423 100644 --- a/src/app/(marketing)/features/holiday-providers/page.tsx +++ b/src/app/(marketing)/features/holiday-providers/page.tsx @@ -4,6 +4,7 @@ import { FeaturePageShell } from "@/components/features/FeaturePageShell"; import { crowdColor } from "@/lib/crowd-palette"; export const metadata: Metadata = { + alternates: { canonical: "https://packedplaces.com/features/holiday-providers" }, title: "Multi-Provider Holidays — PackedPlaces.com", description: "Three cascading providers — manual, OpenHolidaysAPI, and date-holidays — ensure global school holiday coverage for 127 regions.", diff --git a/src/app/(marketing)/features/seasonality/page.tsx b/src/app/(marketing)/features/seasonality/page.tsx index 9e8c0fe..fe1cc02 100644 --- a/src/app/(marketing)/features/seasonality/page.tsx +++ b/src/app/(marketing)/features/seasonality/page.tsx @@ -4,6 +4,7 @@ import { FeaturePageShell } from "@/components/features/FeaturePageShell"; import { crowdColor } from "@/lib/crowd-palette"; export const metadata: Metadata = { + alternates: { canonical: "https://packedplaces.com/features/seasonality" }, title: "Weather-Aware Seasonality — PackedPlaces.com", description: "Each destination has month-by-month peak season data reflecting real-world weather conditions — monsoons, dry seasons, extreme heat, and polar winters.", diff --git a/src/app/(marketing)/features/timeline/page.tsx b/src/app/(marketing)/features/timeline/page.tsx index 68e9622..3455305 100644 --- a/src/app/(marketing)/features/timeline/page.tsx +++ b/src/app/(marketing)/features/timeline/page.tsx @@ -4,6 +4,7 @@ import { FeaturePageShell } from "@/components/features/FeaturePageShell"; import { crowdColor } from "@/lib/crowd-palette"; export const metadata: Metadata = { + alternates: { canonical: "https://packedplaces.com/features/timeline" }, title: "Week-by-Week Timeline — PackedPlaces.com", description: "Scrub or animate through every week of the year to see crowdedness shift in real time across 700+ destinations.", diff --git a/src/app/(marketing)/page.tsx b/src/app/(marketing)/page.tsx index 542553f..c6336b3 100644 --- a/src/app/(marketing)/page.tsx +++ b/src/app/(marketing)/page.tsx @@ -30,6 +30,7 @@ const webAppJsonLd = { export const metadata: Metadata = { title: "PackedPlaces.com — See How Crowded Travel Destinations Really Get", + alternates: { canonical: "https://packedplaces.com" }, }; /* Dot-grid world map — small circles placed at land positions (equirectangular, viewBox 0 0 100 50) */ diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index e6af017..db107f0 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,23 +1,17 @@ import type { MetadataRoute } from "next"; -import { locales } from "@/i18n/config"; import { allDestinationSlugs, hreflangSlugMap } from "@/lib/destinations"; const BASE_URL = "https://packedplaces.com"; -function langAlternates(path: string) { - const url = path ? `${BASE_URL}/${path}` : BASE_URL; - return { - languages: Object.fromEntries([ - ...locales.map((l) => [l, url]), - ["x-default", url], - ]), - }; -} - -/** hreflang cluster for a destination slug (German exonym slugs get their own de URL). */ +/** + * hreflang cluster for a destination slug. Only destinations with a German + * exonym slug have genuinely distinct per-language URLs; those get a de/en + * cluster. Everything else is served from a single URL in every language, so + * emitting same-URL alternates would signal nothing — return undefined instead. + */ function destinationAlternates(slug: string) { const map = hreflangSlugMap(slug); - if (!map) return langAlternates(`destination/${slug}`); + if (!map) return undefined; return { languages: Object.fromEntries( Object.entries(map).map(([lang, s]) => [lang, `${BASE_URL}/destination/${s}`]), @@ -39,12 +33,13 @@ const PATHS = [ ]; export default function sitemap(): MetadataRoute.Sitemap { + // Static pages are served from a single URL in every language (locale is + // negotiated, not path-prefixed), so they carry no hreflang alternates. const staticEntries = PATHS.map(({ path, changeFrequency, priority }) => ({ url: path ? `${BASE_URL}/${path}` : BASE_URL, lastModified: new Date(), changeFrequency, priority, - alternates: langAlternates(path), })); const destinationEntries = allDestinationSlugs().map((slug) => {