Saltar al contenido
IA

Mi primer blog

Ludo Fourrage Ludo Fourrage 8 de abril de 2026 10 min de lectura
Mi primer blog

Planteamiento del problema:

La plataforma carece de un canal de content marketing. Un blog permite que cada tenant publique artículos multilingües optimizados para SEO, redactados en Strapi CMS y renderizados en la aplicación SvelteKit, lo que impulsa el tráfico orgánico, consolida autoridad y aprovecha la canalización i18n existente de 13 configuraciones regionales.

Criterios de éxito

  • Las entradas de blog creadas en Strapi se renderizan en el frontend con la identidad de marca del tenant
  • Las entradas se traducen automáticamente a los 13 idiomas a través del pipeline de webhooks existente (sin cambios en la infraestructura de traducción)
  • Las páginas de blog posicionan en buscadores (datos estructurados, sitemap, hreflang, og:article)
  • Los tenants sin el feature flag blog no tienen rutas de blog (404)

Historias de usuario

  • Como autor de contenido, quiero escribir entradas de blog en Strapi con markdown enriquecido, imágenes y vídeos incrustados para poder publicar contenido sin tocar código.
  • Como visitante del sitio, quiero explorar las entradas del blog filtradas por categoría para poder encontrar contenido relevante.
  • Como visitante del sitio, quiero una tabla de contenidos en las entradas largas para poder navegar rápidamente.
  • Como visitante del sitio, quiero leer las entradas en mi idioma, con traducción automática desde el inglés.
  • Como visitante del sitio, quiero compartir las entradas en redes sociales.
  • Como visitante del sitio, quiero descubrir entradas relacionadas y conocer al autor.
  • Como responsable de SEO, quiero que cada entrada tenga metatags correctas, datos estructurados y entradas en el sitemap.

Requisitos funcionales

Imprescindible (MVP)

  1. Tipos de contenido en Strapi: blog-posts, blog-authors, blog-categories — todos con campos de tenant, locale e inherit
  2. Página de listado del blog (/blog) — grid paginado de posts, filtro por categoría mediante query param
  3. Página de detalle del post (/blog/[slug]) — renderizado completo de markdown con ToC, tarjeta de autor, posts relacionados y opción de compartir en redes sociales
  4. Página de categoría (/blog/category/[slug]) — listado filtrado reutilizando los mismos componentes de listado
  5. Página de autor (/blog/author/[slug]) — biografía del autor + sus posts
  6. Tabla de contenidos — generada automáticamente a partir de los encabezados h2/h3, sidebar fija en escritorio, colapsable en móvil, resaltado del encabezado activo mediante Intersection Observer
  7. Embeds de YouTube/Vimeo — las URLs simples en markdown se convierten automáticamente en iframes responsive (youtube-nocookie.com por privacidad)
  8. Tiempo de lectura — calculado en el servidor a partir del recuento de palabras (Math.ceil(words / 200))
  9. Botones para compartir en redes — X, Facebook, LinkedIn, email, copiar enlace (Web Share API cuando esté disponible)
  10. Posts relacionados — misma categoría, excluir el actual, límite 3, ordenados por publishedAt desc
  11. Feed RSS (/blog/rss.xml) — RSS 2.0, locale mediante query param
  12. SEO — SeoComponent por post, etiquetas og:article, esquema JSON-LD BlogPosting, URLs canónicas
  13. Sitemap — URLs dinámicas de posts de blog/categorías/autores añadidas al sitemap existente con hreflang
  14. Feature flagblog: boolean en TenantConfig.features, el layout guard devuelve 404 cuando está deshabilitado
  15. i18n — claves de mensajes Paraglide para todos los textos de la interfaz (navegación, etiquetas, paginación), contenido de Strapi traducido automáticamente mediante el webhook existente
  16. Paginación — basada en páginas usando la paginación nativa de Strapi (9 posts por página)
  17. Post destacado — flag booleano featured en blog-posts; el post destacado se muestra como tarjeta hero en la parte superior de la página de listado /blog (tarjeta más grande, a ancho completo, por encima del grid)

Deseable (Post-MVP)

  1. Búsqueda de texto completo en todos los posts del blog
  2. Series de posts / artículos en varias partes
  3. Barra de progreso de lectura
  4. Comentarios (probablemente mediante un embed de terceros)
  5. CTA de suscripción a newsletter dentro de los posts
  6. Contador de visitas / integración de analítica

Requisitos técnicos

Modelo de datos (Tipos de contenido de Strapi)

blog-posts (api::blog-post.blog-post):

Campo Tipo Localizado Notas
title Texto (obligatorio) Vinculado por la configuración de traducción
slug UID (desde title) No Excluido de la traducción
excerpt Texto Para tarjetas + meta descripción
body Rich Text (Markdown) Renderizado vía marked
featuredImage Media (único) No URL + alternativeText
category Relación (manyToOne -> blog-category) No
author Relación (manyToOne -> blog-author) No
featured Boolean (por defecto false) No Publicación destacada mostrada como tarjeta hero en el listado
seo Componente (shared.seo) Reutiliza el SeoComponent existente
tenant Texto No Filtro tenant estándar
inherit Boolean (por defecto true) No Indicador de auto-traducción

blog-authors (api::blog-author.blog-author):

Campo Tipo Localizado Notas
name Texto (obligatorio)
slug UID (desde name) No
bio Rich Text (Markdown)
avatar Media (único) No
socialLinks JSON No { twitter?, linkedin?, website? }
tenant Texto No
inherit Boolean (por defecto true) No

blog-categories (api::blog-category.blog-category):

Campo Tipo Localizado Notas
name Texto (obligatorio)
slug UID (desde name) No
description Texto
color Texto No Color hex para insignias de la UI
tenant Texto No
inherit Boolean (por defecto true) No

Interfaces TypeScript

Ver src/lib/services/strapi-types.ts para BlogPost, BlogAuthor, BlogCategory.

Rutas

Todas bajo src/routes/(marketing)/blog/:

Ruta Server Load Propósito
blog/+layout.server.ts Feature guard + carga de categorías Puerta de acceso + datos de navegación
blog/+page.server.ts Publicaciones paginadas, filtro ?category= opcional Listado
blog/[slug]/+page.server.ts Publicación por slug + publicaciones relacionadas + cálculo de tiempo de lectura Detalle
blog/category/[slug]/+page.server.ts Publicaciones filtradas por categoría Listado por categoría
blog/author/[slug]/+page.server.ts Autor + sus publicaciones Página de autor
blog/rss.xml/+server.ts RSS 2.0 XML, parámetro ?locale= Feed

Componentes de UI

Todos en src/lib/components/blog/:

Componente Props Notas
BlogPostCard.svelte post: BlogPost Tarjeta con imagen, distintivo de categoría, título, extracto, autor, fecha, tiempo de lectura
BlogPostContent.svelte body: string Markdown a HTML vía marked con extensión para incrustar vídeo + IDs de encabezado
TableOfContents.svelte entries: TocEntry[] Barra lateral fija (sticky), Intersection Observer para el estado activo, colapsable en móvil
AuthorCard.svelte author: BlogAuthor Avatar, nombre, biografía, enlaces sociales
SocialShare.svelte url: string, title: string Botones para compartir con fallback de Web Share API
RelatedPosts.svelte posts: BlogPost[] Grid de BlogPostCard
BlogPagination.svelte pagination: StrapiPagination, basePath: string Anterior/siguiente + números de página

Seguridad y autenticación

  • El blog es público (no se requiere autenticación): contenido de marketing
  • Acceso de lectura a la API de Strapi mediante el token existente STRAPI_JWT
  • Las incrustaciones de vídeo usan youtube-nocookie.com para el cumplimiento del RGPD

Casos límite y manejo de errores

  • Publicación no encontrada: 404 a través de SvelteKit error()
  • Sin publicaciones en la categoría: estado vacío con mensaje blog_no_posts
  • Imagen destacada ausente: la tarjeta se muestra sin imagen
  • Traducción no disponible: loadContentWithFallback() hace fallback al inglés
  • Idiomas RTL: el diseño respeta dir="rtl" (ar, he, ur)

Plan de pruebas E2E

Los archivos de prueba se encuentran en tests/blog/. Sigue los patrones existentes: parametrización en distintos tamaños de pantalla, Axe para a11y, networkidle espera al contenido de Strapi.

Pruebas de ruta crítica

  1. Carga del listado de blog/blog renderiza una cuadrícula de tarjetas de post con título, extracto, distintivo de categoría, autor, fecha y tiempo de lectura
  2. La paginación funciona/blog?page=2 muestra la segunda página de resultados; los botones anterior/siguiente navegan correctamente; la página 1 no tiene botón de «Anterior»
  3. Filtro por categoría — al hacer clic en una chip de categoría se filtran los posts; la URL se actualiza a ?category=<slug>; la chip «All» limpia el filtro
  4. Detalle de post se renderiza/blog/[slug] muestra título, imagen destacada, cuerpo formateado, tarjeta de autor, tiempo de lectura y fecha de publicación
  5. Tabla de contenidos — la página de detalle del post genera un ToC a partir de los encabezados h2/h3; al hacer clic en una entrada del ToC se hace scroll hasta el encabezado
  6. Posts relacionados — el detalle del post muestra hasta 3 posts relacionados de la misma categoría (excluyendo el actual)
  7. Página de categoría/blog/category/[slug] muestra nombre de la categoría, descripción y solo posts de esa categoría
  8. Página de autor/blog/author/[slug] muestra biografía del autor, avatar, enlaces sociales y sus posts
  9. Botones para compartir en redes — los botones de compartir son visibles en el detalle del post; «Copiar enlace» copia la URL y muestra un texto de confirmación
  10. RSS feedGET /blog/rss.xml devuelve XML válido con Content-Type: application/rss+xml y contiene entradas de post
  11. Navegación — el enlace al blog en el menú del sitio navega a /blog; las migas de pan muestran la jerarquía correcta
  12. Post destacado/blog muestra el post destacado como una tarjeta hero más grande por encima de la cuadrícula normal

Pruebas de casos límite y errores

  1. Post no encontrado/blog/nonexistent-slug devuelve una página 404
  2. Categoría vacía — una categoría sin posts muestra el mensaje de estado vacío blog_no_posts
  3. Imagen destacada ausente — la tarjeta de post se renderiza correctamente sin imagen (sin <img> roto)
  4. Autor/categoría ausentes — un post sin autor o categoría se renderiza sin esos elementos (sin fallo)
  5. Feature flag desactivada — cuando blog: false, /blog devuelve 404

Pruebas transversales

  1. i18n: cambio de locale — navega a /fr/blog, verifica que las cadenas de la interfaz (título de la página, «Leer más», etiquetas de paginación) estén en francés; el contenido del post vuelve al inglés si no está traducido
  2. i18n: diseño RTL — navega a /ar/blog y /ar/blog/[slug], verifica que dir="rtl" esté configurado y que el diseño se renderice correctamente (ToC en el lado correcto)
  3. SEO: meta tags — la página de detalle del post tiene og:type=article, og:title, og:description, article:published_time y enlace canónico
  4. SEO: JSON-LD — la página de detalle del post contiene un <script type="application/ld+json"> con @type: BlogPosting, headline, author, datePublished
  5. SEO: sitemap/sitemap.xml incluye URLs de posts del blog con alternancias hreflang
  6. Accesibilidad — ejecuta auditorías Axe en /blog, /blog/[slug], /blog/author/[slug] tanto en móviles como en escritorio (siguiendo el patrón de pruebas de a11y existente)
  7. Navegación por teclado — las entradas del ToC y los botones de paginación se pueden enfocar y activar mediante teclado; los indicadores de foco son visibles

Fuera de alcance

  • Comentarios en las entradas del blog
  • Búsqueda de texto completo
  • Integración de newsletter dentro de las entradas
  • Analíticas / recuento de visualizaciones
  • Plantillas de entradas personalizadas por tenant
Ludo Fourrage
Ludo Fourrage

Ludo es educador y emprendedor en el campo de la tecnología, guiado por una creencia sencilla: la educación de calidad debe ser accesible para todos.