惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
Kaspersky official blog
Engineering at Meta
Engineering at Meta
D
DataBreaches.Net
Stack Overflow Blog
Stack Overflow Blog
Microsoft Security Blog
Microsoft Security Blog
Y
Y Combinator Blog
B
Blog RSS Feed
GbyAI
GbyAI
P
Proofpoint News Feed
aimingoo的专栏
aimingoo的专栏
MyScale Blog
MyScale Blog
D
Docker
阮一峰的网络日志
阮一峰的网络日志
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recorded Future
Recorded Future
美团技术团队
The Register - Security
The Register - Security
V
Visual Studio Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
Tailwind CSS Blog
爱范儿
爱范儿
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
The Blog of Author Tim Ferriss
博客园 - 司徒正美
量子位
B
Blog
F
Fortinet All Blogs
Martin Fowler
Martin Fowler
博客园 - 【当耐特】
MongoDB | Blog
MongoDB | Blog
A
About on SuperTechFans
I
InfoQ
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
有赞技术团队
有赞技术团队
雷峰网
雷峰网
大猫的无限游戏
大猫的无限游戏
J
Java Code Geeks
L
LangChain Blog
Latest news
Latest news
S
SegmentFault 最新的问题
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Blog — PlanetScale
Blog — PlanetScale
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Cisco Talos Blog
Cisco Talos Blog
F
Full Disclosure
C
Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
W
WeLiveSecurity
T
Tenable Blog
T
Tor Project blog

Piccalilli - Everything

Use cases for aria-expanded The Index: Issue #190 Proxy and Reflect The Index: Issue #189 Rendering AT protocol posts on my /feed Loading AT protocol posts data The Index: Issue #188 Publishing on the Atmosphere with Standard.site The Index: Issue #187 The Index: Issue #186 The Index: Issue #185 A Front-end developer’s guide to the hybrid mobile app development landscape The Index: Issue #184 Navigating the age-old problem of checkmarks in UI with progressive enhancement The Index: Issue #183 Framework-agnostic design systems: a practical approach to web components The Index: Issue #182 Getting started with the HTML only build
Completing the WordPress headless CMS integration
AuthorAndy Bell · 2026-04-16 · via Piccalilli - Everything

Let’s work out what I need to account for here by referring back to our existing page audit.

A Penpot file that has every page type from my existing site mapped out next to each other in labelled sections

I need to account for the following:

  1. Post listings, limited by a defined items per page
  2. Pagination to render each page of post listings
  3. The actual blog items themselves

With those, I’d consider this base implementation done, which is where I want to be in this iteration.

---
const { next, previous } = Astro.props;
---

{
  (previous || next) && (
    <div class="wrapper">
      <nav class="repel" aria-label="Pagination links">
        {previous ? <a href={previous}>Newer posts</a> : <>&nbsp;</>}
        {next && <a href={next}>Older posts</a>}
      </nav>
    </div>
  )
}

I’ve used a <nav> here, so it needs an appropriate label: “Pagination links”. The logic is quite straightforward here:

  1. Attempt to grab next and previous props, which are URLs
  2. Test if either next or previous have anything for us
  3. If so, we’re safe to render any wrapping markup because there will be at least one link
  4. If previous is good to go, render the link, if not render a non-breaking space (I’ll explain in a moment)
  5. If next is good to go, render the link

The repel CSS class is a composable layout, which is part of our CSS system, that pushes items away from each other on the inline axis where there’s available space. If there’s not enough space available, the elements stack vertically, with configurable space.

The reason there’s a non-breaking space is if there’s no previous link, we still want to push the “Older posts” link out to the right. You might see a hack here but I see it as using the tools the browser gives us to keep everything simple.

AdvertMindful Design. Learn to design for real humans. Available now

Rigging up the paginated blog index

All the parts are ready now, so I can wire it up and move on to the next thing.

Astro comes with handy pre-baked pagination functionality so all I need to do here is let the system know what pages I need building. This is done with a [...page].astro file, which tells Astro to use rest parameters for this dynamic route.

---
import { fetchAllWordPressPosts } from '@repo/data/wordPressData';
import PageLayout from 'src/layouts/PageLayout.astro';
import PostListRegion from '@repo/ui/PostListRegion';
import Pagination from '@repo/ui/Pagination';

export async function getStaticPaths({ paginate }) {
  const posts = await fetchAllWordPressPosts();
  return paginate(posts, { pageSize: 10 });
}

const { page } = Astro.props;
---

<PageLayout title="Blog" summary="" socialImage="" allowRobots={true}>
  <PostListRegion posts={page.data} />
  <Pagination previous={page.url.prev} next={page.url.next} />
</PageLayout>


This is it. It’s all we need to have a paginated blog index. The perks of building in small parts, eh?

Let’s just zoom in on this part:

export async function getStaticPaths({ paginate }) {
  const posts = await fetchAllWordPressPosts();
  return paginate(posts, { pageSize: 10 });
}

const { page } = Astro.props;

My website is going to be 99% statically generated, which means the output is HTML pages in directories. In order for Astro to render these pages, it needs to know about them. To do that, we need to return paths (AKA relative URLs) to the system, using the getStaticPaths function.

Using a destructuring assignment we can pull out the paginate function from the available arguments. We then use that to paginate posts — which is our familiar fetchAllWordPressPosts() data — limiting each page to 10 items. Paginate then supplies each path, which results in pages, like this one on the live website.

Blog post items

We’ve got the means to navigate around the blog now, but not the means to actually read the posts. Let’s fix that with another simple astro file.

---
import { fetchAllWordPressPosts } from '@repo/data/wordPressData';
import { stripHTML, decodeHTMLEntities } from '../utils/helpers';
import PageLayout from 'src/layouts/PageLayout.astro';
import PostRegion from '@repo/ui/PostRegion';

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

const { entry } = Astro.props;
---

<PageLayout
  title={decodeHTMLEntities(entry.title.rendered)}
  summary={decodeHTMLEntities(stripHTML(entry.excerpt.rendered))}
  socialImage={entry?.jetpack_featured_media_url}
  allowRobots={true}
>
  <PostRegion
    title={decodeHTMLEntities(entry.title.rendered)}
    date={entry.date}
    content={entry.content.rendered}
  />
</PageLayout>

This is all very familiar, right? Fundamentally, there’s not much difference going on here than in the paginated blog index file. Instead of paginating the WordPress data, we’re rendering a page for each blog post and passing the data into other components to do the rendering for us.

Cleaning up content

Let’s just take a look at a couple of helper functions which might have caught your attention decodeHTMLEntities() and stripHTML(). Unfortunately, there’s quite a lot of crap when it comes to WordPress content, especially over the API.

export function decodeHTMLEntities(input) {
  return input.replace(/&#(\d+);/g, (match, dec) => {
    return String.fromCharCode(dec);
  });
}

This function grabs the unicode number from each encoded match then replaces the encoded part with a proper, readable character. It’s mainly titles and summaries where this applies because WordPress does some weird stuff with those.

export function stripHTML(input) {
  return input.replace(/<[^>]*>?/gm, '');
}

This is the Ronseal of JavaScript functions: it does exactly what it says on the tin. It takes a string of HTML and returns back plain text.

I used this function in conjunction with decodeHTMLEntities() because entry.excerpt.rendered is both HTML and riddled with HTML entities — both of which I don’t want.

There’s no need to clean up entry.content.rendered because we do want the HTML for that. I’m using Astro’s set:html in that context again, in the <PostRegion />, which accounts for any nasties for me.

Here we have it, the paginated index: A very rough looking list of blog post links, with newer and older pagination links

And here’s an item: A very rough looking blog post, rendered as simple HTML

Looks pretty bland! But that’s fine, we’ll come to that later.

It’s all about AT protocol/Bluesky integration now, while I’m already in the mood to integrate 3rd party stuff.

See you in the next one!

Enjoyed this article? You can support us by leaving a tip via Open Collective

AdvertComplete CSS. Take your CSS skills beyond the next level. Available now!