diff --git a/.env.example b/.env.example index efef8c1..d9967c5 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# Public site origin used for SEO metadata, sitemap, and robots.txt. +BASE_URL=https://guide.sudloh.com + # Local-only demo mode serves sanitized fixtures and bypasses external services. # Keep this false in every deployed environment. BUZZ_DEMO_MODE=false diff --git a/app/layout.tsx b/app/layout.tsx index e2a20f3..5f9006f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import { Anuphan, Geist_Mono } from "next/font/google"; import { DeploymentUpdateNotifier } from "@/components/deployment-update-notifier"; import { PageProgress } from "@/components/page-progress"; import { Toaster } from "@/components/ui/sonner"; +import { siteUrl } from "@/lib/site-url"; import "./globals.css"; @@ -19,12 +20,13 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - metadataBase: new URL("https://guide.sudloh.com"), + metadataBase: new URL(siteUrl), title: { default: "Buzz Guide", template: "%s", }, description: "ไกด์ตัวละครจากช่องเกนชินไม่ใช่เกมมือถือ", + keywords: ["เกนชิน", "เกนชินไม่ใช่เกมมือถือ", "Buzz", "ไกด์", "สุดหล่อ"], }; export default function RootLayout({ children }: LayoutProps<"/">) { diff --git a/app/robots.ts b/app/robots.ts index 9398ddc..96aa6f5 100644 --- a/app/robots.ts +++ b/app/robots.ts @@ -1,5 +1,7 @@ import type { MetadataRoute } from "next"; +import { siteUrl } from "@/lib/site-url"; + export default function robots(): MetadataRoute.Robots { return { rules: { @@ -7,5 +9,6 @@ export default function robots(): MetadataRoute.Robots { allow: "/", disallow: ["/admin", "/api", "/register"], }, + sitemap: `${siteUrl}/sitemap.xml`, }; } diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..9f0bcb1 --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,17 @@ +import type { MetadataRoute } from "next"; +import { connection } from "next/server"; + +import { getCachedPublicGuideCards } from "@/lib/cache/public"; +import { siteUrl } from "@/lib/site-url"; + +export default async function sitemap(): Promise { + await connection(); + const guides = await getCachedPublicGuideCards(); + + return [ + { url: siteUrl }, + ...guides.map((guide) => ({ + url: `${siteUrl}/${encodeURIComponent(guide.slug)}`, + })), + ]; +} diff --git a/lib/site-url.ts b/lib/site-url.ts new file mode 100644 index 0000000..c39f5bd --- /dev/null +++ b/lib/site-url.ts @@ -0,0 +1,7 @@ +const configuredBaseUrl = process.env.BASE_URL?.trim(); + +if (!configuredBaseUrl) { + throw new Error("BASE_URL is required for site metadata and sitemaps."); +} + +export const siteUrl = new URL(configuredBaseUrl).origin;