Learn how to connect to any WordPress site, explore its data, and build templates with live preview.
Enter any WordPress site URL into the input field on the home page — for example techcrunch.com or wordpress.org/news. HeadlessPlayground will auto-detect the REST API endpoint and connect.
No API keys or authentication needed. Any WordPress site with the REST API enabled (which is most of them) will work out of the box.
https:// or /wp-json — just the domain is enough. The tool figures out the rest.Once connected, you'll see a full-screen IDE. The code editor is on the left, the live preview is on the right. Code uses an Astro-like syntax with two parts:
---
// Frontmatter: fetch data here (async JavaScript)
const posts = await fetchPosts({ per_page: 5 });
---
<!-- Template: HTML with {expressions} -->
<div class="grid gap-4">
{posts.map(post => (
<h3 class="text-white text-lg">{post.title.rendered}</h3>
))}
</div>Frontmatter (between the --- fences) runs as async JavaScript. Declare variables here — they're automatically available in the template below.
Template (below the fences) is HTML with {expression} interpolation. Tailwind CSS is available — all utility classes work in the preview.
Press Run (or Cmd+Enter / Ctrl+Enter) to execute your code and see the result.
These functions are available in the frontmatter. They fetch from the connected WordPress site automatically — you don't need to pass the site URL.
| Function | Description |
|---|---|
fetchPosts(params?) | Get blog posts |
fetchPages(params?) | Get pages |
fetchCategories() | Get all categories |
fetchTags() | Get all tags |
fetchMedia(params?) | Get media items (images, files) |
fetchSiteInfo() | Get site name, description, URL |
Functions that accept params take an object with WordPress REST API query parameters:
// Get 3 posts matching "design"
const posts = await fetchPosts({ per_page: 3, search: "design" });
// Get posts in category ID 5
const catPosts = await fetchPosts({ categories: 5 });
// Get 20 media items
const media = await fetchMedia({ per_page: 20 });Available in both frontmatter and templates:
| Function | What it does |
|---|---|
stripHtml(html) | Removes all HTML tags, returns plain text |
formatDate(dateString) | Formats a date nicely — e.g. "March 7, 2026" |
console.log(...) | Prints output to the Console panel in the preview |
& entities, <p> tags, etc). Use stripHtml() to get clean text for display.Click the Data button in the toolbar to open the Data Explorer panel on the left. It shows actual data from the connected site, organized into tabs: Posts, Pages, Categories, Tags, and Media.
Hover over any item to see a tooltip listing all available data paths — things like post.title.rendered, post.date, post.slug, etc.
Click a data path in the tooltip to insert it directly into your code at the current cursor position. This is the fastest way to reference data fields without having to remember the exact path.
The template section supports several patterns:
Loops — use .map() with HTML inside parentheses:
{posts.map(post => (
<div class="p-4 border border-white/10 rounded-xl">
<h3>{post.title.rendered}</h3>
</div>
))}Conditionals — use && for simple show/hide:
{post._embedded?.["wp:featuredmedia"]?.[0]?.source_url && (
<img src={post._embedded["wp:featuredmedia"][0].source_url}
class="w-full h-44 object-cover rounded-lg" />
)}Ternaries — use for if/else rendering:
{posts.length === 0 ? (
<p class="text-zinc-500">No posts found.</p>
) : (
<div class="grid gap-4">
{posts.map(post => (
<h3>{post.title.rendered}</h3>
))}
</div>
)}Attribute binding — use {expressions} inside attributes:
<img src={post._embedded["wp:featuredmedia"][0].source_url}
alt={stripHtml(post.title.rendered)} />Click the Examples button in the toolbar to load ready-made templates. Here are some patterns to try:
Blog post cards with images:
---
const posts = await fetchPosts({ per_page: 5 });
---
<div class="grid gap-4">
{posts.map(post => (
<article class="bg-white/5 border border-white/10 rounded-xl p-5">
{post._embedded?.["wp:featuredmedia"]?.[0]?.source_url && (
<img src={post._embedded["wp:featuredmedia"][0].source_url}
class="w-full h-44 object-cover rounded-lg mb-3" />
)}
<h3 class="text-white font-semibold text-lg mb-1">{post.title.rendered}</h3>
<p class="text-zinc-500 text-sm">{formatDate(post.date)}</p>
<p class="text-zinc-400 text-sm mt-2">
{stripHtml(post.excerpt.rendered).slice(0, 120)}...
</p>
</article>
))}
</div>Site dashboard with multiple API calls:
---
const [site, categories, pages] = await Promise.all([
fetchSiteInfo(),
fetchCategories(),
fetchPages({ per_page: 100 })
]);
---
<div class="text-center py-6">
<h2 class="text-3xl font-bold text-white mb-1">{site.name}</h2>
<p class="text-zinc-500 mb-8">{site.description}</p>
<div class="flex gap-4 justify-center">
<div class="bg-white/5 border border-white/10 rounded-xl px-8 py-5">
<div class="text-3xl font-bold text-emerald-400">{categories.length}</div>
<div class="text-zinc-500 text-sm mt-1">Categories</div>
</div>
<div class="bg-white/5 border border-white/10 rounded-xl px-8 py-5">
<div class="text-3xl font-bold text-emerald-400">{pages.length}</div>
<div class="text-zinc-500 text-sm mt-1">Pages</div>
</div>
</div>
</div>Image gallery:
---
const media = await fetchMedia({ per_page: 12 });
const images = media.filter(m => m.media_type === "image");
---
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
{images.map(item => (
<div class="aspect-square rounded-xl overflow-hidden border border-white/10">
<img src={item.source_url} alt={item.alt_text || ""}
class="w-full h-full object-cover hover:scale-110 transition-transform duration-500" />
</div>
))}
</div>Promise.all() when you need data from multiple endpoints. It fetches everything in parallel instead of one at a time, making your code noticeably faster.console.log(posts[0]) in the frontmatter to inspect the shape of the data. The output shows up in the Console panel above the preview — handy for discovering available fields._embedded field. Use optional chaining (?.) to safely access nested fields like post._embedded?.["wp:featuredmedia"]?.[0]?.source_url.text-white, bg-white/5, border-white/10 for a clean look.posts.filter(p => p.categories.includes(5)) or posts.slice(0, 3).