Blog koji čitate nema bazu podataka ni hostovani CMS. Svaki tekst je folder u Git repozitorijumu sa fajlom index.md i njegovim slikama. Jedan commit pokreće build sajta i objavljuje tekst. Evo kako da napravite isto sa Next.js App Routerom.
Zašto Markdown u Gitu
- Besplatno i brzo. Bez pretplate na CMS, bez upita bazi; svaka stranica je statički HTML.
- Verzionisano. Svaka izmena je commit koji možete da pregledate i vratite.
- Prenosivo. Markdown nadživljava svaku platformu. Prelazak na drugi framework znači promenu renderera, ne migraciju sadržaja.
- Može da se uređuje bilo gde. GitHub-ov veb editor, VS Code ili CMS zasnovan na Gitu, poput Decap ili Sveltia, ako neko više voli formu.
Kompromis: urednicima koji nisu tehnički potkovani treba malo Gita ili CMS zasnovan na Gitu ispred njega.
Struktura foldera
Jedan folder po tekstu drži slike pored teksta koji ih koristi — a relativni linkovi se ispravno prikazuju i na GitHubu:
content/blog/
my-first-post/
index.md
cover.jpg
diagram.pngTekst počinje YAML front matterom:
---
title: My first post
date: 2026-10-08
excerpt: One or two sentences for the listing and the meta description.
cover: ./cover.jpg
tags: [Next.js, Markdown]
---
The post starts here.Zavisnosti
npm install gray-matter remark remark-gfm remark-html highlight.jsgray-matterodvaja front matter od teksta.remark+remark-gfmparsiraju Markdown sa GitHub-ovim proširenjima (tabele, liste zadataka).remark-htmlga pretvara u HTML.highlight.jsističe blokove koda tokom build-a, pa čitaoci ne preuzimaju nikakvu biblioteku za isticanje.
Učitavanje tekstova
import fs from 'node:fs'
import path from 'node:path'
import matter from 'gray-matter'
import { remark } from 'remark'
import remarkGfm from 'remark-gfm'
import remarkHtml from 'remark-html'
const DIR = path.join(process.cwd(), 'content/blog')
export async function getAllPosts() {
const slugs = fs.readdirSync(DIR).filter((name) =>
fs.existsSync(path.join(DIR, name, 'index.md')),
)
const posts = await Promise.all(slugs.map(getPost))
return posts.sort((a, b) => b.date.localeCompare(a.date))
}
export async function getPost(slug: string) {
const file = fs.readFileSync(path.join(DIR, slug, 'index.md'), 'utf8')
const { data, content } = matter(file)
const html = String(
await remark().use(remarkGfm).use(remarkHtml, { sanitize: false }).process(content),
)
return {
slug,
title: String(data.title),
date: new Date(data.date).toISOString().slice(0, 10),
excerpt: String(data.excerpt ?? ''),
html,
}
}sanitize: false propušta sirovi HTML iz tekstova — to je u redu kada ste jedini autor. Ako i drugi mogu da šalju tekstove, zadržite filtriranje.
Pošto se ovo izvršava samo u Server Components i u generateStaticParams, fs je u redu: radi tokom build-a, nikada u pregledaču.
Stranice
Indeks prikazuje spisak tekstova; ruta za detalje je statička za svaki slug:
import { getAllPosts, getPost } from '@/lib/blog'
export const dynamicParams = false
export async function generateStaticParams() {
return (await getAllPosts()).map(({ slug }) => ({ slug }))
}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
)
}Isticanje koda tokom build-a
Mali remark plugin zamenjuje svaki čvor sa kodom istaknutim HTML-om:
import hljs from 'highlight.js'
import { visit } from 'unist-util-visit'
export function highlightCode() {
return (tree: any) => {
visit(tree, 'code', (node: any, index, parent: any) => {
const lang = node.lang && hljs.getLanguage(node.lang) ? node.lang : null
const html = lang
? hljs.highlight(node.value, { language: lang }).value
: hljs.highlightAuto(node.value).value
parent.children[index!] = {
type: 'html',
value: `<pre><code class="hljs language-${lang ?? 'text'}">${html}</code></pre>`,
}
})
}
}Dodajte ga pre remarkHtml sa .use(highlightCode) i uvezite highlight.js temu, na primer highlight.js/styles/github-dark.css.
Slike i video
Next.js poslužuje statičke fajlove iz public/, pa resursi tekstova moraju da završe tamo. Mala skripta koja se pokreće pre dev i build kopira sve što nije Markdown:
import fs from 'node:fs'
import path from 'node:path'
const SRC = 'content/blog'
const OUT = 'public/blog-media'
fs.rmSync(OUT, { recursive: true, force: true })
for (const slug of fs.readdirSync(SRC)) {
const dir = path.join(SRC, slug)
if (!fs.statSync(dir).isDirectory()) continue
for (const file of fs.readdirSync(dir)) {
if (file.endsWith('.md')) continue
fs.mkdirSync(path.join(OUT, slug), { recursive: true })
fs.copyFileSync(path.join(dir, file), path.join(OUT, slug, file))
}
}{
"scripts": {
"predev": "node scripts/copy-blog-media.mjs",
"prebuild": "node scripts/copy-blog-media.mjs"
}
}Zatim, u drugom remark pluginu, prepišite relativne URL-ove slika (./cover.jpg) u /blog-media/<slug>/cover.jpg. Kad ste već tu, pretvorite slike koje pokazuju na .mp4 ili .webm fajlove u <video controls> elemente, a YouTube linkove u ugrađene plejere — autori i dalje pišu običnu Markdown sintaksu za slike.
Dodajte public/blog-media u .gitignore; on se generiše.
Objavljivanje
Povežite repozitorijum sa statičkim hostingom — Cloudflare Pages, Netlify ili Vercel — sa npm run build kao komandom za build. Pisanje teksta sada izgleda ovako:
- Napravite folder i
index.md. - Commit i push.
- Hosting izgradi i objavi sajt za par minuta.
SEO dodaci koji se isplate
generateMetadatasa naslovom, sažetkom, kanonskim URL-om i naslovnom slikom kao Open Graph slikom.sitemap.tskoji navodi svaki tekst.BlogPostingJSON-LD.- ID-jevi za naslove, kako bi se na odeljke moglo linkovati.
Sve to je opisano u tekstu tehnički SEO za Next.js App Router.
Kada ga prerasti
Ako nekoliko netehničkih ljudi objavljuje svakog dana, potrebno im je zakazivanje objava ili urednički tokovi, headless CMS počinje da se isplati. Za lični sajt, studio ili blog proizvoda sa nekoliko tekstova mesečno, Markdown u Gitu je teško nadmašiti.

