Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/app/(map)/map/layout.tsx
Original file line number Diff line number Diff line change
@@ -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<{
Expand Down
1 change: 1 addition & 0 deletions src/app/(marketing)/best-time/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export async function generateMetadata(): Promise<Metadata> {
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: { canonical: "https://packedplaces.com/best-time" },
};
}

Expand Down
1 change: 1 addition & 0 deletions src/app/(marketing)/features/categories/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/app/(marketing)/features/crowdedness/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/app/(marketing)/features/events/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/app/(marketing)/features/holiday-boost/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/app/(marketing)/features/holiday-providers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/app/(marketing)/features/seasonality/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/app/(marketing)/features/timeline/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/app/(marketing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down
23 changes: 9 additions & 14 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -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}`]),
Expand All @@ -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) => {
Expand Down
Loading