Creare un blog MDX con Next.js da zero: come ho costruito questo blog
Come ho costruito con Next.js 16 App Router un blog che gira solo con file markdown, senza CMS. Struttura delle cartelle, configurazione MDX e SEO, spiegati con il flusso di codice reale.
In sintesi
Metti i post come file .mdx sotto src/content/posts e renderizzali con @next/mdx: il blog gira anche senza CMS. Il file è l'URL, e al posto del frontmatter un export di meta genera lista e metadati.
In questa pagina
Questo blog che stai leggendo non ha né un database né un pannello di amministrazione. Ogni post è semplicemente un file markdown. Ti mostro esattamente come l'ho costruito.
Perché basato su file
Di strumenti per blog ce ne sono tanti, ma per uno sviluppatore indie la maggior parte è eccessiva.
- CMS: si porta dietro pannello di gestione, integrazioni e costi
- Integrazione con Notion: comoda, ma ti blocchi su rendering, velocità e personalizzazione
- File markdown: i post sono versionati insieme al codice, è gratis e
git pushequivale a pubblicare
Io ho scelto la terza opzione. È quella con meno attrito.
Struttura delle cartelle
Guardando solo l'essenziale, è fatta così.
src/
├── app/
│ ├── page.tsx # 홈 - 글 목록
│ ├── posts/[slug]/page.tsx # 글 상세
│ └── sitemap.ts # /sitemap.xml
├── content/posts/*.mdx # ← 글은 여기에만 추가
└── lib/posts.ts # 목록·메타 로딩
Il nome del file diventa l'URL. hello.mdx si apre su /posts/hello.
Configurazione MDX
Per usare MDX con l'App Router si aggiunge @next/mdx. In Next 16 il default è Turbopack, quindi i plugin remark/rehype vanno passati come nomi in stringa (non si possono passare riferimenti a funzioni).
const withMDX = createMDX({
options: {
remarkPlugins: ["remark-gfm"], // 표·취소선 지원
rehypePlugins: ["rehype-slug"], // 제목 앵커
},
})
Un export di meta al posto del frontmatter
Di solito titolo e data si scrivono nel blocco --- in cima al markdown (il frontmatter), ma MDX può contenere JavaScript, quindi c'è un modo più pulito: esportare un oggetto, e basta.
export const meta = {
title: "글 제목",
date: "2026-07-06",
category: "dev",
}
## 본문 시작
Nella pagina della lista si importa dinamicamente ogni file e si legge solo questo meta.
const mod = await import(`@/content/posts/${slug}.mdx`)
return mod.meta // 본문 렌더 없이 메타만
Niente libreria di parsing, niente indice separato.
SEO fin dall'inizio
Se lo scopo del blog è portare traffico, la SEO va messa all'inizio, non dopo. Io ho impostato fin da subito metadati per ogni post, Open Graph, dati strutturati JSON-LD, più sitemap.xml, robots.txt e rss.xml. Ma di questo parlerò più in dettaglio nel prossimo post.
In sintesi
- Ogni post è un singolo file
.mdx, il nome del file è l'URL @next/mdx+ plugin come stringhe (Turbopack)export const metaal posto del frontmatter- SEO dal primo commit
Con questa stessa struttura anche il tuo blog può essere online in 30 minuti. Io la uso per continuare ad accumulare storie sui miei prodotti, come L'Onda del giorno.
Domande frequenti
Perché usare file MDX invece di un CMS?
I post sono versionati insieme al codice, è gratis e il deploy coincide con la pubblicazione: per uno sviluppatore indie è la soluzione con meno attrito. E puoi inserire liberamente immagini, codice e componenti.
Come si crea la lista dei post senza frontmatter?
In cima a ogni .mdx esporti un oggetto meta, e nella pagina della lista importi dinamicamente il file leggendo solo quel meta. Non serve nessuna libreria di parsing.
Articoli correlati
- 💻 Sviluppo
Se un bucket ha due proprietari, vince l'ultimo che fa apply
Ho aggiunto una regola lifecycle a S3 e terraform apply è fallito. La causa: due risorse possedevano ciascuna la configurazione lifecycle dello stesso bucket. Il lifecycle di S3 non funziona per singola regola ma per documento intero, quindi l'ultimo vincitore cancella in silenzio le regole dell'altro.
- 💻 Sviluppo
I test erano verdi, ma le notifiche non erano mai partite
Il codice che accodava i job falliva sempre, fin dal primo giorno. L'errore veniva inghiottito e i test restavano verdi grazie ai mock. La storia di come un singolo due punti ha ucciso in silenzio tre funzionalità.
- 💻 Sviluppo
È arrivata la notifica che il DB era morto. Il DB non è mai morto
In una sola giornata si sono accumulati quattro alert CRITICAL. 504, Prisma P2028, un 500 sull'admin e 'servizio DATABASE down'. Ho aperto per prime le metriche RDS: per 21 ore filate la CPU era arrivata al massimo al 19.7%, tutto tranquillo. A essere lento non era il DB, ma un andirivieni che attraversava l'Atlantico venticinque volte, e l'alert di 'down' se l'era fabbricato da solo l'health check.