Nazad na blog

Markdown blog u Next.js bez CMS-a i baze podataka

Markdown blog u Next.js bez CMS-a i baze podataka

8. oktobar 2026. — Next.js

Objavljeno

8. oktobar 2026.

Vreme čitanja

2 min čitanja

Kategorija

Next.js

Oznake

  • Next.js
  • Markdown
  • Blog
  • remark
  • Static Site

(Uvod)Blog u kom je svaki tekst Markdown fajl u Git repozitorijumu. Front matter, isticanje koda, slike, video i statičke stranice sa Next.js App Routerom.

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:

text
content/blog/
  my-first-post/
    index.md
    cover.jpg
    diagram.png

Tekst počinje YAML front matterom:

content/blog/my-first-post/index.md
---
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

bash
npm install gray-matter remark remark-gfm remark-html highlight.js
  • gray-matter odvaja front matter od teksta.
  • remark + remark-gfm parsiraju Markdown sa GitHub-ovim proširenjima (tabele, liste zadataka).
  • remark-html ga pretvara u HTML.
  • highlight.js ističe blokove koda tokom build-a, pa čitaoci ne preuzimaju nikakvu biblioteku za isticanje.

Učitavanje tekstova

lib/blog.ts
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:

app/blog/[slug]/page.tsx
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:

lib/highlight.ts
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:

scripts/copy-blog-media.mjs
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))
  }
}
package.json
{
  "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:

  1. Napravite folder i index.md.
  2. Commit i push.
  3. Hosting izgradi i objavi sajt za par minuta.

SEO dodaci koji se isplate

  • generateMetadata sa naslovom, sažetkom, kanonskim URL-om i naslovnom slikom kao Open Graph slikom.
  • sitemap.ts koji navodi svaki tekst.
  • BlogPosting JSON-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.

Bilten

Novi tekstovi na email

Kratak email kad objavim nešto novo. Bez spama, a pretplatu možete otkazati u bilo kom trenutku.

Serverless kontakt forma sa Cloudflare Workers i D1
Serverless kontakt forma sa Cloudflare Workers i D1
Sledeći tekst →