Hello, world. Di nuovo.
nutellino.it riparte dopo qualche anno di pausa. Com'è fatto il sito nuovo: Markdown in git, export statico su Cloudflare Pages, un pannello di gestione nel mio home lab e niente database.

Il 20 dicembre 2015 scrivevo il primo post di questo sito: "Pubblicherò guide, curiosità e tutto quello che mi sembrerà divertente o utile." L'ultimo articolo è del 2021, poi il blog si è fermato.
Adesso riparte. Stessa idea, sito nuovo. E visto che è un blog tecnico, il primo articolo non poteva che essere su come è fatto.
L'idea: niente da tenere acceso
Il vecchio nutellino.it è nato su WordPress ed è finito su Ghost: un'applicazione sempre accesa, un database, aggiornamenti da inseguire. Per un blog che aggiorno quando ho tempo è un sacco di roba da mantenere.
Questa volta volevo l'opposto:
- gli articoli sono file Markdown in un repository git, separato dal codice del sito;
- il sito è statico: HTML, CSS e qualche file JavaScript, generati dalla build e serviti da Cloudflare Pages;
- in produzione non gira nessun backend: niente Node, niente database, niente pannello di login esposto su Internet.
flowchart TD
agents["agenti AI<br/>via MCP"] -->|solo bozze| admin
me["io"] --> admin["pannello di gestione<br/>nell'home lab"]
admin -->|un commit a salvataggio| content["nutellinoit-content<br/>repository git"]
content --> build["build nell'home lab<br/>validazione + Next.js + ricerca"]
build --> preview["anteprima<br/>nell'home lab"]
preview -->|Pubblica| prod["Cloudflare Pages<br/>nutellino.it"]Gli articoli
Ogni articolo è un file Markdown con un'intestazione di metadati:
---
schemaVersion: 1
id: si-riparte
locale: it
slug: si-riparte
title: "Hello, world. Di nuovo."
description: "..."
date: "2026-10-11"
tags: ["personale", "self-hosting"]
draft: false
---Le intestazioni vengono validate con uno schema Zod prima della build: una data scritta male, uno slug duplicato o una traduzione mancante fanno fallire tutto. Preferisco una build rossa a un articolo rotto online.
Gli articoli in draft: true e quelli con una data futura non vengono pubblicati. Basta mettere la data giusta e il pezzo esce alla prima build utile.
Il sito
Il codice è Next.js 16 con output: 'export': in fase di build genera una pagina HTML per ogni articolo, in italiano e in inglese, con URL separati (/it/... e /en/...), sitemap, robots.txt e i metadati per i motori di ricerca.
Anche la ricerca è statica: Pagefind costruisce un indice durante la build, e il browser scarica solo i pezzi che gli servono quando cerchi qualcosa.
Il risultato è una cartella di file statici, che Cloudflare distribuisce dalla sua rete. Nessun server mio da aggiornare, nessuna porta aperta verso l'home lab.
Anteprima e pubblicazione
La build gira nel mio home lab, nel pannello di gestione: valida i contenuti, genera il sito e lo serve come anteprima. Quello che vedo lì è esattamente il sito che andrà online.
Quando mi convince, il pulsante Pubblica carica quella stessa cartella su Cloudflare Pages. Niente build da rifare altrove, nessuna differenza tra quello che ho riletto e quello che esce. Se qualcosa va storto, i contenuti tornano indietro con un git revert, e Cloudflare tiene comunque lo storico delle pubblicazioni.
Il prossimo passo è portare i repository sul mio Gitea, con una pipeline a ogni push e un'anteprima per ogni branch.
Il pannello di gestione e gli agenti
Scrivere tutto da terminale va bene, ma non sempre. Per questo c'è un piccolo pannello di gestione che gira solo nel mio home lab: elenco degli articoli, editor Markdown con l'anteprima resa dagli stessi componenti del sito, caricamento delle immagini. Ogni salvataggio è un commit sul repository dei contenuti, quindi la cronologia dice sempre chi ha cambiato cosa.
Lo stesso pannello espone un'API e un server MCP, così anche gli agenti AI possono lavorare sul blog. Con delle regole precise:
- ogni agente ha il suo token e firma i commit con il suo nome;
- gli agenti scrivono e modificano solo bozze;
- togliere un articolo dalle bozze e mandare il sito online posso farlo solo io.
Tutto in mise e Docker Compose
Sulla mia macchina non installo niente a mano: le versioni degli strumenti stanno in mise.toml, i comandi girano dentro container Docker Compose.
mise run dev # server di sviluppo
mise run validate # controlla gli articoli
mise run build # export statico
mise run admin # pannello di gestione e anteprima localeChi clona il repository (cioè io, tra sei mesi, dopo essermi dimenticato tutto) deve solo lanciare mise install.
Il vecchio blog
I vecchi articoli non sono andati persi del tutto: li ho recuperati dalla Wayback Machine e da un vecchio backup di Ghost, e convertiti in Markdown. Li trovate nell'archivio, con un avviso in cima, perché alcune cose sono invecchiate parecchio (ciao Xen Server). Quelli che avevo scritto in una lingua sola hanno anche una traduzione automatica, segnalata come tale.
I vecchi indirizzi del tipo /2018/09/02/journey-to-kubernetes-parte-1-la-ricetta/ reindirizzano alla nuova pagina tramite le regole di redirect di Cloudflare, generate a ogni build, quindi i link sparsi in giro continuano a funzionare. Dal backup sono tornate anche quasi tutte le immagini; mancano solo quelle degli articoli più recenti.
Con cosa l'ho costruito
La prima bozza del sito l'ho fatta con v0. Poi l'ho portata avanti consumando la quota del mio ChatGPT Plus, e il resto, backend compreso, l'ho fatto con Claude.
L'idea architetturale, lo stile, il logo che si muove e quello che racconto sono farina del mio sacco: l'AI ha scritto tanto codice, ma la rotta l'ho data io. Ne parlo meglio in L'AI e il PEBCAK.
Niente cookie
Il sito non usa cookie, non ha analytics e non carica niente da altri domini: i font sono inclusi nel sito stesso. Quindi niente banner da accettare. L'unica cosa che vede gli indirizzi IP è Cloudflare, come qualsiasi hosting, per far funzionare e proteggere il sito.
Ciao di nuovo! 🙂