This site is a headless architecture: Strapi v5 stores the content and exposes a REST API, while Astro consumes it at build time to produce a fully static site. Here is how a single article travels from the CMS database to the HTML you are reading.
1. The fetch helper
Every request to Strapi goes through one typed wrapper, fetchAPI<T>(). It takes an API path like /api/articles, serializes query params with the qs library, attaches a Bearer token when one is set, and returns the JSON as a typed promise T. It also handles the messy parts of production fetching: an AbortController enforces a 10-second timeout, and a retry loop with exponential backoff re-attempts on 408, 429, and 5xx responses. A 404 short-circuits into a ContentNotFoundError so the calling query can decide whether that is fatal or just means "no content yet".
2. Typed queries
On top of that helper sit focused query functions, one per content type. getArticles() and getArticleBySlug(slug) both hit /api/articles, but they shape the request differently. Strapi returns relations as IDs by default, so each query passes a populate array to pull nested fields in a single request — cover, category, author.avatar, and the dynamic blocks.file / blocks.files for media inside the article body. Results are validated against a StrapiListResponse<T> shape and narrowed to typed models like Article, so the rest of the app never touches any.
3. Build-time fetching
Because Astro is used in static mode, data fetching happens once at build time, not on every request. The blog index calls getArticles(20) at the top of the frontmatter. The detail page goes further: its getStaticPaths() function calls getAllArticles(), which follows Strapi's pagination across every page so a route is generated for each article slug. The fetched Article object is passed straight through as a page prop — no client-side fetch, no loading state.
4. Blocks → HTML
An article body is not a single markdown blob. Strapi v5 stores it as an array of dynamic-zone blocks, each tagged with a __component string: shared.rich-text, shared.quote, shared.media, or shared.slider. The <BlockRenderer> component maps over that array and renders the right markup for each component. Rich-text blocks are parsed with remark-parse (via the unified ecosystem) into an AST, sanitized with rehype-sanitize to strip dangerous markup, and stringified back to HTML with rehype-stringify before being injected via set:html, so editors can write markdown without opening an XSS surface.
5. The result
The end product is a folder of static HTML files. The CMS can be edited, previewed, and published independently; a rebuild is all it takes for changes to go live. No API calls happen in the browser, which keeps the site fast and the Strapi instance private.