How to Fetch WordPress Posts in Astro That Actually Render

Disclaimer: Content may contain affiliate links, WPThink.com may earn a commission from qualifying purchases.

When Data Isn’t the Page

The fetch worked; the page pipeline didn’t.

Posts can appear perfectly in server logs and still vanish on the page. That usually means Astro solved only the data half of the job. Rendering is a separate contract: the route must exist, the template must read the right fields, and WordPress HTML must be injected as HTML rather than printed as escaped text.

The same split explains broken images and missing pages. A valid API response says nothing about getStaticPaths, set:html, image URLs, or whether content arrives as content.rendered instead of a nested object. Fetching proves transport; rendering proves shape, routes, and output rules.

Key signals
  • Most blank Astro pages come from route generation or field mismatches, not failed requests.
  • WordPress REST commonly stores display-ready markup in *.rendered fields.
Core flow

Build the rendering pipeline first

Astro only becomes predictable when data, routes, and markup are treated as separate jobs.

Astro rendering works best as a three-part pipeline:

  • Fetch data from WordPress
  • Create routes from that data with getStaticPaths()
  • Output templates from a stable, render-ready object

That separation matters because a raw WordPress post is a poor view model. It usually contains mixed concerns: REST metadata, embedded relations, rendered HTML, dates in multiple formats, and optional fields that appear or disappear by endpoint.

Before templating, map each post into a smaller shape such as:

  • slug
  • title
  • html
  • excerpt
  • date
  • featuredImage

Once normalized, routing becomes deterministic and templates stop depending on WordPress response quirks. Astro pages can then render from one dependable contract instead of reaching deep into _embedded, guessing at missing media, or repeating fallback logic in multiple files.

In practice, normalization is the real boundary between “API data exists” and “pages actually render.”

Choose the API on purpose

REST is usually the quickest route to pages that render. Astro still needs the same three pieces either way: a list query for routes, a detail query for content, and a template that outputs sanitized HTML or mapped fields. Switching to GraphQL does not remove getStaticPaths(), pagination work, or image handling.

WPGraphQL earns its keep when schemas get wide or inconsistent. It can request only needed fields, compose nested data cleanly, and make refactors safer across templates. For a practical comparison, see choosing between WPGraphQL and REST in Astro.

  • Choose REST for speed, simpler debugging, and broad plugin support.
  • Choose GraphQL for query control, reusable fragments, and complex content models.
Core helper

Build a single fetch helper

Normalize once, render everywhere

A reliable Astro integration starts with one data function, not scattered fetch() calls. It should read the WordPress origin from an environment variable, request _embed so featured media arrives in the same payload, and return a small, stable post shape.

const WP = import.meta.env.WORDPRESS_URL?.replace(/\/$/, '');

export async function getPosts(page = 1) {
  const url = `${WP}/wp-json/wp/v2/posts?_embed&per_page=10&page=${page}`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`WP ${res.status}: ${await res.text()}`);

  const totalPages = Number(res.headers.get('X-WP-TotalPages') || 1);
  const posts = (await res.json()).map((p: any) => ({
    slug: p.slug,
    title: p.title?.rendered ?? '',
    excerpt: p.excerpt?.rendered ?? '',
    content: p.content?.rendered ?? '',
    date: p.date,
    featuredImage: p._embedded?.['wp:featuredmedia']?.[0]?.source_url ?? null,
  }));

  return { posts, totalPages };
}

Details that prevent breakage

  • Base URL via env avoids hard-coded domains across local, preview, and production.
  • Header-aware pagination prevents silent truncation after the first page.
  • Null-safe mapping stops missing excerpts or media from crashing builds.
  • Explicit error messages make bad routes, auth blocks, and malformed responses obvious during SSR or static generation.
Common breakage

Make post routes and page data line up

A successful fetch only proves that WordPress responded. Astro still needs a static path for every post and a matching data lookup for each generated page.

The reliable mental model is simple:

  • List page fetches many posts and renders links.
  • getStaticPaths() turns that same post set into route params.
  • Post page receives one post through props, then renders it.

Where rendering usually fails

The first common failure is a working archive with broken detail pages. That usually means the list fetched posts, but getStaticPaths() returned nothing, returned the wrong param name, or generated /posts/[slug] paths while links point somewhere else.

The second failure is subtler: routes build, but each page shows the wrong content or no content at all. That happens when route params are based on slug, while page data is looked up by id, or when the lookup re-fetches without using the same normalization rules as the list.

A safer pattern is to generate paths from normalized posts and pass the full post forward:

export async function getStaticPaths() {
  const posts = await getPosts();
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post }
  }));
}

This keeps listing, route generation, and rendering on one data shape, which removes most empty-page bugs.

Common trap

Render CMS fields as HTML, not text

WordPress post fields are rarely uniform. In REST responses, title.rendered, excerpt.rendered, and content.rendered are typically HTML strings. Printing them as {post.title} or {post.excerpt} often exposes tags or entities instead of real markup.

In Astro, those fields need set:html at the point of output. That is the missing piece when a correct fetch still fails to display WordPress content in Astro properly.

  • Use normal interpolation for plain data: slug, date, IDs, author names, custom booleans.
  • Use set:html for editor-produced markup: headings, links, emphasis, lists, embeds, and formatted excerpts.
  • Prefer normalized names like post.titleHtml and post.contentHtml so the template signals intent.

The trust boundary matters. set:html injects raw HTML into the page, so it should only receive content from a trusted CMS workflow or a sanitization step. If content can come from untrusted users, sanitize before rendering and allow only the tags and attributes the design actually needs.

A literal string is the clue

If <p> or &amp; appears on the page, Astro is treating CMS markup as text. That field needs set:html or prior transformation.

Last-mile mapping

Normalize the content edges

Move production-only fixes out of Astro templates

A post can render and still feel unfinished: missing hero images, broken /wp-json domain links, empty author names, or taxonomies that only exist as IDs. Those are data-shaping problems, not template problems. Astro components should receive a stable object, not negotiate WordPress quirks on every render.

Map related data once

During normalization, extract a featured image from _embedded["wp:featuredmedia"] or the GraphQL equivalent, then store a compact object such as src, alt, width, and height. Resolve author data the same way: flatten name, slug, avatar, and URL into one predictable field.

For taxonomies, convert category and tag arrays into label/slug pairs immediately. Templates should loop over ready-made terms, not chase relationship IDs or mixed response shapes.

Rewrite links before rendering

WordPress content often contains absolute links back to the CMS origin. Rewrite internal href and image src values during normalization so production pages point to Astro routes and public assets. Leave external URLs untouched.

  • Keep custom fields behind a mapper, even when using ACF.
  • Coerce nullable values to null, empty arrays, or defaults.
  • Strip unused fields so templates stay small and type-safe.

That single normalized contract prevents dozens of tiny production regressions.

Quick pass

Run a five-minute render check

  • Hit the exact post endpoint

    Confirm the slug resolves from the same base URL used at build time. A 200 on the collection endpoint proves nothing if the single-post route is wrong.

  • Verify embedded media exists

    Check for _embedded["wp:featuredmedia"] before blaming Astro. Missing _embed usually explains absent images and authors.

  • Match paths to page props

    Ensure getStaticPaths() returns the same normalized slug and fields the page expects. Divergent shapes cause blank pages and undefined data.

  • Render HTML as HTML

    If content prints tags, the template is escaping a rendered string. Use set:html only on trusted, normalized fields.

  • Lock the production pattern

    One fetch helper, _embed enabled, one normalized post contract, paths generated from that contract, templates reading only normalized HTML and media, plus cached list requests reused across routes.

Conclusion

A Stable Pipeline Wins

Reliable Astro pages come from a stable chain: fetch the right records, normalize them once, generate matching routes, and render CMS HTML deliberately. When that chain is explicit, failures become debuggable instead of mysterious.

Only after that baseline holds should a project reach for WPGraphQL or REST API tradeoffs, SSR, or richer content blocks. Those tools expand capability, but they do not repair a broken render path; they amplify a sound one.