🏆 Project – पूरी Website बनाएँ · Lesson 34
Project: Markdown वाला Blog
Markdown blog क्यों?
Database CMS
- MySQL + admin panel चाहिए
- हर page DB query करता है
- Secure करने को ज़्यादा चीज़ें
Markdown files
- किसी भी editor में posts लिखें
- Pages एक बार build (SSG) – बहुत तेज़
- Git में version history
Step 1: Post लिखिए
content/posts/ की हर file एक post है। --- lines के बीच का हिस्सा front matter (metadata) है।
Markdown – content/posts/how-to-start-coding.md
--- title: How to Start Coding in 2026 – A Simple Roadmap date: '2026-09-02' excerpt: Confused where to begin? Follow this 4-step roadmap that our students use. tags: [roadmap, beginners] --- Starting to code feels hard only because there are too many choices. Here is the path our students follow. ## 1. Pick one language Choose **C** if you are in school or B.Tech, or **Python** if you want AI and data science. Do not learn both at the same time. ## 2. Practise every day Write at least **one small program daily** — even 20 minutes is enough. Typing code yourself is what builds memory. ## 3. Build a tiny project After 4 weeks, build something small: a calculator, a marks report or a to-do list. ## 4. Learn the web Once basics are clear, learn HTML, CSS and JavaScript, then a framework like **Next.js**. > Consistency beats talent. Code a little every day.
Step 2: lib/posts.js
JS – lib/posts.js
import fs from 'node:fs'
import path from 'node:path'
import matter from 'gray-matter'
import { marked } from 'marked'
// Every .md file inside content/posts becomes a blog post.
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
export function getAllPosts() {
return fs
.readdirSync(POSTS_DIR)
.filter((f) => f.endsWith('.md'))
.map((file) => {
const slug = file.replace(/\.md$/, '')
const { data } = matter(fs.readFileSync(path.join(POSTS_DIR, file), 'utf8'))
return { slug, title: data.title, date: data.date, excerpt: data.excerpt, tags: data.tags || [] }
})
.sort((a, b) => (a.date < b.date ? 1 : -1)) // newest first
}
export function getPost(slug) {
const file = path.join(POSTS_DIR, slug + '.md')
if (!/^[a-z0-9-]+$/.test(slug) || !fs.existsSync(file)) return null
const { data, content } = matter(fs.readFileSync(file, 'utf8'))
return { slug, ...data, html: marked.parse(content) }
}Regex /^[a-z0-9-]+$/ ../../etc/passwd जैसे paths रोकता है — file path बनाने में use होने वाली हर चीज़ validate करें।
Step 3: Blog list page
JSX – app/blog/page.jsx
import PostCard from '@/components/PostCard'
import { getAllPosts } from '@/lib/posts'
export const metadata = {
title: 'Blog – Coding Tips & Roadmaps',
description: 'Simple articles on learning to code: roadmaps, language comparisons and project ideas.',
alternates: { canonical: '/blog' },
}
export default function BlogPage() {
const posts = getAllPosts()
return (
<div className="container section">
<h1>Blog</h1>
<p className="lead">{posts.length} articles to help you learn faster.</p>
<div className="grid" style={{ marginTop: 24 }}>
{posts.map((p) => (
<PostCard key={p.slug} post={p} />
))}
</div>
</div>
)
}Step 4: Post page
JSX – app/blog/[slug]/page.jsx
import Link from 'next/link'
import { notFound } from 'next/navigation'
import JsonLd from '@/components/JsonLd'
import { getAllPosts, getPost } from '@/lib/posts'
import { site } from '@/lib/site'
export function generateStaticParams() {
return getAllPosts().map((p) => ({ slug: p.slug }))
}
// Only the posts above exist. Any other slug gets a 404 without rendering.
export const dynamicParams = false
export async function generateMetadata({ params }) {
const { slug } = await params
const post = getPost(slug)
if (!post) return { title: 'Post not found' }
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: `/blog/${slug}` },
openGraph: { type: 'article', publishedTime: post.date },
}
}
export default async function PostPage({ params }) {
const { slug } = await params
const post = getPost(slug)
if (!post) notFound()
return (
<div className="container" style={{ maxWidth: 820 }}>
<p className="breadcrumb"><Link href="/">Home</Link> › <Link href="/blog">Blog</Link></p>
<article className="section">
<h1 style={{ marginTop: 0 }}>{post.title}</h1>
<p style={{ color: 'var(--muted)' }}>
{new Date(post.date).toLocaleDateString('en-IN', { day: 'numeric', month: 'long', year: 'numeric' })}
</p>
{/* HTML comes from our own Markdown files, so it is safe to render. */}
<div className="prose" dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
<JsonLd
data={{
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
datePublished: post.date,
author: { '@type': 'Organization', name: site.name },
mainEntityOfPage: `${site.url}/blog/${slug}`,
}}
/>
</div>
)
}⚠️ गलत slugs पर असली 404
dynamicParams = false से generateStaticParams में न लौटे हर slug पर Next.js 404 देता है। ध्यान दें: हमने app/blog/ में loading.jsx नहीं रखा। [slug] के ऊपर loading file response को status 200 के साथ stream करना शुरू कर देती है, फिर बाद का notFound() status code नहीं बदल पाता — SEO के लिए बुरा।
.md file से web page तक
Result


Checklist
- ✅ Posts newest first sorted हैं।
- ✅
/blog/does-not-existऔर/blog/..%2F..404 लौटाते हैं। - ✅ View source में BlogPosting JSON-LD block और
og:type = articleदिखता है।
✅ Practice
Post page पर post के tags को badges के रूप में दिखाइए, और "Related posts" section जोड़िए जो दो दूसरे posts दिखाए।
अक्सर पूछे जाने वाले प्रश्न (FAQ)
क्या यहाँ dangerouslySetInnerHTML safe है?
हाँ, क्योंकि HTML आपकी खुद लिखी Markdown files से आता है। User input का HTML बिना sanitize किए इस तरह कभी render न करें।
नया post कैसे जोड़ें?
content/posts में front matter में title, date और excerpt के साथ नई .md file बनाइए, फिर rebuild कीजिए। वह list में आ जाएगा, उसका अपना page बनेगा और sitemap में जुड़ जाएगा।
