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
npm install @yamblog/core @yamblog/next1. Create the blog instance
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
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
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
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
import { createSitemapExport } from '@yamblog/next';import { blog } from '@/lib/blog';
export default createSitemapExport(blog);6. OG image
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:
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 sitemapcontent/posts/ markdown fileslib/blog.ts blog instanceCustom 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.