Next.js Blog with Markdown: Categories, Reading Time, and RSS Feed

Table of Contents
- Introduction: Why Markdown + Next.js for a Blog?
- Project Setup and Content Model
- Parsing Markdown and Frontmatter with gray-matter
- Static Site Generation: getStaticProps and getStaticPaths
- Rendering Markdown with remark and rehype
- Adding Categories and Category Pages
- Generate an RSS Feed for Syndication
- Advanced: Reading Time Algorithms and MDX Considerations
- SEO: Meta Tags, Open Graph, and Sitemap
- Case Study: Sample Project Structure and Deployment
- Conclusion, Further Reading, and Next Steps
- About Prateeksha Web Design
Introduction: Why Markdown + Next.js for a Blog?
Markdown-driven blogs remain one of the simplest, fastest and most portable ways to publish content on the web. Combining Markdown with Next.js gives you the best of two worlds: the simplicity of authoring in plain-text files and the performance, routing and SEO advantages of a modern React framework.
What you'll build across this series
- A file-based content system where each post is a Markdown file with YAML frontmatter.
- Category support surfaced on index, category pages and individual posts.
- An estimated reading-time value per post, calculated at build time.
- An RSS feed that publishes your latest posts for feed readers and subscribers.
Why choose Markdown for a blog
- Author-friendly: Write posts in any editor; Git-based workflows and editors like VS Code make collaboration simple.
- Portable: Markdown files can be moved between tools, backed up in Git, or converted to other formats later.
- Fast builds: Pure Markdown (no client-side runtime for MDX components) usually yields smaller bundles and faster page loads.
When to choose MDX instead
MDX lets you embed React components inside Markdown. Use MDX when you need interactive widgets or components in the body of posts (charts, live examples, complex embeds). Opt for plain Markdown when posts are mostly text and static content — it simplifies rendering and reduces client-side JavaScript.
FactPlain Markdown often produces smaller client bundles and faster initial loads than MDX because the Markdown rendering can happen entirely at build time.Static Site Generation (SSG) benefits with Next.js
- Performance: Pages render to static HTML and can be served from CDNs with negligible latency.
- SEO: SSG provides full HTML to crawlers with metadata available for previews and social cards.
- Simplicity: No need to build a separate CMS to start publishing; Git + Markdown is enough.
Roadmap for the tutorial series (high-level)
Part 1 (this file): Introduction, project setup, content model, parsing Markdown with gray-matter, and SSG basics with reading time. Part 2: Rendering Markdown safely with remark/rehype, syntax highlighting, and images. Part 3: Categories pages, pagination and tag filters. Part 4: RSS feed generation, SEO metadata, and deployment notes.
Prerequisites
- Basic JavaScript and React familiarity
- Node.js & npm installed
- Familiarity with Git and the command line
Further Reading
- Next.js Documentation: https://nextjs.org/docs — Official docs for getStaticProps, getStaticPaths, and SSG.
- CommonMark / Markdown Guide: https://www.markdownguide.org — Markdown syntax reference.
Project Setup and Content Model
Create a new Next.js app and install a few packages we'll use throughout the tutorial. From your terminal:
npx create-next-app@latest next-markdown-blog
cd next-markdown-blog
npm install gray-matter remark remark-html reading-time date-fns
Packages used here
- gray-matter: parse YAML frontmatter from Markdown files.
- remark + remark-html: convert Markdown to HTML during build (we'll swap to remark-rehype later for safer rendering).
- reading-time: small utility to estimate minutes from a text blob.
- date-fns: lightweight date utilities for parsing/formatting.
Content folder layout
Use a content/posts directory at the project root. Each post is a .md file with YAML frontmatter. Example structure:
/content
/posts
2023-08-01-welcome.md
2024-01-01-new-year.md
Sample frontmatter schema (recommended fields)
- title: string — Post title
- date: ISO date string (YYYY-MM-DD or full ISO) — Publication date
- categories: array of strings — One or more categories
- description: string — Short excerpt for list pages and meta description
- slug: string (optional) — If not present, derived from filename
- draft: boolean (optional) — Skip building if true
Example Markdown file: content/posts/2024-01-01-new-year.md
--- title: 'Happy New Year' date: '2024-01-01' categories:
- announcements
- personal description: 'A short note about plans for the new year.' slug: 'new-year'
Hello readers — this is my first post for the new year!
Naming conventions and slugs
- You can name files like YYYY-MM-DD-title.md to keep chronological order.
- If you include a slug field, use it for the URL path; otherwise derive a slug from filename by stripping the date prefix.
Project helpers
Create a small helper folder: lib/posts.ts (or .js). This file will centralize reading and listing posts. Start by exporting constants for the content path and allowed fields.
Further Reading
- Next.js Getting Started: https://nextjs.org/learn/basics/getting-started — Quickstart for new Next.js apps.
- gray-matter: https://github.com/mdx-js/gray-matter — Library to parse frontmatter from Markdown files.
Parsing Markdown and Frontmatter with gray-matter
The next step is reading Markdown files from disk and parsing their frontmatter and content. We'll use Node's fs and path modules together with gray-matter.
A minimal utility to read files and parse frontmatter (TypeScript-flavored pseudocode):
import fs from 'fs' import path from 'path' import matter from 'gray-matter' import { parseISO, isFuture } from 'date-fns'const postsDir = path.join(process.cwd(), 'content', 'posts')
export function getPostFiles() { return fs.readdirSync(postsDir).filter(f => f.endsWith('.md')) }
export function readPostFile(filename) { const fullPath = path.join(postsDir, filename) const raw = fs.readFileSync(fullPath, 'utf8') const { data, content } = matter(raw)
return { data, content, filename } }
Normalize metadata and create safe slugs
When you parse frontmatter, normalize the values you need downstream:
- Ensure date is a valid Date object (or ISO string) and ignore posts with future dates or draft: true unless you want them published locally.
- Normalize categories to an array of lowercase strings.
- Derive slug by looking for a slug field, otherwise strip prefix from filename: 2024-01-01-title.md -> title
Example normalization function:
function normalizePost(raw) { const { data, content, filename } = raw const slugFromFile = filename.replace(/^(\d{4}-\d{2}-\d{2}-)?(.*)\.md$/, '$2') const slug = (data.slug || slugFromFile).toString() const date = data.date ? parseISO(data.date) : null const isDraft = !!data.draft const categories = (data.categories || []).map(c => c.toLowerCase())
return { ...data, date, slug, content, categories, isDraft } }
Handling drafts and future dates
It's common to skip draft posts or posts dated for the future in production builds. During development you can include them locally.
WarningDo not publish posts with future dates unless you intend them to go live. SSG will include only what you return from getStaticProps — but it's easy to accidentally push drafts if your build script includes them.Validating required fields
At build time, ensure required fields (title, date, description) exist. Fail the build with a helpful error if not — this avoids broken pages and missing metadata in RSS/SEO.
Further Reading
- gray-matter README: https://github.com/jonschlinkert/gray-matter — Examples and API for parsing frontmatter.
- moment.js Alternatives: https://date-fns.org — Use date-fns for lightweight date parsing/formatting.
Static Site Generation: getStaticProps and getStaticPaths
With parsed posts and normalized metadata, wire SSG to create the blog index and individual post pages.
Generating paths for each post
In pages/posts/[slug].tsx (or .js) implement getStaticPaths using the slugs you computed in your posts helper:
export async function getStaticPaths() { const posts = getAllPosts() // returns normalized posts const paths = posts .filter(p => !p.isDraft && !isFuture(p.date)) .map(p => ({ params: { slug: p.slug } }))
return { paths, fallback: false } }
Fetching post content for each page
In getStaticProps, locate the post by slug and convert Markdown to HTML using remark. Add reading-time metadata either using the reading-time package or a small custom function.
import readingTime from 'reading-time' import { remark } from 'remark' import remarkHtml from 'remark-html'export async function getStaticProps({ params }) { const post = getPostBySlug(params.slug) const md = await remark().use(remarkHtml).process(post.content) const contentHtml = md.toString() const rt = readingTime(post.content)
return { props: { post: { ...post, contentHtml, readingTime: rt } } } }
Custom reading-time (simple)
If you want a zero-dependency approach, a typical method counts words and divides by an average words-per-minute (commonly 200–250 wpm):
function estimateReadingTime(text, wordsPerMinute = 200) {
const words = text.trim().split(/\s+/).length
const minutes = Math.max(1, Math.round(words / wordsPerMinute))
return { minutes, words }
}
Where to render reading time
Display it in the post header next to the date and categories. Also include readingTime.minutes (or minutes) in list pages so readers can scan posts quickly.
Incremental static regeneration (ISR)
If you plan to add posts without redeploying, use revalidate in getStaticProps to enable ISR. For a mostly static blog that updates on deploy, you can omit revalidate and rely on normal builds.
TipSet fallback to 'blocking' if you expect new slugs after deployment and want Next.js to render them on-demand the first time they're requested.Edge cases and performance
- Avoid loading full post content when building the index. For lists, read only frontmatter and produce excerpts to keep build times low.
- If you have many posts (thousands), consider a caching strategy or incremental builds to limit build-time IO.
Further Reading
- Next.js Data Fetching: https://nextjs.org/docs/basic-features/data-fetching — Reference for getStaticProps and getStaticPaths.
- reading-time npm: https://www.npmjs.com/package/reading-time — A small library that computes reading time from text.
Rendering Markdown with remark and rehype
Next.js blogs that source posts from Markdown usually convert that Markdown into HTML (or React elements) during build time. The unified ecosystem (remark -> rehype) is the standard approach: use remark to parse/transform Markdown, then rehype to transform/sanitize HTML and (optionally) produce React elements.
Here are two common server-side approaches:
- Use unified (remark + rehype) to produce sanitized HTML strings that you dangerouslySetInnerHTML in a React component; or
- Use next-mdx-remote or rehype-react to compile Markdown into React elements so you can render components inline.
Minimal unified pipeline (produce HTML string):
import { unified } from 'unified' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import rehypeStringify from 'rehype-stringify' import rehypeSanitize from 'rehype-sanitize'export async function markdownToHtml(markdown) { const result = await unified() .use(remarkParse) .use(remarkRehype) .use(rehypeSanitize) // important .use(rehypeStringify) .process(markdown)
return String(result) }
If you want React nodes instead of raw HTML, either use rehype-react or use next-mdx-remote (recommended for Next.js because of good SSR support):
- rehype-react converts an HAST (HTML AST) into React elements (you can map tags to components).
- next-mdx-remote compiles MDX/Markdown to a serialized form that you hydrate with React on the client — good for MDX features and server-side rendering.
Tip on choosing: if you only need to render Markdown and want simple HTML with fast builds, produce HTML strings and render with a simple React wrapper. If you need interactive embeds or React components inside posts, adopt MDX/next-mdx-remote.
TipUse rehype-sanitize (or a strict sanitization schema) whenever you accept Markdown from untrusted sources — it prevents XSS by stripping dangerous nodes/attributes.Syntax highlighting
For code block highlighting you have a few popular options:
- rehype-highlight (uses highlight.js) — simple plugin chain:
import rehypeHighlight from 'rehype-highlight'
// .use(rehypeHighlight) before rehypeStringify
- Shiki (more modern, themeable, high-quality) — use remark-shiki or a shiki-based rehype plugin. Shiki works at build time and outputs highlighted HTML or inline tokens.
Example with rehype-highlight:
unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeHighlight) // add language classes + markup
.use(rehypeSanitize)
.use(rehypeStringify)
Factrehype-highlight injects language-specific class names (e.g., language-js) so you can pair with CSS themes or Prism-style stylesheets.Images, external links, and asset handling
Images: prefer using Next.js'
next/imagefor performance (optimized sizes, lazy loading). To integrate, parse images in Markdown and replace tags with a React Image component when you produce React nodes (rehype-react or next-mdx-remote component mapping). If you output raw HTML, consider post-processing image tags to add loading attributes and wrapper classes, or keep them and rely on normal behavior.External links: use a rehype plugin such as rehype-external-links (or write a small rehype plugin) to add
rel="noopener noreferrer"andtarget="_blank"to external anchors.
Sanitization and security
- Always sanitize HTML produced from Markdown unless you fully control all posts. rehype-sanitize is commonly used; it accepts a schema you can extend to allow specific tags/attributes.
- When using MDX and allowing components, strictly control which components you pass into the MDX scope. Avoid passing a global set of components that expose privileged APIs.
Embedding components safely
If you adopt MDX or next-mdx-remote, embed components by providing a components map during render. For example with next-mdx-remote:
TipIf you need just a few dynamic widgets (e.g., Tweets, YouTube embeds), create explicit shortcodes/components and whitelist them in MDX rather than allowing arbitrary component imports.// In the page component import { MDXRemote } from 'next-mdx-remote' import MyImage from '@/components/MyImage'const components = { img: MyImage }
<MDXRemote {...source} components={components} />
Further Reading
Adding Categories and Category Pages
Categories are best stored in post frontmatter (e.g., a YAML array under categories:). The steps are:
- Normalize and aggregate categories across all posts during build.
- Create a slug for each category (URL-friendly) and generate static pages at /categories/[slug].
- On the main blog index, provide a list of categories with counts and allow filtering.
Extract and normalize categories
When you parse frontmatter for each post, normalize categories to a consistent shape — trim whitespace, collapse case (e.g., to lowercase for de-dup), and keep the original display name if you care about capitalization.
Example normalization:
import slugify from 'slugify'
function normalizeCategory(name) { const trimmed = String(name).trim() return { name: trimmed, slug: slugify(trimmed, { lower: true, strict: true }) } }
Aggregate categories and counts
While you collect posts (for example in getStaticProps or a pre-build script), build a map:
const categories = new Map() posts.forEach(post => { (post.categories || []).forEach(cat => { const { name, slug } = normalizeCategory(cat) const entry = categories.get(slug) || { name, slug, count: 0, posts: [] } entry.count++ entry.posts.push(post) categories.set(slug, entry) }) })
const categoryArray = Array.from(categories.values())
Generate static category pages
Use getStaticPaths to create a path for each category slug and getStaticProps to feed the list of posts for that category to the page:
export async function getStaticPaths() { const categorySlugs = await getAllCategorySlugs() // implement return { paths: categorySlugs.map(slug => ({ params: { category: slug } })), fallback: false } }
export async function getStaticProps({ params }) { const posts = await getPostsByCategory(params.category) return { props: { posts, category: params.category } } }
UI & UX considerations
- Show counts next to each category (e.g., Tech (12)).
- Provide a canonical URL for each category page using the canonical link rel to avoid duplicate content issues.
- Consider a flat category namespace vs nested tags. If you plan many categories, allow search or paginate category pages.
Further Reading
- slugify — Helpful for converting category names to URL-friendly slugs.
- SEO Best Practices — Guidance on category pages and crawlability.
Generate an RSS Feed for Syndication
RSS is XML: each should include title, link, guid, pubDate, and description/content. You can also include category elements and custom fields (e.g., reading time via itunes:duration for podcasts — for blogs just include a custom namespace or add reading time inside description).
Two main approaches:
- Use the feed npm package to generate RSS/Atom programmatically.
- Hand-roll a simple XML builder (string templates) and write to public/feed.xml during build.
Using the feed package (concise example)
import { Feed } from 'feed' import fs from 'fs'export async function generateFeed(siteMeta, posts) { const feed = new Feed({ title: siteMeta.title, description: siteMeta.description, id: siteMeta.url, link: siteMeta.url, language: 'en' })
posts.forEach(post => { feed.addItem({ title: post.title, id: `${siteMeta.url}/posts/${post.slug}`, link: `${siteMeta.url}/posts/${post.slug}`, description: post.excerpt || post.description, date: new Date(post.date), category: (post.categories || []).map(c => ({ name: c })) }) })
// Write both RSS and Atom to public fs.writeFileSync('public/feed.xml', feed.rss2()) fs.writeFileSync('public/atom.xml', feed.atom1()) }
When to run
- Call generateFeed during your build (e.g., from getStaticProps at the root index page or a dedicated build script). Writing to /public ensures Next.js serves it as a static file.
Including reading time and categories
- You can include reading time in or a custom element within the item. Remember to follow RSS rules: add custom namespaces if you create nonstandard elements.
- Categories can be included with elements using the feed package or manual XML.
Validation and testing
- Validate your feed with an online validator (W3C or dedicated RSS validators) and spot-check in an aggregator (Feedly, Inoreader) to ensure items render correctly.
Further Reading
- RSS 2.0 Specification — Official specification for RSS elements.
- feed npm — Utility to build RSS/Atom feeds programmatically.
Advanced: Reading Time Algorithms and MDX Considerations
Reading time basics
A simple reading-time algorithm counts words and divides by a words-per-minute (WPM) baseline (commonly 200–275 WPM). The npm reading-time package uses a 200 WPM baseline and includes nuance such as rounding and adding a minimum of one minute.
Custom adjustments
- Exclude code blocks and captions: code and markup normally read more slowly or are skipped. When you compute reading time from the Markdown AST, ignore
or fenced code nodes. - Images: treat images as contributing a small fixed time (for example, 12–15 seconds) instead of words.
- Minimum and rounding: round to the nearest minute or always show "< 1 min" for very short reads.
Simple custom implementation:
function readingTimeFromText(markdown) {
const text = stripMarkdown(markdown) // remove code fences, images, links
const words = text.trim().split(/\s+/).length
const wpm = 220
const minutes = Math.max(1, Math.round(words / wpm))
return { words, minutes }
}
MDX vs Markdown
- Markdown + remark: excellent for static content and fast builds, minimal runtime. You get a stable pipeline for parsing, transforming, sanitizing, and producing HTML.
- MDX: allows embedding React components directly inside your posts. It increases flexibility (interactive embeds) but also increases bundle size and complexity (component hydration, security considerations).
When to choose MDX
- Use MDX if you plan to include interactive components (charts, embeds) inside many posts.
- Stick with Markdown if most content is static text with images and code blocks — you’ll benefit from simpler render paths and smaller client bundles.
Further Reading
- reading-time Implementation Notes — Examine how reading-time counts words and calculates minutes.
- MDX — Documentation for embedding React components in Markdown.
SEO: Meta Tags, Open Graph, and Sitemap
A strong SEO setup gives each post and category page its own, crawlable identity. We'll build a small reusable Meta component that reads frontmatter (title, description, date, canonical, image, author, categories) and emits standard meta tags, Open Graph, and Twitter Card markup. Then we'll generate a sitemap.xml at build time so search engines discover every post and category route.
Key ideas:
- Use a single Meta/Head component for pages to ensure consistent markup.
- Pull canonical URLs from a SITE_URL env var to avoid accidental relative links.
- Generate sitemap.xml during build using your posts' slugs and category routes.
Example reusable Meta component (components/Meta.tsx):
import Head from 'next/head' import React from 'react'interface MetaProps { title: string description?: string canonical?: string image?: string date?: string author?: string type?: 'article' | 'website' }
export default function Meta({ title, description = '', canonical, image, date, author, type = 'article' }: MetaProps) { const siteTitle = `${title}` return ( <Head> <title>{siteTitle}</title> <meta name="description" content={description} /> {canonical && <link rel="canonical" href={canonical} />}
{/* Open Graph */} <meta property="og:type" content={type} /> <meta property="og:title" content={siteTitle} /> <meta property="og:description" content={description} /> {image && <meta property="og:image" content={image} />} {canonical && <meta property="og:url" content={canonical} />} {/* Twitter */} <meta name="twitter:card" content={image ? 'summary_large_image' : 'summary'} /> <meta name="twitter:title" content={siteTitle} /> <meta name="twitter:description" content={description} /> {image && <meta name="twitter:image" content={image} />} {author && <meta name="twitter:creator" content={`@${author}`} />} {/* Article metadata (optional) */} {date && <meta property="article:published_time" content={date} />} </Head>
) }
Usage in a post page (pages/posts/[slug].tsx):
// after parsing frontmatter
<Meta
title={frontmatter.title}
description={frontmatter.description || excerpt}
canonical={`${process.env.NEXT_PUBLIC_SITE_URL}/posts/${slug}`}
image={frontmatter.image ? `${process.env.NEXT_PUBLIC_SITE_URL}${frontmatter.image}` : undefined}
date={frontmatter.date}
author={frontmatter.author}
/>
TipAlways set NEXT_PUBLIC_SITE_URL (e.g. https://example.com). It makes canonical, og:url, and sitemap generation deterministic across environments.FactTwitter Cards prefer summary_large_image if you provide an image — it increases clickthroughs on many platforms.WarningDon’t emit duplicate title/description markup across category pages and posts; keep category descriptions unique to avoid thin-content penalties.Generating sitemap.xml during build
A simple Node script can read your posts (or import your posts index utility), assemble URLs (posts + category pages), and write public/sitemap.xml. Example script: scripts/generate-sitemap.js
const fs = require('fs') const path = require('path') const glob = require('glob') const matter = require('gray-matter')const SITE_URL = process.env.SITE_URL || 'https://example.com' const postsDir = path.join(process.cwd(), 'posts')
const files = glob.sync('**/*.md', { cwd: postsDir })
const urls = files.map(file => { const source = fs.readFileSync(path.join(postsDir, file), 'utf8') const { data } = matter(source) const slug = file.replace(/.mdx?$/, '').replace(/index$/, '') const loc = `${SITE_URL}/posts/${slug}`.replace(//+/g, '/') return { loc, lastmod: data.date || fs.statSync(path.join(postsDir, file)).mtime.toISOString() } })
// Add category pages if you have a categories list const categories = new Set() files.forEach(file => { const source = fs.readFileSync(path.join(postsDir, file), 'utf8') const { data } = matter(source) ;(data.categories || []).forEach(c => categories.add(c)) }) ;[...categories].forEach(cat => { urls.push({ loc: `${SITE_URL}/category/${encodeURIComponent(cat)}`, lastmod: new Date().toISOString() }) })
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls .map(u => ` <url>\n <loc>${u.loc}</loc>\n <lastmod>${u.lastmod}</lastmod>\n </url>`) .join('\n')}\n</urlset>`
fs.writeFileSync(path.join(process.cwd(), 'public', 'sitemap.xml'), xml) console.log('sitemap.xml written')
Add to package.json so sitemap is produced after build:
{
"scripts": {
"build": "next build",
"postbuild": "node scripts/generate-sitemap.js"
}
}
SEO for category pages
- Give category pages unique, descriptive meta descriptions (e.g., "Articles about JavaScript patterns and tips").
- Use canonical links pointing to the category URL.
- If you paginate categories, include rel="next"/"prev" as appropriate and canonicalize to the first page or a dedicated canonical URL.
Further Reading
- Google SEO Starter Guide: https://developers.google.com/search/docs/basics/seo-starter-guide
- Open Graph Protocol: https://ogp.me
Case Study: Sample Project Structure and Deployment
Below is a representative repository layout that includes the pieces you built across the tutorial. This layout keeps content, build scripts, and reusable components clear.
Example project tree (abridged):
my-blog/
├─ posts/ # Markdown posts
│ ├─ 2024-01-01-welcome.md
│ └─ ...
├─ public/
│ ├─ images/
│ └─ sitemap.xml # generated at build
├─ components/
│ ├─ Meta.tsx
│ ├─ Layout.tsx
│ └─ PostPreview.tsx
├─ lib/
│ ├─ posts.ts # helpers: getAllPosts, getPostBySlug, categories
│ └─ reading-time.ts
├─ pages/
│ ├─ index.tsx
│ ├─ posts/[slug].tsx
│ ├─ category/[slug].tsx
│ └─ rss.xml.tsx # optional serverless RSS XML route
├─ scripts/
│ ├─ generate-sitemap.js
│ └─ generate-rss.js
├─ next.config.js
├─ package.json
└─ README.md
Key files to check before deployment:
- components/Meta.tsx — consistent meta tags for posts and categories
- lib/posts.ts — single source of truth for listing posts and categories
- scripts/generate-sitemap.js and scripts/generate-rss.js — run at build to produce public files
- pages/rss.xml.tsx or public/rss.xml — RSS reachable at /rss.xml
- next.config.js — ensure images.domains and basePath (if used) match SITE_URL
Deployment tips
Vercel (recommended for Next.js):
- Connect your Git repo and set the framework to Next.js.
- Set Environment Variables: NEXT_PUBLIC_SITE_URL (e.g. https://yourdomain.com), SITE_URL for scripts if needed.
- Build command: npm run build (postbuild will run the sitemap script and produce public/sitemap.xml).
Netlify (static export or serverless):
- If using static export (next export) be sure to test that all dynamic pages are pre-rendered.
- Alternatively use adapters like next-on-netlify or Vercel build.
Caching, invalidation, and incremental updates
- If you use purely static builds: any new post requires a rebuild (trigger via Git push or CI).
- Consider Incremental Static Regeneration (ISR) with revalidate in getStaticProps for faster updates without full rebuilds — but you must host where ISR is supported (Vercel).
- For caches (CDN), ensure you invalidate or use cache headers appropriately. Vercel handles most cache invalidation on deploys; if you serve via a CDN in front of S3, you may need a cache purge step.
Deployment checklist (quick):
- NEXT_PUBLIC_SITE_URL set in production env
- sitemap.xml generated and reachable at /sitemap.xml
- rss.xml available at /rss.xml
- robots.txt in public (allow/disallow rules)
- Analytics (e.g., Google Analytics or Plausible) added to Layout
- Redirects configured (Netlify/_redirects or vercel.json) for legacy URLs
Further Reading
- Vercel Deployment: https://vercel.com/docs
- Netlify Docs: https://docs.netlify.com
Conclusion, Further Reading, and Next Steps
You've built a static Next.js blog powered by Markdown with categories, reading time, RSS, and SEO-ready metadata. This final section summarizes the completed features, recommended next enhancements, and a short maintenance checklist.
What you now have:
- Markdown posts parsed with frontmatter (title, date, categories, description, image).
- Category pages that aggregate posts by tag/category.
- Reading time calculated and displayed alongside posts.
- RSS feed generation and sitemap.xml produced at build time.
- Reusable Meta component emitting title, description, canonical, Open Graph, and Twitter Card tags.
Sensible next features to add
- Pagination: load a limited number of posts per page for index and category pages. Add rel="next"/"prev" and canonicalization for SEO.
- Multi-author support: add an authors/ directory or authors in frontmatter; create author pages with social links and bios.
- Comments: add lightweight systems like Staticman, Utterances (GitHub issues), or third-party providers (Disqus, Commento).
- Full-text search: integrate Algolia, MeiliSearch, or client-side search via lunr.js or FlexSearch for small sites.
- MDX migration: move to MDX when you want React components inside posts (e.g., interactive examples).
- Remark and Rehype plugins: add syntax highlighting, auto-linked headings, footnotes, and image optimization. Example plugins: remark-prism, rehype-slug, remark-autolink-headings.
Maintenance and automation checklist
- Automate builds whenever content changes (Git pushes or a CMS webhook).
- Ensure script-based generation for sitemap and RSS is part of CI/CD (postbuild or a CI step).
- Periodically verify canonical URLs and og:image accessibility (images must be public and accessible to social crawlers).
- Monitor performance and keep bundle size small; avoid heavy client-side libraries where possible.
Resources to deepen your knowledge
- remark Plugins: https://github.com/remarkjs/remark/tree/main/doc/plugins — extend Markdown processing with many plugins.
- MDX Migration Guide: https://mdxjs.com/getting-started — when you need inline React in posts.
Final checklist before shipping to production
- All pages have Meta component with correct metadata
- sitemap.xml and rss.xml are generated and valid (use online validators)
- Environment variables (NEXT_PUBLIC_SITE_URL) set in production
- Build and deploy tested end-to-end (links, images, social previews)
- Monitoring/analytics configured and privacy considerations reviewed
Further Reading
- remark Plugins: https://github.com/remarkjs/remark/tree/main/doc/plugins
- MDX Migration Guide: https://mdxjs.com/getting-started
About Prateeksha Web Design
Prateeksha Web Design helps businesses turn tutorials like "Next.js Blog with Markdown: Categories, Reading Time, and RSS Feed" into real-world results with custom websites, performance optimization, and automation. From strategy to implementation, our team supports you at every stage of your digital journey.
Chat with us now Contact us today.