Meu primeiro blog

Enunciado do Problema:
A plataforma não dispõe de um canal de marketing de conteúdo. Um blog permite que cada tenant publique artigos multilíngues otimizados para SEO, redigidos no Strapi CMS e renderizados na aplicação SvelteKit — gerando tráfego orgânico, estabelecendo autoridade e suportando o pipeline de i18n já existente com 13 locales.
Critérios de sucesso
- Posts de blog criados no Strapi são renderizados no frontend dentro da identidade visual do tenant
- Posts são traduzidos automaticamente para os 13 idiomas via o pipeline de webhook existente (sem mudanças na infraestrutura de tradução)
- Páginas de blog ranqueiam em motores de busca (dados estruturados, sitemap, hreflang, og:article)
- Tenants sem o feature flag
blognão exibem rotas de blog (404)
User Stories
- Como autor de conteúdo, quero escrever posts de blog no Strapi com markdown avançado, imagens e vídeos incorporados para poder publicar conteúdo sem tocar em código
- Como visitante do site, quero navegar por posts filtrados por categoria para encontrar conteúdo relevante
- Como visitante do site, quero um índice em posts longos para conseguir navegar rapidamente
- Como visitante do site, quero ler os posts no meu idioma, com tradução automática a partir do inglês
- Como visitante do site, quero partilhar os posts nas redes sociais
- Como visitante do site, quero descobrir posts relacionados e saber mais sobre o autor
- Como gestor de SEO, quero que cada post tenha meta tags corretas, dados estruturados e entradas no sitemap
Requisitos Funcionais
Must Have (MVP)
- Tipos de conteúdo Strapi:
blog-posts,blog-authors,blog-categories— todos com campos tenant, locale, inherit - Página de listagem do blog (
/blog) — grelha paginada de posts, filtro por categoria via query param - Página de detalhe do post (
/blog/[slug]) — renderização completa de markdown com ToC, cartão de autor, posts relacionados, partilha social - Página de categoria (
/blog/category/[slug]) — listagem filtrada reutilizando os mesmos componentes de listagem - Página de autor (
/blog/author/[slug]) — biografia do autor + os seus posts - Tabela de Conteúdos — gerada automaticamente a partir de headings h2/h3, sidebar fixa no desktop, colapsável no mobile, destaque do heading ativo via Intersection Observer
- Embeds de YouTube/Vimeo — URLs simples em markdown convertem-se automaticamente em iframes responsivos (youtube-nocookie.com por motivos de privacidade)
- Tempo de leitura — calculado no servidor a partir da contagem de palavras (Math.ceil(words / 200))
- Botões de partilha social — X, Facebook, LinkedIn, email, copiar link (Web Share API quando disponível)
- Posts relacionados — mesma categoria, excluir o atual, limite de 3, ordenados por publishedAt desc
- Feed RSS (
/blog/rss.xml) — RSS 2.0, locale via query param - SEO — SeoComponent por post, tags og:article, schema JSON-LD BlogPosting, URLs canónicas
- Sitemap — URLs dinâmicas de posts de blog / categoria / autor adicionadas ao sitemap existente com hreflang
- Feature flag —
blog: booleanem TenantConfig.features, guarda de layout devolve 404 quando desativado - i18n — chaves de mensagem Paraglide para todas as strings de UI (nav, labels, paginação), conteúdo Strapi traduzido automaticamente via webhook existente
- Paginação — baseada em páginas usando a paginação nativa do Strapi (9 posts por página)
- Post em destaque — flag booleana
featuredem blog-posts; post em destaque exibido como um cartão hero no topo da página de listagem/blog(cartão maior, largura total, acima da grelha)
Nice to Have (Pós-MVP)
- Pesquisa de texto integral em todos os posts do blog
- Séries de posts / artigos em múltiplas partes
- Barra de progresso de leitura
- Comentários (provavelmente via embed de terceiros)
- CTA de inscrição na newsletter dentro dos posts
- Contador de visualizações / integração de analytics
Requisitos Técnicos
Modelo de Dados (Tipos de Conteúdo Strapi)
blog-posts (api::blog-post.blog-post):
| Campo | Tipo | Localizado | Notas |
|---|---|---|---|
| title | Texto (obrigatório) | Sim | Correspondente na config de tradução |
| slug | UID (a partir de title) | Não | Excluído da tradução |
| excerpt | Texto | Sim | Para cards + meta description |
| body | Rich Text (Markdown) | Sim | Renderizado via marked |
| featuredImage | Media (single) | Não | URL + alternativeText |
| category | Relation (manyToOne -> blog-category) | Não | |
| author | Relation (manyToOne -> blog-author) | Não | |
| featured | Boolean (padrão false) | Não | Post em destaque exibido como card hero na listagem |
| seo | Component (shared.seo) | Sim | Reutiliza o SeoComponent existente |
| tenant | Texto | Não | Filtro padrão de tenant |
| inherit | Boolean (padrão true) | Não | Flag de tradução automática |
blog-authors (api::blog-author.blog-author):
| Campo | Tipo | Localizado | Notas |
|---|---|---|---|
| name | Texto (obrigatório) | Sim | |
| slug | UID (a partir de name) | Não | |
| bio | Rich Text (Markdown) | Sim | |
| avatar | Media (single) | Não | |
| socialLinks | JSON | Não | { twitter?, linkedin?, website? } |
| tenant | Texto | Não | |
| inherit | Boolean (padrão true) | Não |
blog-categories (api::blog-category.blog-category):
| Campo | Tipo | Localizado | Notas |
|---|---|---|---|
| name | Texto (obrigatório) | Sim | |
| slug | UID (a partir de name) | Não | |
| description | Texto | Sim | |
| color | Texto | Não | Cor em hex para badges de UI |
| tenant | Texto | Não | |
| inherit | Boolean (padrão true) | Não |
Interfaces TypeScript
Ver src/lib/services/strapi-types.ts para BlogPost, BlogAuthor, BlogCategory.
Rotas
Todas em src/routes/(marketing)/blog/:
| Rota | Server Load | Finalidade |
|---|---|---|
blog/+layout.server.ts |
Feature guard + carregamento de categorias | Gate + dados de navegação |
blog/+page.server.ts |
Posts paginados, filtro opcional ?category= |
Listagem |
blog/[slug]/+page.server.ts |
Post por slug + posts relacionados + cálculo de tempo de leitura | Detalhe |
blog/category/[slug]/+page.server.ts |
Posts filtrados por categoria | Listagem por categoria |
blog/author/[slug]/+page.server.ts |
Autor + seus posts | Página de autor |
blog/rss.xml/+server.ts |
RSS 2.0 XML, parâmetro ?locale= |
Feed |
Componentes de UI
Todos em src/lib/components/blog/:
| Componente | Props | Notas |
|---|---|---|
BlogPostCard.svelte |
post: BlogPost |
Card com imagem, badge de categoria, título, excerto, autor, data, tempo de leitura |
BlogPostContent.svelte |
body: string |
Markdown para HTML via marked com extensão de incorporação de vídeo + IDs em headings |
TableOfContents.svelte |
entries: TocEntry[] |
Sidebar fixa, Intersection Observer para estado ativo, colapsável em mobile |
AuthorCard.svelte |
author: BlogAuthor |
Avatar, nome, bio, links sociais |
SocialShare.svelte |
url: string, title: string |
Botões de partilha com fallback para Web Share API |
RelatedPosts.svelte |
posts: BlogPost[] |
Grid de BlogPostCard |
BlogPagination.svelte |
pagination: StrapiPagination, basePath: string |
Anterior/próximo + números de página |
Segurança & Autenticação
- Blog é público (sem autenticação necessária) — conteúdo de marketing
- Acesso de leitura à API Strapi através do token
STRAPI_JWTexistente - Embeds de vídeo usam
youtube-nocookie.compara conformidade com o RGPD
Casos de Borda & Tratamento de Erros
- Publicação não encontrada: 404 via SvelteKit
error() - Nenhuma publicação na categoria: Estado vazio com mensagem
blog_no_posts - Imagem de destaque ausente: Cartão é exibido sem imagem
- Tradução não disponível:
loadContentWithFallback()recua para o inglês - Idiomas RTL: Layout respeita
dir="rtl"(ar, he, ur)
Plano de Testes E2E
Os arquivos de teste ficam em tests/blog/. Siga os padrões existentes: parametrização entre diferentes viewports, Axe para acessibilidade (a11y), networkidle aguarda o conteúdo do Strapi.
Testes de Caminho Crítico
- Listagem do blog carrega —
/blogrenderiza uma grade de cards de posts com título, excerto, selo de categoria, autor, data e tempo de leitura - Paginação funciona —
/blog?page=2mostra a segunda página de resultados; os botões anterior/próximo navegam corretamente; a página 1 não tem botão “Anterior” - Filtro por categoria — clicar em um chip de categoria filtra os posts; a URL é atualizada para
?category=<slug>; o chip “Todos” limpa o filtro - Detalhe do post renderiza —
/blog/[slug]mostra título, imagem em destaque, corpo formatado, card do autor, tempo de leitura e data de publicação - Tabela de Conteúdos (ToC) — a página de detalhe do post gera uma ToC a partir dos headings h2/h3; clicar em uma entrada da ToC rola até o heading correspondente
- Posts relacionados — a página de detalhe do post mostra até 3 posts relacionados da mesma categoria (excluindo o atual)
- Página de categoria —
/blog/category/[slug]mostra o nome da categoria, descrição e apenas posts daquela categoria - Página de autor —
/blog/author/[slug]mostra a bio do autor, avatar, links sociais e seus posts - Botões de compartilhamento social — os botões de compartilhamento são visíveis no detalhe do post; “Copiar link” copia a URL e exibe um texto de confirmação
- Feed RSS —
GET /blog/rss.xmlretorna XML válido comContent-Type: application/rss+xmle contém entradas de posts - Navegação — o link de blog no menu do site navega para
/blog; breadcrumbs mostram a hierarquia correta - Post em destaque —
/blogexibe o post em destaque como um card hero maior acima da grade normal
Testes de Casos Limite & Erros
- Post não encontrado —
/blog/nonexistent-slugretorna uma página 404 - Categoria vazia — uma categoria sem posts exibe a mensagem de estado vazio
blog_no_posts - Imagem em destaque ausente — o card do post é renderizado corretamente sem imagem (nenhuma
<img>quebrada) - Autor/categoria ausentes — post sem autor ou categoria é renderizado sem esses elementos (sem crash)
- Feature flag desativada — quando
blog: false,/blogretorna 404
Testes Transversais
- i18n: troca de locale — navegar para
/fr/blog, verificar que as strings de UI (título da página, “Ler mais”, labels de paginação) estão em francês; o conteúdo do post permanece em inglês se não houver tradução - i18n: layout RTL — navegar para
/ar/bloge/ar/blog/[slug], verificar quedir="rtl"está definido e o layout é renderizado corretamente (ToC no lado correto) - SEO: meta tags — a página de detalhe do post tem
og:type=article,og:title,og:description,article:published_timee link canônico - SEO: JSON-LD — a página de detalhe do post contém um
<script type="application/ld+json">com@type: BlogPosting, headline, autor, datePublished - SEO: sitemap —
/sitemap.xmlinclui URLs de posts do blog com alternâncias hreflang - Acessibilidade — rodar auditorias Axe em
/blog,/blog/[slug],/blog/author/[slug]tanto em viewport mobile quanto desktop (seguindo o padrão existente de testes de a11y) - Navegação por teclado — entradas da ToC e botões de paginação são focalizáveis e acionáveis via teclado; os indicadores de foco são visíveis
Fora de Escopo
- Comentários em posts de blog
- Busca em texto integral
- Integração de newsletter dentro dos posts
- Análises/contagem de visualizações
- Modelos de post personalizados por tenant
Ludo é um educador e empreendedor na área de tecnologia, movido por uma crença simples: educação de qualidade deve ser acessível a todos.