From d91e16b6ef6774615b043f7924501f373b04067e Mon Sep 17 00:00:00 2001 From: arifal Date: Mon, 27 Apr 2026 02:14:08 +0700 Subject: [PATCH] feat(seo): add metadata, sitemap, robots, JSON-LD, dynamic OG image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Centralize brand/keyword config in lib/site.ts (slogan, 22 keywords). - Root layout: metadataBase, title template, OG/Twitter defaults, robots rules. - Per-page metadata: home, trips list (filter-aware), trip detail, profile (noindex). - Layout wrappers add metadata to client-component pages: login, register, create-trip. - Trip detail: generateMetadata + JSON-LD Event + BreadcrumbList (price, availability, rating). - Home page: JSON-LD Organization + WebSite + SearchAction (sitelink search). - app/sitemap.ts: dynamic sitemap pulling OPEN/FULL trips from Prisma. - app/robots.ts: disallow /api/, /profile, /create-trip; references sitemap. - app/trips/[id]/opengraph-image.tsx: dynamic 1200x630 OG image per trip with cover photo, title, mountain, date, price, brand badge. - Seeder: switch trip images from local SVG placeholders to real Unsplash CDN URLs. - Drop 18 obsolete seed SVGs from public/images/seed/. New env: NEXT_PUBLIC_SITE_URL (defaults to localhost:3000) — set to prod domain on deploy. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/create-trip/layout.tsx | 13 ++ app/layout.tsx | 48 +++++- app/login/layout.tsx | 13 ++ app/page.tsx | 46 ++++++ app/profile/page.tsx | 6 + app/register/layout.tsx | 12 ++ app/robots.ts | 16 ++ app/sitemap.ts | 43 ++++++ app/trips/[id]/opengraph-image.tsx | 227 ++++++++++++++++++++++++++++ app/trips/[id]/page.tsx | 134 ++++++++++++++++ app/trips/page.tsx | 20 +++ lib/site.ts | 39 +++++ prisma/seed.ts | 61 +++++--- public/images/seed/ciremai-1.svg | 13 -- public/images/seed/ciremai-2.svg | 13 -- public/images/seed/ciremai-3.svg | 13 -- public/images/seed/gede-1.svg | 13 -- public/images/seed/gede-2.svg | 13 -- public/images/seed/gede-3.svg | 13 -- public/images/seed/gede-4.svg | 13 -- public/images/seed/guntur-1.svg | 13 -- public/images/seed/guntur-2.svg | 13 -- public/images/seed/guntur-3.svg | 13 -- public/images/seed/malabar-1.svg | 13 -- public/images/seed/malabar-2.svg | 13 -- public/images/seed/malabar-3.svg | 13 -- public/images/seed/papandayan-1.svg | 13 -- public/images/seed/papandayan-2.svg | 13 -- public/images/seed/papandayan-3.svg | 13 -- public/images/seed/tangkuban-1.svg | 13 -- public/images/seed/tangkuban-2.svg | 13 -- 31 files changed, 657 insertions(+), 255 deletions(-) create mode 100644 app/create-trip/layout.tsx create mode 100644 app/login/layout.tsx create mode 100644 app/register/layout.tsx create mode 100644 app/robots.ts create mode 100644 app/sitemap.ts create mode 100644 app/trips/[id]/opengraph-image.tsx create mode 100644 lib/site.ts delete mode 100644 public/images/seed/ciremai-1.svg delete mode 100644 public/images/seed/ciremai-2.svg delete mode 100644 public/images/seed/ciremai-3.svg delete mode 100644 public/images/seed/gede-1.svg delete mode 100644 public/images/seed/gede-2.svg delete mode 100644 public/images/seed/gede-3.svg delete mode 100644 public/images/seed/gede-4.svg delete mode 100644 public/images/seed/guntur-1.svg delete mode 100644 public/images/seed/guntur-2.svg delete mode 100644 public/images/seed/guntur-3.svg delete mode 100644 public/images/seed/malabar-1.svg delete mode 100644 public/images/seed/malabar-2.svg delete mode 100644 public/images/seed/malabar-3.svg delete mode 100644 public/images/seed/papandayan-1.svg delete mode 100644 public/images/seed/papandayan-2.svg delete mode 100644 public/images/seed/papandayan-3.svg delete mode 100644 public/images/seed/tangkuban-1.svg delete mode 100644 public/images/seed/tangkuban-2.svg diff --git a/app/create-trip/layout.tsx b/app/create-trip/layout.tsx new file mode 100644 index 0000000..911b0e5 --- /dev/null +++ b/app/create-trip/layout.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Buat Open Trip", + description: + "Buat open trip pendakian gunung di SeTrip. Atur itinerary, harga, dan ajak pendaki lain ikut serta.", + alternates: { canonical: "/create-trip" }, + robots: { index: false, follow: false }, +}; + +export default function CreateTripLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/app/layout.tsx b/app/layout.tsx index 93c0a7d..bfb10f9 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import { SessionProvider } from "@/components/providers/session-provider"; import { Navbar } from "@/components/shared/navbar"; +import { siteConfig, siteUrl } from "@/lib/site"; import "./globals.css"; const geistSans = Geist({ @@ -15,9 +16,50 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "SeTrip", - description: - "Cari open trip pendakian gunung, gabung bareng, nikmati petualangan ke gunung-gunung Jawa Barat.", + metadataBase: new URL(siteUrl), + title: { + default: `${siteConfig.name} — Open Trip Pendakian Gunung`, + template: `%s · ${siteConfig.name}`, + }, + description: siteConfig.description, + applicationName: siteConfig.name, + keywords: [...siteConfig.keywords], + authors: [{ name: siteConfig.name }], + creator: siteConfig.name, + publisher: siteConfig.name, + alternates: { canonical: "/" }, + openGraph: { + type: "website", + locale: "id_ID", + url: "/", + siteName: siteConfig.name, + title: `${siteConfig.name} — Open Trip Pendakian Gunung`, + description: siteConfig.description, + images: [ + { + url: "/images/SeTrip.png", + width: 1200, + height: 630, + alt: `${siteConfig.name} — ${siteConfig.slogan}`, + }, + ], + }, + twitter: { + card: "summary_large_image", + title: `${siteConfig.name} — Open Trip Pendakian Gunung`, + description: siteConfig.description, + images: ["/images/SeTrip.png"], + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-image-preview": "large", + "max-snippet": -1, + }, + }, icons: { icon: "/SeTrip.ico", apple: "/images/SeTrip.png", diff --git a/app/login/layout.tsx b/app/login/layout.tsx new file mode 100644 index 0000000..85ab886 --- /dev/null +++ b/app/login/layout.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Masuk", + description: + "Masuk ke akun SeTrip untuk gabung open trip pendakian dan kelola perjalananmu.", + alternates: { canonical: "/login" }, + robots: { index: false, follow: true }, +}; + +export default function LoginLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/app/page.tsx b/app/page.tsx index 4a29a25..9eabe88 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,20 @@ +import type { Metadata } from "next"; import Link from "next/link"; import Image from "next/image"; import { tripService } from "@/server/services/trip.service"; import { TripCard } from "@/features/trip/components/trip-card"; +import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site"; + +export const metadata: Metadata = { + title: "Cari Open Trip Pendakian Gunung Bareng", + description: `${siteConfig.slogan} ${siteConfig.description}`, + alternates: { canonical: "/" }, + openGraph: { + title: `${siteConfig.name} — Open Trip Pendakian Gunung Bareng`, + description: siteConfig.slogan, + url: "/", + }, +}; export default async function HomePage() { const trips = await tripService.getOpenTrips(); @@ -25,8 +38,41 @@ export default async function HomePage() { .filter((t) => !shownIds.has(t.id) && t.price <= 300000) .slice(0, 3); + const orgJsonLd = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "Organization", + "@id": `${siteUrl}/#organization`, + name: siteConfig.name, + url: siteUrl, + logo: absoluteUrl("/images/SeTrip.png"), + slogan: siteConfig.slogan, + description: siteConfig.description, + }, + { + "@type": "WebSite", + "@id": `${siteUrl}/#website`, + url: siteUrl, + name: siteConfig.name, + description: siteConfig.description, + publisher: { "@id": `${siteUrl}/#organization` }, + inLanguage: "id-ID", + potentialAction: { + "@type": "SearchAction", + target: `${siteUrl}/trips?q={search_term_string}`, + "query-input": "required name=search_term_string", + }, + }, + ], + }; + return (
+