ForgeWP

Build a Latest Posts Block

Create a reusable post listing that reads live WordPress titles, excerpts, images, and links through WpQueryLoop.

Build a Latest Posts section that editors can insert into a Page. They can change its section heading in Gutenberg, while the cards read their content from published WordPress Posts.

This follows Build an Editable Hero Block. The new idea is post context: while a query loop renders each result, the data hooks read that particular post.

What you will build

Table · 3 columns
Part
Where its content comes from
Where editors change it
Section heading
The block's heading attribute
The Page's block canvas
Card title and excerpt
Each queried Post
Posts in the WordPress dashboard
Card image
The Post's featured image
The Post's featured-image setting
Card destination
The Post's permalink
WordPress post routing
Post count and ordering
Query props in source
PostListing.tsx for this recipe

The listing reads WordPress on each uncached page request. Updating a Post's title or excerpt updates its card without rebuilding the theme; refresh the relevant page cache when caching is enabled.

1. Prepare your test content

Use the React-adapter theme from the hero recipe, and a WordPress test site with that theme installed. In WordPress, create at least three published Posts, each with a distinct title and a short manual excerpt. Give at least one Post a featured image and leave another without one.

For example:

Table · 2 columns
Post title
Manual excerpt
A Weekend by the Coast
A simple itinerary for a relaxed coastal break.
Inside Our Garden Rooms
Meet the spaces overlooking our courtyard garden.
Breakfast at Your Own Pace
Start the morning with local produce and fresh coffee.

These Posts are separate from the Page that will contain the listing. Local mock Posts in cms/mock-data.ts are useful for design previews; create the live test Posts through WordPress for this walkthrough.

Add the fallback image

Create public/assets/story-placeholder.svg in the theme project. The public/ folder supplies theme assets; this small graphic keeps cards readable when a Post has no featured image.

public/assets/story-placeholder.svg
1<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="675" viewBox="0 0 1200 675">
2  <rect width="1200" height="675" fill="#e2e8f0" />
3  <path d="M400 270h400M400 335h300M400 400h350" stroke="#94a3b8" stroke-width="16" />
4</svg>

2. Scaffold the block

Run this in the theme root:

Theme terminal: create the post listing block
pnpm forgewp make:block PostListing --attributes=heading

Replace src/blocks/PostListing.tsx with the complete code below. Keep your earlier HeroBlock.tsx; this recipe creates a different block.

src/blocks/PostListing.tsx
1import {
2  defineBlock,
3  WpEditable,
4  WpImage,
5  WpQueryLoop,
6  useWpExcerpt,
7  useWpFeaturedImage,
8  useWpPermalink,
9  useWpTitle,
10} from "@forgewp/react"
11
12function PostCard() {
13  return (
14    <article className="overflow-hidden border border-slate-200 bg-white">
15      <WpImage
16        src={useWpFeaturedImage() || "/assets/story-placeholder.svg"}
17        alt=""
18        className="aspect-video w-full object-cover"
19      />
20      <div className="p-6">
21        <h3 className="text-xl font-bold text-slate-950">
22          <a href={useWpPermalink()} className="underline-offset-4 hover:underline">
23            {useWpTitle()}
24          </a>
25        </h3>
26        <p className="mt-3 text-base leading-relaxed text-slate-600">
27          {useWpExcerpt()}
28        </p>
29      </div>
30    </article>
31  )
32}
33
34export default defineBlock({
35  name: "post-listing",
36  title: "Latest Posts",
37  category: "widgets",
38  icon: "list-view",
39  attributes: {
40    heading: { type: "string", label: "Section heading", default: "From our journal" },
41  },
42  edit: ({ attributes, setAttributes }) => (
43    <section className="bg-slate-50 px-6 py-16">
44      <div className="mx-auto max-w-6xl">
45        <WpEditable
46          tagName="h2"
47          value={attributes.heading}
48          onChange={(value) => setAttributes({ heading: value })}
49          className="mb-8 text-3xl font-bold text-slate-950"
50        />
51        <div className="grid gap-6 md:grid-cols-3">
52          <WpQueryLoop postType="post" postsPerPage={3} orderby="date" order="DESC">
53            <PostCard />
54          </WpQueryLoop>
55        </div>
56      </div>
57    </section>
58  ),
59  save: ({ attributes }) => (
60    <section className="bg-slate-50 px-6 py-16">
61      <div className="mx-auto max-w-6xl">
62        <WpEditable
63          tagName="h2"
64          value={attributes.heading}
65          className="mb-8 text-3xl font-bold text-slate-950"
66        />
67        <div className="grid gap-6 md:grid-cols-3">
68          <WpQueryLoop postType="post" postsPerPage={3} orderby="date" order="DESC">
69            <PostCard />
70          </WpQueryLoop>
71        </div>
72      </div>
73    </section>
74  ),
75})

3. Follow the post context

WpQueryLoop queries up to three Posts in descending date order and renders its children for each result. PostCard is inside the loop, so its hooks receive the current result's context. Calling useWpTitle() outside the loop would read a different context, such as the containing Page.

This version calls the data hooks directly in the card JSX, in a fixed order, so the block compiler can map each binding to its WordPress equivalent. The image source uses the featured image when present and the theme asset as its fallback. The grid belongs on a normal div; WpQueryLoop supplies query behavior and context rather than layout classes.

The thumbnail uses alt="" because the nearby linked title identifies the story and the thumbnail is decorative in this design. If an image communicates information beyond that title, provide a suitable alternative-text data source for that design.

4. Export and insert the listing

Theme terminal: check and export the listing
pnpm typecheck
pnpm forgewp export --validate --strict

The standard starter provides typecheck; use your project's equivalent if its scripts differ.

  1. Update the theme on your test WordPress site.
  2. Open the Page used in the hero recipe, or create a new Page.
  3. Select ForgeWP Builder, or another template that renders the saved Page body.
  4. Insert Latest Posts below the hero and edit its section heading.
  5. Save the Page, reload the editor, and open the visitor page.

Edit the individual stories through Posts. The listing block's own inline field is its section heading; the cards use Post data. Use the visitor page to verify the live query and ordering, while the editor canvas provides the section preview and heading control.

5. Verify the query

  • Up to three published Posts appear, ordered by publication date from newest to oldest.
  • Each card has its own Post's title, excerpt, and permalink.
  • A card without a featured image displays the placeholder asset alongside its title and excerpt.
  • Updating a Post's title and excerpt changes the visitor card after saving and refreshing relevant caches.
  • Updating the block's section heading affects the section, not the individual Posts.
  • Narrow screens stack the cards; wider screens show three columns.

Check an empty test site too: with no matching published Posts, this example renders its section heading and an empty grid. Use that result to decide your empty-content presentation for a production design. Local preview placeholders are a design aid; the live WordPress query reflects actual matching Posts.

Inspect .forgewp/out/<theme-slug>/blocks/post-listing/render.php. You should find the generated WP_Query, the three-post limit, per-post data calls, and wp_reset_postdata() to restore the surrounding context after the query.

Common checks

Table · 2 columns
What you see
What to check
Cards are missing
Confirm the site has published Posts and the installed block renderer contains the query
Cards repeat the containing Page's title
Keep PostCard inside WpQueryLoop and check the generated per-post bindings
The wrong image appears
Check the featured image on the corresponding Post
The preview and live list differ
Compare local/editor preview data with the published Posts on the test site
A recent change appears stale
Save the Post and refresh applicable page, object, and CDN caches

For query props and interactive alternatives, see Loops, Custom Post Types & Queries.

Practice next

Try Build a Featured Story Section. You will select one story through a category filter and provide a visual fallback when it has no image.