1// app/layout.tsx
2import { Inter, JetBrains_Mono } from "next/font/google";
3
4const inter = Inter({
5 subsets: ["latin"],
6 display: "swap",
7 variable: "--font-sans",
8});
9
10const mono = JetBrains_Mono({
11 subsets: ["latin"],
12 display: "swap",
13 variable: "--font-mono",
14});
15
16export default function RootLayout({ children }: { children: React.ReactNode }) {
17 return (
18 <html lang="en" className={`${inter.variable} ${mono.variable}`}>
19 <body>{children}</body>
20 </html>
21 );
22}At build time, Next.js downloads the font files and serves them from your own domain — no Google CDN call from the user's browser. Faster, more private, no CLS.
1import localFont from "next/font/local";
2
3const display = localFont({
4 src: [
5 { path: "./fonts/Display-Regular.woff2", weight: "400" },
6 { path: "./fonts/Display-Bold.woff2", weight: "700" },
7 ],
8 variable: "--font-display",
9});1npm i lucide-react1import { Search, ArrowRight } from "lucide-react";
2
3<Search className="h-5 w-5 text-gray-500" />Tree-shakes per icon — you don't pay for the whole library.
1// app/layout.tsx
2import type { Metadata } from "next";
3
4export const metadata: Metadata = {
5 metadataBase: new URL("https://coachnest.dev"),
6 title: { default: "CoachNest", template: "%s — CoachNest" },
7 description: "A modern learning platform.",
8 openGraph: {
9 siteName: "CoachNest",
10 type: "website",
11 locale: "en_US",
12 },
13 twitter: {
14 card: "summary_large_image",
15 creator: "@coachnest",
16 },
17};Per-page override:
1// app/about/page.tsx
2export const metadata: Metadata = {
3 title: "About us", // → "About us — CoachNest" via the template
4 description: "Who we are.",
5};1// app/courses/[slug]/page.tsx
2export async function generateMetadata({ params }): Promise<Metadata> {
3 const { slug } = await params;
4 const course = await getCourse(slug);
5 if (!course) return { title: "Course not found" };
6
7 return {
8 title: course.title,
9 description: course.shortDesc,
10 openGraph: { images: [course.thumbnail] },
11 };
12}Drop opengraph-image.tsx in any segment:
1// app/courses/[slug]/opengraph-image.tsx
2import { ImageResponse } from "next/og";
3
4export const runtime = "edge";
5export const size = { width: 1200, height: 630 };
6export const contentType = "image/png";
7
8export default async function og({ params }: { params: { slug: string } }) {
9 const course = await getCourse(params.slug);
10 return new ImageResponse(
11 (
12 <div style={{ fontSize: 64, background: "#0a0a0a", color: "white", width: "100%", height: "100%", padding: 60 }}>
13 <p style={{ opacity: 0.7, fontSize: 28 }}>CoachNest</p>
14 <h1>{course?.title}</h1>
15 </div>
16 ),
17 { ...size },
18 );
19}A fresh OG image per course, rendered at the edge — beautifully scalable.
1// app/sitemap.ts
2export default async function sitemap() {
3 const courses = await db.course.findMany({ select: { slug: true, updatedAt: true } });
4 return [
5 { url: "https://coachnest.dev", lastModified: new Date(), priority: 1.0 },
6 ...courses.map((c) => ({
7 url: `https://coachnest.dev/courses/${c.slug}`,
8 lastModified: c.updatedAt,
9 priority: 0.7,
10 })),
11 ];
12}1// app/robots.ts
2export default function robots() {
3 return {
4 rules: [{ userAgent: "*", allow: "/", disallow: "/api/" }],
5 sitemap: "https://coachnest.dev/sitemap.xml",
6 };
7}Drop the files; Next.js routes /sitemap.xml and /robots.txt automatically.