Il mio primo blog

Definizione del problema:
La piattaforma non dispone di un canale di content marketing. Un blog permette a ogni tenant di pubblicare articoli multilingue ottimizzati per la SEO, creati in Strapi CMS e renderizzati nell’app SvelteKit — generando traffico organico, costruendo autorevolezza e supportando l’attuale pipeline i18n con 13 locale.
Criteri di successo
- I post del blog creati in Strapi vengono visualizzati nel frontend con il branding del tenant
- I post vengono tradotti automaticamente in tutte le 13 lingue tramite la pipeline webhook esistente (nessuna modifica all’infrastruttura di traduzione)
- Le pagine del blog si posizionano sui motori di ricerca (structured data, sitemap, hreflang, og:article)
- I tenant senza il feature flag
blognon vedono alcuna route del blog (404)
User Stories
- Come autore di contenuti, voglio scrivere articoli del blog in Strapi con markdown avanzato, immagini e video incorporati così posso pubblicare contenuti senza toccare il codice
- Come visitatore del sito, voglio sfogliare gli articoli del blog filtrandoli per categoria così posso trovare contenuti rilevanti
- Come visitatore del sito, voglio un indice dei contenuti negli articoli lunghi così posso navigare rapidamente
- Come visitatore del sito, voglio leggere gli articoli nella mia lingua, con traduzione automatica dall’inglese
- Come visitatore del sito, voglio condividere gli articoli sui social media
- Come visitatore del sito, voglio scoprire articoli correlati e conoscere l’autore
- Come SEO manager, voglio che ogni articolo abbia meta tag corretti, dati strutturati e voci nella sitemap
Requisiti Funzionali
Must Have (MVP)
- Content type Strapi:
blog-posts,blog-authors,blog-categories— tutti con campi tenant, locale, inherit - Pagina elenco blog (
/blog) — griglia paginata di post, filtro per categoria via query param - Pagina dettaglio post (
/blog/[slug]) — rendering completo del markdown con ToC, scheda autore, post correlati, condivisione social - Pagina categoria (
/blog/category/[slug]) — elenco filtrato che riutilizza gli stessi componenti di listing - Pagina autore (
/blog/author/[slug]) — bio dell’autore + i suoi post - Table of Contents — generata automaticamente dagli heading h2/h3, sidebar fissa su desktop, comprimibile su mobile, evidenziazione dell’heading attivo via Intersection Observer
- Embed YouTube/Vimeo — URL “nudi” nel markdown si convertono automaticamente in iframe responsive (youtube-nocookie.com per la privacy)
- Tempo di lettura — calcolato server-side dal conteggio parole (Math.ceil(words / 200))
- Pulsanti di condivisione social — X, Facebook, LinkedIn, email, copia-link (Web Share API dove disponibile)
- Post correlati — stessa categoria, escluso il corrente, limite 3, ordinati per publishedAt desc
- Feed RSS (
/blog/rss.xml) — RSS 2.0, locale via query param - SEO — SeoComponent per ogni post, tag og:article, schema JSON-LD BlogPosting, URL canoniche
- Sitemap — URL dinamiche di post blog/categorie/autori aggiunte alla sitemap esistente con hreflang
- Feature flag —
blog: booleanin TenantConfig.features, layout guard restituisce 404 quando disabilitata - i18n — chiavi di messaggio Paraglide per tutte le stringhe UI (nav, label, paginazione), contenuti Strapi tradotti automaticamente tramite webhook esistente
- Paginazione — basata su pagina usando la paginazione nativa di Strapi (9 post per pagina)
- Post in evidenza — flag booleano
featuredsu blog-posts; il post in evidenza viene mostrato come hero card in cima alla pagina di listing/blog(card più grande, full-width, sopra la griglia)
Nice to Have (Post-MVP)
- Ricerca full-text sui post del blog
- Serie di post / articoli in più parti
- Barra di avanzamento lettura
- Commenti (probabilmente tramite embed di terze parti)
- CTA di iscrizione alla newsletter all’interno dei post
- Conteggio delle visualizzazioni / integrazione analytics
Requisiti tecnici
Modello dati (Tipi di contenuto Strapi)
blog-posts (api::blog-post.blog-post):
| Campo | Tipo | Localizzato | Note |
|---|---|---|---|
| title | Text (obbligatorio) | Sì | Allineato tramite config di traduzione |
| slug | UID (da title) | No | Escluso dalla traduzione |
| excerpt | Text | Sì | Per card + meta description |
| body | Rich Text (Markdown) | Sì | Renderizzato tramite marked |
| featuredImage | Media (single) | No | URL + alternativeText |
| category | Relation (manyToOne -> blog-category) | No | |
| author | Relation (manyToOne -> blog-author) | No | |
| featured | Boolean (default false) | No | Post in evidenza mostrato come hero card nella lista |
| seo | Component (shared.seo) | Sì | Riusa il componente SeoComponent esistente |
| tenant | Text | No | Filtro tenant standard |
| inherit | Boolean (default true) | No | Flag per auto-traduzione |
blog-authors (api::blog-author.blog-author):
| Campo | Tipo | Localizzato | Note |
|---|---|---|---|
| name | Text (obbligatorio) | Sì | |
| slug | UID (da name) | No | |
| bio | Rich Text (Markdown) | Sì | |
| avatar | Media (single) | No | |
| socialLinks | JSON | No | { twitter?, linkedin?, website? } |
| tenant | Text | No | |
| inherit | Boolean (default true) | No |
blog-categories (api::blog-category.blog-category):
| Campo | Tipo | Localizzato | Note |
|---|---|---|---|
| name | Text (obbligatorio) | Sì | |
| slug | UID (da name) | No | |
| description | Text | Sì | |
| color | Text | No | Colore esadecimale per badge UI |
| tenant | Text | No | |
| inherit | Boolean (default true) | No |
Interfacce TypeScript
Vedi src/lib/services/strapi-types.ts per BlogPost, BlogAuthor, BlogCategory.
Route
Tutte sotto src/routes/(marketing)/blog/:
| Route | Server Load | Scopo |
|---|---|---|
blog/+layout.server.ts |
Feature guard + caricamento categorie | Gate + dati di navigazione |
blog/+page.server.ts |
Post paginati, filtro opzionale ?category= |
Listing |
blog/[slug]/+page.server.ts |
Post per slug + post correlati + calcolo tempo di lettura | Dettaglio |
blog/category/[slug]/+page.server.ts |
Post filtrati per categoria | Listing per categoria |
blog/author/[slug]/+page.server.ts |
Autore + i suoi post | Pagina autore |
blog/rss.xml/+server.ts |
RSS 2.0 XML, parametro ?locale= |
Feed |
Componenti UI
Tutti in src/lib/components/blog/:
| Componente | Props | Note |
|---|---|---|
BlogPostCard.svelte |
post: BlogPost |
Card con immagine, badge categoria, titolo, excerpt, autore, data, tempo di lettura |
BlogPostContent.svelte |
body: string |
Markdown in HTML tramite marked con estensione per embed video + ID sugli heading |
TableOfContents.svelte |
entries: TocEntry[] |
Sidebar fissa, Intersection Observer per lo stato attivo, comprimibile su mobile |
AuthorCard.svelte |
author: BlogAuthor |
Avatar, nome, bio, link social |
SocialShare.svelte |
url: string, title: string |
Pulsanti di condivisione con fallback Web Share API |
RelatedPosts.svelte |
posts: BlogPost[] |
Griglia di BlogPostCard |
BlogPagination.svelte |
pagination: StrapiPagination, basePath: string |
Precedente/successiva + numeri di pagina |
Sicurezza e autenticazione
- Il blog è pubblico (non è richiesta autenticazione) — contenuti di marketing
- Accesso in lettura all’API Strapi tramite il token
STRAPI_JWTesistente - Gli embed video utilizzano
youtube-nocookie.comper la conformità al GDPR
Casi limite e gestione degli errori
- Post non trovato: 404 tramite SvelteKit
error() - Nessun post nella categoria: stato vuoto con messaggio
blog_no_posts - Immagine in evidenza mancante: la card viene renderizzata senza immagine
- Traduzione non disponibile:
loadContentWithFallback()esegue il fallback all'inglese - Lingue RTL: il layout rispetta
dir="rtl"(ar, he, ur)
Piano di test E2E
I file di test si trovano in tests/blog/. Segui i pattern esistenti: parametrizzazione sui diversi viewport, Axe per l’accessibilità (a11y), networkidle attende il contenuto da Strapi.
Test del percorso critico
- Caricamento lista blog —
/blogrende una griglia di card dei post con titolo, estratto, badge categoria, autore, data e tempo di lettura - Funzionamento della paginazione —
/blog?page=2mostra la seconda pagina di risultati; i pulsanti precedente/successivo navigano correttamente; la pagina 1 non ha il pulsante "Precedente" - Filtro per categoria — cliccando su una chip di categoria i post vengono filtrati; l’URL si aggiorna a
?category=<slug>; la chip "Tutti" rimuove il filtro - Rendering dettaglio post —
/blog/[slug]mostra titolo, immagine in evidenza, corpo formattato, scheda autore, tempo di lettura e data di pubblicazione - Indice (Table of Contents) — la pagina di dettaglio genera l’Indice a partire dai titoli h2/h3; cliccando una voce dell’Indice la pagina scorre fino al titolo corrispondente
- Post correlati — il dettaglio post mostra fino a 3 post correlati della stessa categoria (escludendo quello corrente)
- Pagina categoria —
/blog/category/[slug]mostra nome categoria, descrizione e solo i post di quella categoria - Pagina autore —
/blog/author/[slug]mostra bio dell’autore, avatar, link social e i suoi post - Pulsanti di condivisione social — i pulsanti di condivisione sono visibili sul dettaglio post; "Copia link" copia l’URL e mostra un messaggio di conferma
- Feed RSS —
GET /blog/rss.xmlrestituisce XML valido conContent-Type: application/rss+xmle contiene le entry dei post - Navigazione — il link al blog nel menu del sito porta a
/blog; i breadcrumb mostrano la gerarchia corretta - Post in evidenza —
/blogvisualizza il post in evidenza come card hero più grande sopra la griglia normale
Test di edge case ed errori
- Post non trovato —
/blog/nonexistent-slugrestituisce una pagina 404 - Categoria vuota — una categoria senza post mostra il messaggio di stato vuoto
blog_no_posts - Immagine in evidenza mancante — la card del post viene renderizzata correttamente anche senza immagine (nessuna
<img>rotta) - Autore/categoria mancanti — un post senza autore o categoria viene renderizzato senza quegli elementi (nessun crash)
- Feature flag disabilitato — quando
blog: false,/blogrestituisce 404
Test trasversali
- i18n: cambio lingua — navigare a
/fr/blog, verificare che le stringhe UI (titolo pagina, "Leggi di più", etichette della paginazione) siano in francese; il contenuto del post ricade sull’inglese se non tradotto - i18n: layout RTL — navigare a
/ar/bloge/ar/blog/[slug], verificare chedir="rtl"sia impostato e che il layout venga renderizzato correttamente (Indice sul lato corretto) - SEO: meta tag — la pagina di dettaglio del post ha
og:type=article,og:title,og:description,article:published_timee il link canonico - SEO: JSON-LD — la pagina di dettaglio del post contiene un
<script type="application/ld+json">con@type: BlogPosting, headline, autore, datePublished - SEO: sitemap —
/sitemap.xmlinclude gli URL dei post del blog con le varianti hreflang - Accessibilità — eseguire audit Axe su
/blog,/blog/[slug],/blog/author/[slug]sia su viewport mobile sia desktop (seguendo il pattern di test a11y esistente) - Navigazione da tastiera — le voci dell’Indice e i pulsanti di paginazione sono focalizzabili e attivabili via tastiera; gli indicatori di focus sono visibili
Fuori ambito
- Commenti ai post del blog
- Ricerca full-text
- Integrazione newsletter all'interno dei post
- Analitiche / conteggio visualizzazioni
- Modelli di post personalizzati per tenant
Ludo è un formatore e imprenditore nel settore tecnologico, guidato da una semplice convinzione: un'istruzione di qualità dovrebbe essere accessibile a tutti.