SimplePress: Anatomia di un Generatore di Siti Statici


Un Viaggio di Trent'Anni nel Web

Prima di addentrarci nei dettagli tecnici, lasciatemi raccontare una storia personale. Una storia che probabilmente condivido con molti di voi.

Trent'anni fa, quando ho iniziato a muovere i primi passi sul web, i siti erano statici per definizione. File HTML scritti a mano, magari con qualche immagine GIF animata e uno sfondo piastrellato discutibile. Era un'epoca pionieristica: si imparava l'HTML da tutorial sparsi in rete, si caricavano i file via FTP, e ogni pagina era un piccolo manufatto artigianale. C'era qualcosa di genuino in quella semplicità.

Poi è arrivato WordPress, e con lui la promessa di rendere tutto più facile. E per un po' è stato vero. Ho abbracciato con entusiasmo il mondo dei CMS dinamici: database, PHP, temi, plugin. Potevi fare qualsiasi cosa! Volevi un form di contatto? C'era un plugin. Un carosello? Un plugin. SEO? Plugin. Ogni problema aveva la sua soluzione preconfezionata.

Ma la complessità cresce in modo subdolo. Un plugin tira l'altro, i temi diventano framework, gli aggiornamenti si accumulano, e prima che te ne accorga ti ritrovi a gestire un ecosistema fragile dove ogni update rischia di rompere qualcosa. Ho passato più tempo a debuggare conflitti tra plugin che a scrivere contenuti. La manutenzione era diventata un lavoro... un lavoro che facevo già otto ore al giorno.

C'è stato un periodo di rigetto, lo ammetto. Ho smesso di scrivere sul web anche perché gestire tutta la baracca era diventata una palla. Il sito rimaneva lì, con i suoi plugin obsoleti e le sue vulnerabilità potenziali, come un giardino abbandonato che ti fa sentire in colpa ogni volta che ci passi davanti.

Poi ho deciso di ricominciare, ma con un approccio diverso. Minimalista. Solo quello che mi serve. Ho voluto tornare a quella semplicità originaria, ma con gli strumenti moderni. Niente database, niente CMS pesanti, niente dipendenze che si rompono. Solo file di testo che diventano pagine web. Ed è così che è nato SimplePress.

I Vantaggi dei Siti Statici

Un sito statico è composto solo da file HTML, CSS e JavaScript. Non c'è database, non c'è server applicativo, non ci sono query da ottimizzare. I vantaggi sono significativi:

Velocità: senza elaborazione server-side, le pagine vengono servite istantaneamente. Il tempo di risposta è limitato solo dalla latenza di rete.

Sicurezza: nessun database significa nessuna SQL injection. Nessun CMS significa nessuna vulnerabilità da patchare. La superficie di attacco è ridotta al minimo.

Costi: un sito statico può essere ospitato gratuitamente su GitHub Pages, Netlify, o un semplice bucket S3. Niente costi di server, niente scaling da gestire.

Affidabilità: meno componenti significa meno cose che possono rompersi. Un sito statico su una CDN ha uptime praticamente garantito.

Essere padroni dei propri contenuti

C'è un altro aspetto che mi sta particolarmente a cuore, e che va oltre le considerazioni tecniche: la proprietà dei propri contenuti.

Viviamo in un'epoca in cui affidiamo le nostre parole, i nostri pensieri, le nostre creazioni a piattaforme terze. Social network, servizi di blogging, newsletter platform. È comodo, certo. Qualcun altro si occupa dell'infrastruttura, tu devi solo scrivere.

Ma a che prezzo? I tuoi contenuti esistono finché la piattaforma decide che possono esistere. Un cambio di policy, un algoritmo che decide che sei meno visibile, un servizio che chiude i battenti (vi ricordate Google+? Vine? Tumblr nella sua forma originale?), e anni di impegno possono svanire o diventare inaccessibili.

E poi c'è la questione della monetizzazione dei tuoi contenuti da parte di altri. Quando scrivi su una piattaforma gratuita, il prodotto sei tu. I tuoi testi, le tue interazioni, i tuoi dati alimentano algoritmi pubblicitari e modelli di business che non ti appartengono.

Un sito statico, ospitato su un servizio che controlli tu (anche solo un repository GitHub), ti restituisce questa proprietà. I file sono tuoi, in un formato aperto e portabile. Puoi spostarli dove vuoi, quando vuoi. Puoi fare backup, puoi archiviarli, puoi garantire che esisteranno finché lo deciderai tu.

È per questo che credo che un approccio semplice alla pubblicazione web non sia solo una scelta tecnica, ma anche una scelta di principio. Un modo per riprendersi un pezzetto di sovranità digitale in un mondo dove tendiamo a delegare sempre di più.

Per un blog personale, un portfolio, o un sito di documentazione, queste caratteristiche superano spesso la flessibilità di un CMS tradizionale. Ed è esattamente in questo contesto che nasce SimplePress.


L'Architettura di SimplePress

SimplePress segue un'architettura modulare e pulita, organizzata in componenti con responsabilità ben definite. Vediamo come si struttura il progetto:

simplepress/
├── md-src/              # Sorgenti Markdown
│   ├── 01.md           # Post numerati (ordine cronologico)
│   ├── 02.md           # Altri post...
│   ├── 01-en.md        # Traduzioni inglesi
│   └── chi-sono.md     # Pagina "Chi sono"
├── src/                # Codice TypeScript
│   ├── index.ts        # Entry point CLI
│   ├── site-generator.ts
│   ├── markdown-parser.ts
│   ├── template-engine.ts
│   ├── types.ts
│   └── config.ts
├── build/              # Output HTML generato
└── dist/               # TypeScript compilato

La separazione è netta: i contenuti risiedono in md-src/ come file Markdown, il codice sorgente vive in src/, e l'output finale viene generato in build/. Questo approccio mantiene ogni componente isolato e facilmente sostituibile.

I Moduli TypeScript

Il cuore di SimplePress è composto da sei moduli TypeScript, ognuno con una responsabilità specifica:

types.ts - Le Fondamenta del Sistema dei Tipi

export interface Post {
  id: string;
  title: string;
  titleEn?: string;
  content: string;
  contentEn?: string;
  excerpt: string;
  excerptEn?: string;
  filename: string;
  order: number;
  createdAt: Date;
}

export interface Page {
  id: string;
  title: string;
  titleEn?: string;
  content: string;
  contentEn?: string;
  filename: string;
}

export type Language = 'it' | 'en';

Queste interfacce definiscono il contratto che attraversa l'intera applicazione. Un Post ha un titolo, un contenuto, un excerpt (riassunto), e opzionalmente le versioni inglesi di ciascuno. Il campo order determina l'ordinamento cronologico: i numeri più bassi rappresentano i post più vecchi.

config.ts - La Configurazione Centralizzata

export const siteConfig: SiteConfig = {
  title: 'Danilo Paissan',
  titleEn: 'Danilo Paissan',
  description: 'Trentino dentro, ligure fuori',
  descriptionEn: 'Trentino inside, ligurian outside',
  author: 'Danilo Paissan',
  colorPalette: {
    pearl: '#F8F9FA',
    lightSilver: '#E9ECEF',
    smokeGray: '#CED4DA',
    mediumGray: '#6C757D',
    darkGray: '#212529'
  }
};

La configurazione include anche tutte le traduzioni dell'interfaccia utente: etichette di navigazione, testi del cookie banner, messaggi di sistema. Centralizzare queste informazioni rende semplice personalizzare il sito o aggiungere nuove lingue.


Il Flusso di Generazione

Il processo di generazione segue un flusso lineare e prevedibile. Quando eseguiamo npm run build, accade quanto segue:

Fase 1: Discovery dei Contenuti

Il SiteGenerator inizia esplorando la directory md-src/:

private getPostFiles(): string[] {
  return fs.readdirSync(this.srcDir)
    .filter(file => {
      const isMarkdown = file.endsWith('.md');
      const isNumbered = /^\d+/.test(file);
      const isNotAboutPage = !file.startsWith('chi-sono');
      const isNotEnglishTranslation = !file.endsWith('-en.md');
      return isMarkdown && isNumbered && isNotAboutPage && isNotEnglishTranslation;
    })
    .map(file => path.join(this.srcDir, file));
}

I file vengono filtrati con criteri precisi: solo file Markdown, con nome che inizia con un numero (per i post), escludendo la pagina "Chi sono" e le traduzioni inglesi (che verranno associate ai rispettivi post italiani).

La convenzione di naming è elegante nella sua semplicità: 01.md è il primo post, 01-en.md è la sua traduzione inglese. Il sistema riconosce automaticamente questa relazione e costruisce post bilingui.

Fase 2: Parsing del Markdown

Il MarkdownParser si occupa di trasformare i file Markdown in strutture dati utilizzabili. Utilizza la libreria marked per la conversione HTML, ma aggiunge funzionalità importanti.

Estrazione dei Metadati (Frontmatter)

SimplePress supporta il frontmatter YAML per specificare metadati opzionali:

---
title: Titolo Personalizzato
titleEn: Custom English Title
---

# Il Titolo nel Contenuto
Qui inizia il vero contenuto...

Il parser estrae questi metadati e li separa dal contenuto principale:

private extractMetadata(content: string): { metadata: Record<string, string>, cleanContent: string } {
  const lines = content.split('\n');
  const metadata: Record<string, string> = {};
  
  if (lines[0]?.trim() === '---') {
    const endIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
    if (endIndex > 0) {
      for (let i = 1; i < endIndex; i++) {
        const line = lines[i];
        const colonIndex = line.indexOf(':');
        if (colonIndex > 0) {
          const key = line.substring(0, colonIndex).trim();
          const value = line.substring(colonIndex + 1).trim();
          metadata[key] = value;
        }
      }
      cleanContent = lines.slice(endIndex + 1).join('\n');
    }
  }
  
  return { metadata, cleanContent };
}

Generazione Automatica dell'Excerpt

Per la homepage, ogni post mostra un breve riassunto. Il parser lo genera automaticamente estraendo i primi 200 caratteri del contenuto, rimuovendo i tag HTML:

private generateExcerpt(htmlContent: string, maxLength: number = 200): string {
  const textContent = htmlContent.replace(/<[^>]*>/g, '');
  const firstParagraph = textContent.split('\n\n')[0];
  return firstParagraph.length > maxLength 
    ? firstParagraph.substring(0, maxLength).trim() + '...'
    : firstParagraph;
}

Gestione dei Titoli

Un dettaglio sottile ma importante: il parser rimuove il primo tag H1 dal contenuto HTML. Perché? Il titolo viene già visualizzato dal template della pagina, quindi includerlo anche nel contenuto causerebbe una duplicazione. Questo tipo di attenzione ai dettagli distingue un tool ben progettato.

Fase 3: Generazione dei Template

Il TemplateEngine è il componente più corposo, responsabile di trasformare i dati in pagine HTML complete. Genera tre tipi di pagine:

Homepage (index.html e index-en.html)

Mostra l'elenco di tutti i post con titolo ed excerpt. Per la versione inglese, vengono mostrati solo i post che hanno una traduzione disponibile.

Pagine Post (01.html, 01-en.html, etc.)

Ogni post ha la sua pagina dedicata, con navigazione per tornare alla home e switch di lingua.

Pagine Statiche (chi-sono.html e about.html)

La pagina "Chi sono" segue lo stesso pattern bilingue.

Il Sistema di Stili Inline

Una scelta architetturale interessante: SimplePress include tutti gli stili CSS direttamente nell'HTML, evitando file esterni:

private getCSSStyles(): string {
  const { colorPalette } = siteConfig;
  return `
    :root {
      --color-pearl: ${colorPalette.pearl};
      --color-light-silver: ${colorPalette.lightSilver};
      --color-smoke-gray: ${colorPalette.smokeGray};
      --color-medium-gray: ${colorPalette.mediumGray};
      --color-dark-gray: ${colorPalette.darkGray};
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      line-height: 1.6;
      color: var(--color-dark-gray);
      background-color: var(--color-pearl);
    }
    /* ... altri stili ... */
  `;
}

Questa scelta ha pro e contro. Da un lato, ogni pagina è completamente autonoma (non dipende da file esterni). Dall'altro, gli stili vengono ripetuti in ogni file HTML. Per un blog con poche pagine, il trade-off è favorevole: la semplicità vince sulla dimensione dei file.


Il Supporto Bilingue

Una delle caratteristiche distintive di SimplePress è il supporto nativo per contenuti bilingui (italiano/inglese). L'implementazione è elegante e non invasiva.

Contenuti Bilingui

Ci sono due modi per fornire contenuti in inglese:

1. File Separati: per ogni 01.md, si può creare un 01-en.md con la traduzione completa. Il sistema li associa automaticamente.

2. Frontmatter: per contenuti più brevi, si possono includere le traduzioni direttamente nel frontmatter:

---
titleEn: English Title
contentEn: English content here...
excerptEn: English excerpt...
---

# Titolo Italiano
Contenuto italiano qui...

Lo Switch di Lingua

Ogni pagina include un selettore di lingua nell'header. Il codice JavaScript che gestisce lo switch è sorprendentemente semplice:

function switchLanguage(lang) {
  const currentPath = window.location.pathname;
  const filename = currentPath.split('/').pop() || 'index.html';
  
  let newFilename;
  if (lang === 'en') {
    if (filename === 'index.html') {
      newFilename = 'index-en.html';
    } else if (filename === 'chi-sono.html') {
      newFilename = 'about.html';
    } else if (!filename.includes('-en.html')) {
      newFilename = filename.replace('.html', '-en.html');
    }
  } else {
    // Logica inversa per italiano
  }
  
  window.location.href = newFilename;
}

La logica mappa ogni pagina italiana alla sua controparte inglese seguendo convenzioni consistenti: index.htmlindex-en.html, chi-sono.htmlabout.html, 01.html01-en.html.

Gestione dei Post Non Tradotti

Cosa succede se un utente cerca di accedere alla versione inglese di un post non tradotto? SimplePress genera una pagina "non disponibile" con un messaggio appropriato, invece di mostrare un errore o il contenuto italiano. È un tocco di attenzione all'esperienza utente.


Conformità GDPR: Il Cookie Banner

In un'epoca di crescente attenzione alla privacy, SimplePress include un sistema di gestione dei cookie conforme al GDPR e alla direttiva ePrivacy.

Il Paradosso del Sito Senza Cookie

Ecco un aspetto interessante: SimplePress non utilizza cookie di tracciamento o profilazione. Tecnicamente, potrebbe non servire alcun banner. Tuttavia, la normativa richiede di informare gli utenti anche solo sull'uso di cookie tecnici (come quelli usati per memorizzare il consenso stesso).

L'Implementazione

Il cookie banner appare al primo accesso e offre due opzioni: Accetta o Rifiuta.

function checkCookieConsent() {
  const consent = localStorage.getItem('cookieConsent');
  if (consent === 'rejected') {
    document.body.innerHTML = `
      <div class="cookie-rejected">
        <h2>È necessario accettare i cookie per accedere al sito.</h2>
        <button onclick="resetCookieConsent()">Accetto</button>
      </div>
    `;
    return;
  }
  if (!consent) {
    document.getElementById('cookie-banner').style.display = 'block';
  }
}

Se l'utente rifiuta i cookie, l'accesso al sito viene bloccato. Può sembrare drastico, ma è una scelta lecita: il sito dichiara trasparentemente di non usare cookie di tracciamento, e l'utente può scegliere di non procedere.

Il consenso viene memorizzato in localStorage, che tecnicamente non è un cookie ma uno storage locale del browser. Questa scelta evita il paradosso del "cookie per ricordare che non vuoi cookie".


Il Sistema di Build

SimplePress offre diversi comandi per gestire il ciclo di sviluppo:

npm run setup     # Installa dipendenze e compila
npm run build     # Compila TypeScript e genera il sito
npm run generate  # Solo generazione (TypeScript già compilato)
npm run watch     # Rigenera automaticamente ai cambiamenti
npm run serve     # Server locale per preview
npm run clean     # Rimuove i file generati

Watch Mode

La modalità watch è particolarmente utile durante la scrittura:

watch(): void {
  console.log(`Watching ${this.srcDir} for changes...`);
  
  fs.watch(this.srcDir, { recursive: true }, (eventType, filename) => {
    if (filename && (filename.endsWith('.md') || filename.includes('img/'))) {
      console.log(`File changed: ${filename}`);
      console.log('Regenerating site...');
      this.generate();
    }
  });
}

Ogni modifica a un file Markdown o a un'immagine triggera una rigenerazione completa del sito. Per un blog con poche decine di post, questa operazione è istantanea.

Gestione delle Immagini

SimplePress copia automaticamente le immagini da md-src/img/ a build/img/:

private copyImages(): void {
  const srcImgDir = path.join(this.srcDir, 'img');
  const buildImgDir = path.join(this.buildDir, 'img');

  if (!fs.existsSync(srcImgDir)) {
    return;
  }

  if (!fs.existsSync(buildImgDir)) {
    fs.mkdirSync(buildImgDir, { recursive: true });
  }

  const files = fs.readdirSync(srcImgDir);
  for (const file of files) {
    const srcFile = path.join(srcImgDir, file);
    const buildFile = path.join(buildImgDir, file);
    
    if (fs.statSync(srcFile).isFile()) {
      fs.copyFileSync(srcFile, buildFile);
    }
  }
}

Nel Markdown, le immagini possono essere referenziate con path relativi: ![Descrizione](img/mia-immagine.png).


Il Design System

SimplePress utilizza una palette di colori attentamente selezionata, basata su tonalità di grigio che garantiscono leggibilità e un aspetto professionale:

Nome Colore Uso
Pearl #F8F9FA Sfondo principale
Light Silver #E9ECEF Header e footer
Smoke Gray #CED4DA Bordi
Medium Gray #6C757D Testo secondario
Dark Gray #212529 Testo principale

Questa palette neutra funziona bene per contenuti testuali, dove il focus deve rimanere sul contenuto stesso piuttosto che sull'interfaccia.

Design Responsive

Gli stili includono media query per adattarsi a schermi più piccoli:

@media (max-width: 768px) {
  .nav-links {
    gap: 1rem;
  }
  
  .container {
    padding: 0 15px;
  }
  
  .post-content {
    padding: 1.5rem;
  }
}

Il layout è mobile-first: funziona bene su smartphone e si espande elegantemente su schermi più grandi.


Estendibilità e Personalizzazione

SimplePress è progettato per essere facilmente personalizzabile:

Configurazione: modificando src/config.ts si possono cambiare titolo, descrizione, autore, palette colori e tutte le traduzioni.

Stili: il CSS in template-engine.ts può essere modificato per cambiare completamente l'aspetto del sito.

Struttura: aggiungendo nuovi tipi di pagine in types.ts e relativi metodi di generazione, si può estendere il sistema.

Traduzioni: aggiungendo nuove chiavi in translations, il supporto per altre lingue è immediato (anche se richiederebbe modifiche al type Language).


Conclusioni

SimplePress rappresenta un approccio minimalista ma completo alla generazione di siti statici. Non compete con Hugo o Jekyll per funzionalità, ma offre qualcosa di diverso: un codebase leggibile e modificabile, che può essere compreso interamente in un pomeriggio.

Per chi gestisce un blog personale o un piccolo sito, questo livello di controllo è prezioso. Non ci sono dipendenze misteriose, non ci sono comportamenti magici da debuggare, non ci sono aggiornamenti che rischiano di rompere tutto.

La scelta di TypeScript garantisce type safety e autocompletamento durante lo sviluppo. L'architettura modulare permette di modificare un componente senza impattare gli altri. Il supporto bilingue nativo evita la complessità di plugin esterni. E la conformità GDPR out-of-the-box elimina una preoccupazione comune per i siti europei.


Vuoi provare SimplePress? Il setup è semplice:

git clone [repository]
cd simplepress
npm run setup
# Aggiungi i tuoi contenuti in md-src/
npm run build
npm run serve
# Visita http://localhost:8000

Il tuo blog statico, bilingue e GDPR-compliant è pronto in pochi minuti.