Skip to content
AI

My first blog

Ludo Fourrage Ludo Fourrage April 8, 2026 9 min read
My first blog

Problem Statement:

The platform lacks a content marketing channel. A blog enables each tenant to publish SEO-optimized, multilingual articles authored in Strapi CMS and rendered in the SvelteKit app — driving organic traffic, establishing authority, and supporting the existing 13-locale i18n pipeline.

Success Criteria

  • Blog posts authored in Strapi are rendered on the frontend within the tenant's branding
  • Posts auto-translate to all 13 locales via the existing webhook pipeline (no changes to translation infra)
  • Blog pages rank in search engines (structured data, sitemap, hreflang, og:article)
  • Tenants without the blog feature flag see no blog routes (404)

User Stories

  • As a content author, I want to write blog posts in Strapi with rich markdown, images, and video embeds so I can publish content without touching code
  • As a site visitor, I want to browse blog posts filtered by category so I can find relevant content
  • As a site visitor, I want a table of contents on long posts so I can navigate quickly
  • As a site visitor, I want to read posts in my language, with automatic translation from English
  • As a site visitor, I want to share posts on social media
  • As a site visitor, I want to discover related posts and learn about the author
  • As an SEO manager, I want each post to have proper meta tags, structured data, and sitemap entries

Functional Requirements

Must Have (MVP)

  1. Strapi content types: blog-posts, blog-authors, blog-categories — all with tenant, locale, inherit fields
  2. Blog listing page (/blog) — paginated grid of posts, category filter via query param
  3. Post detail page (/blog/[slug]) — full markdown rendering with ToC, author card, related posts, social share
  4. Category page (/blog/category/[slug]) — filtered listing reusing the same listing components
  5. Author page (/blog/author/[slug]) — author bio + their posts
  6. Table of Contents — auto-generated from h2/h3 headings, sticky sidebar on desktop, collapsible on mobile, active heading highlight via Intersection Observer
  7. YouTube/Vimeo embeds — bare URLs in markdown auto-convert to responsive iframes (youtube-nocookie.com for privacy)
  8. Read time — calculated server-side from word count (Math.ceil(words / 200))
  9. Social share buttons — X, Facebook, LinkedIn, email, copy-link (Web Share API where available)
  10. Related posts — same category, exclude current, limit 3, sorted by publishedAt desc
  11. RSS feed (/blog/rss.xml) — RSS 2.0, locale via query param
  12. SEO — SeoComponent per post, og:article tags, BlogPosting JSON-LD schema, canonical URLs
  13. Sitemap — dynamic blog post/category/author URLs added to existing sitemap with hreflang
  14. Feature flagblog: boolean in TenantConfig.features, layout guard returns 404 when disabled
  15. i18n — Paraglide message keys for all UI strings (nav, labels, pagination), Strapi content auto-translated via existing webhook
  16. Pagination — page-based using Strapi's native pagination (9 posts per page)
  17. Featured post — boolean featured flag on blog-posts; featured post displayed as a hero card at the top of the /blog listing page (larger card, full-width, above the grid)

Nice to Have (Post-MVP)

  1. Full-text search across blog posts
  2. Post series / multi-part articles
  3. Reading progress bar
  4. Comments (likely via third-party embed)
  5. Newsletter signup CTA within posts
  6. View count / analytics integration

Technical Requirements

Data Model (Strapi Content Types)

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

Field Type Localized Notes
title Text (required) Yes Matched by translation config
slug UID (from title) No Excluded from translation
excerpt Text Yes For cards + meta description
body Rich Text (Markdown) Yes Rendered via marked
featuredImage Media (single) No URL + alternativeText
category Relation (manyToOne -> blog-category) No
author Relation (manyToOne -> blog-author) No
featured Boolean (default false) No Featured post shown as hero card on listing
seo Component (shared.seo) Yes Reuse existing SeoComponent
tenant Text No Standard tenant filter
inherit Boolean (default true) No Auto-translation flag

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

Field Type Localized Notes
name Text (required) Yes
slug UID (from name) No
bio Rich Text (Markdown) Yes
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):

Field Type Localized Notes
name Text (required) Yes
slug UID (from name) No
description Text Yes
color Text No Hex color for UI badges
tenant Text No
inherit Boolean (default true) No

TypeScript Interfaces

See src/lib/services/strapi-types.ts for BlogPost, BlogAuthor, BlogCategory.

Routes

All under src/routes/(marketing)/blog/:

Route Server Load Purpose
blog/+layout.server.ts Feature guard + load categories Gate + nav data
blog/+page.server.ts Paginated posts, optional ?category= filter Listing
blog/[slug]/+page.server.ts Post by slug + related posts + read time calc Detail
blog/category/[slug]/+page.server.ts Posts filtered by category Category listing
blog/author/[slug]/+page.server.ts Author + their posts Author page
blog/rss.xml/+server.ts RSS 2.0 XML, ?locale= param Feed

UI Components

All in src/lib/components/blog/:

Component Props Notes
BlogPostCard.svelte post: BlogPost Card with image, category badge, title, excerpt, author, date, read time
BlogPostContent.svelte body: string Markdown to HTML via marked with video embed extension + heading IDs
TableOfContents.svelte entries: TocEntry[] Sticky sidebar, Intersection Observer for active state, mobile collapsible
AuthorCard.svelte author: BlogAuthor Avatar, name, bio, social links
SocialShare.svelte url: string, title: string Share buttons with Web Share API fallback
RelatedPosts.svelte posts: BlogPost[] Grid of BlogPostCard
BlogPagination.svelte pagination: StrapiPagination, basePath: string Prev/next + page numbers

Security & Auth

  • Blog is public (no auth required) — marketing content
  • Strapi API read access via existing STRAPI_JWT token
  • Video embeds use youtube-nocookie.com for GDPR compliance

Edge Cases & Error Handling

  • Post not found: 404 via SvelteKit error()
  • No posts in category: Empty state with blog_no_posts message
  • Missing featured image: Card renders without image
  • Translation not available: loadContentWithFallback() falls back to English
  • RTL languages: Layout respects dir="rtl" (ar, he, ur)

E2E Test Plan

Test files live in tests/blog/. Follow existing patterns: parameterized across viewports, Axe for a11y, networkidle waits for Strapi content.

Critical Path Tests

  1. Blog listing loads/blog renders a grid of post cards with title, excerpt, category badge, author, date, and read time
  2. Pagination works/blog?page=2 shows the second page of results; prev/next buttons navigate correctly; page 1 has no "Previous" button
  3. Category filter — clicking a category chip filters posts; URL updates to ?category=<slug>; "All" chip clears the filter
  4. Post detail renders/blog/[slug] shows title, featured image, formatted body, author card, read time, and published date
  5. Table of Contents — post detail page generates ToC from h2/h3 headings; clicking a ToC entry scrolls to the heading
  6. Related posts — post detail shows up to 3 related posts from the same category (excluding current)
  7. Category page/blog/category/[slug] shows category name, description, and only posts from that category
  8. Author page/blog/author/[slug] shows author bio, avatar, social links, and their posts
  9. Social share buttons — share buttons are visible on post detail; "Copy link" copies the URL and shows confirmation text
  10. RSS feedGET /blog/rss.xml returns valid XML with Content-Type: application/rss+xml and contains post entries
  11. Navigation — blog link in the site menu navigates to /blog; breadcrumbs show correct hierarchy
  12. Featured post/blog displays the featured post as a larger hero card above the regular grid

Edge Case & Error Tests

  1. Post not found/blog/nonexistent-slug returns a 404 page
  2. Empty category — a category with no posts shows the blog_no_posts empty state message
  3. Missing featured image — post card renders gracefully without an image (no broken <img>)
  4. Missing author/category — post without author or category renders without those elements (no crash)
  5. Feature flag disabled — when blog: false, /blog returns 404

Cross-Cutting Tests

  1. i18n: locale switch — navigate to /fr/blog, verify UI strings (page title, "Read more", pagination labels) are in French; post content falls back to English if untranslated
  2. i18n: RTL layout — navigate to /ar/blog and /ar/blog/[slug], verify dir="rtl" is set and layout renders correctly (ToC on correct side)
  3. SEO: meta tags — post detail page has og:type=article, og:title, og:description, article:published_time, and canonical link
  4. SEO: JSON-LD — post detail page contains a <script type="application/ld+json"> with @type: BlogPosting, headline, author, datePublished
  5. SEO: sitemap/sitemap.xml includes blog post URLs with hreflang alternates
  6. Accessibility — run Axe audits on /blog, /blog/[slug], /blog/author/[slug] at both mobile and desktop viewports (follows existing a11y test pattern)
  7. Keyboard navigation — ToC entries and pagination buttons are focusable and activatable via keyboard; focus indicators are visible

Out of Scope

  • Blog post comments
  • Full-text search
  • Newsletter integration within posts
  • Analytics/view counts
  • Custom post templates per tenant
Ludo Fourrage
Ludo Fourrage

Ludo is an educator and entrepreneur in the technology field, driven by a simple belief: quality education should be accessible to everyone.