Skip to content

Next.js Guide

Recipe — Next.js App Router

Full integration of @yamblog/core + @yamblog/next in a Next.js 15+ App Router project. Examples use the async params / searchParams introduced in Next.js 15 — on Next.js 14, read them as plain objects instead of awaiting.

Install

Terminal window
npm install @yamblog/core @yamblog/next

1. Create the blog instance

lib/blog.ts
import { defineBlog } from '@yamblog/core';
export const blog = defineBlog('content/posts');

defineBlog auto-detects siteUrl from environment variables, checked in this order: SITE, PUBLIC_SITE_URL, NEXT_PUBLIC_SITE_URL, NEXT_PUBLIC_BASE_URL, VERCEL_URL. blog.siteUrl is then available everywhere — no separate env var wrangling across files.

2. Blog listing page

app/blog/page.tsx
import { BlogListPage } from '@yamblog/next';
import { blog } from '@/lib/blog';
export default async function BlogPage({
searchParams,
}: {
searchParams: Promise<{ q?: string | string[] }>;
}) {
const { q } = await searchParams;
const query = Array.isArray(q) ? q[0] : q; // repeated ?q= arrives as an array
const posts = query ? await blog.search(query) : await blog.getPosts();
return <BlogListPage posts={posts} query={query} />;
}

3. Post detail page

app/blog/[slug]/page.tsx
import {
BlogPostPage,
generatePostMetadata,
generateBlogJsonLd,
generateBreadcrumbJsonLd,
createStaticParams,
} from '@yamblog/next';
import { blog } from '@/lib/blog';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
return createStaticParams(blog);
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await blog.findPostBySlug(slug);
if (!post) return {};
return generatePostMetadata(post, { siteUrl: blog.siteUrl, basePath: blog.basePath, siteName: 'My Blog' });
}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await blog.findPostBySlug(slug);
if (!post) notFound();
const adjacent = await blog.getAdjacentPosts(slug);
const related = await blog.getRelatedPosts(slug);
const jsonLd = generateBlogJsonLd(post, { siteUrl: blog.siteUrl, basePath: blog.basePath });
const breadcrumbJsonLd = generateBreadcrumbJsonLd([
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' },
{ name: post.title, url: `/blog/${post.slug}` },
]);
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }} />
<BlogPostPage
post={post}
prev={adjacent.prev}
next={adjacent.next}
related={related}
/>
</>
);
}

4. RSS feed

app/feed.xml/route.ts
import { createRssHandler } from '@yamblog/next';
import { blog } from '@/lib/blog';
export const GET = createRssHandler(blog, {
title: 'My Blog',
description: 'Latest posts',
});

siteUrl is read from blog.siteUrl automatically.

5. Sitemap

app/sitemap.ts
import { createSitemapExport } from '@yamblog/next';
import { blog } from '@/lib/blog';
export default createSitemapExport(blog);

6. OG image

app/blog/[slug]/opengraph-image/route.ts
import { createOgImageHandler } from '@yamblog/next';
import { blog } from '@/lib/blog';
export const GET = createOgImageHandler(blog, { siteName: 'My Blog' });

7. remark plugins (optional)

BlogPostPage renders via react-markdown. To use a custom pipeline with @yamblog/remark plugins, use toHtml and render the result yourself:

app/blog/[slug]/page.tsx
import { toHtml, remarkToc, remarkEmbed } from '@yamblog/remark';
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await blog.findPostBySlug(slug);
if (!post) notFound();
const html = await toHtml(post.content, {
remarkPlugins: [remarkToc, remarkEmbed],
});
return (
<main>
<h1>{post.title}</h1>
<article className="prose" dangerouslySetInnerHTML={{ __html: html }} />
</main>
);
}

File layout

app/
blog/
page.tsx listing + search
[slug]/
page.tsx post detail + JSON-LD
opengraph-image/route.ts OG image
feed.xml/route.ts RSS
sitemap.ts sitemap
content/posts/ markdown files
lib/blog.ts blog instance

Custom fields

Extend the default schema to add typed custom frontmatter fields. next build runs tsc, so type errors in pages are caught automatically.

See the Custom Fields recipe for validation types, full examples, and type enforcement details.