A beautifully designed static HTML site and a content-rich WordPress blog are not mutually exclusive. Thousands of agencies and developers maintain a polished marketing front-end built on a template like the Canvas HTML Template while routing blog traffic through WordPress — keeping the performance benefits of static HTML without sacrificing the editorial power of a CMS. This guide explains exactly how to wire the two together using the WordPress REST API, with practical code you can drop into any Bootstrap 5 layout today.
Key Takeaways
- The WordPress REST API lets you pull posts, categories, and authors into any static HTML page without a PHP server.
- A simple
fetch()call and a Handlebars-style render function are all you need to display a blog feed. - Pagination, category filtering, and featured images are all available from the same API endpoints.
- Deferred script loading keeps your hybrid site fast — critical when mixing third-party CMS requests with static assets.
- Schema markup applied to dynamically rendered posts improves search visibility for the hybrid setup.
Why a Hybrid WordPress HTML Approach Makes Sense
Full WordPress installs introduce PHP overhead, plugin sprawl, and a heavier server footprint than many projects need. Conversely, a pure static site forces developers to hard-code blog posts or adopt a build pipeline for every content update. The hybrid WordPress HTML model splits responsibilities cleanly: the marketing site lives as static HTML (fast, secure, cheap to host), while WordPress handles authoring, tagging, and SEO metadata at a separate subdomain such as blog.yourdomain.com.
Visitors see a seamless experience because the static front-end fetches and renders posts at runtime via the WordPress REST API. Authors log into a familiar dashboard. Developers deploy HTML updates without touching the CMS. This architecture is a lighter-weight cousin of the fully headless pattern covered in Headless WordPress with a Static Bootstrap 5 Front-End, suited to teams that want dynamic blog content without rebuilding the entire site as a JavaScript application.

Understanding the WordPress REST API Endpoints
Every WordPress install exposes a JSON API at /wp-json/wp/v2/. The endpoints you will use most are:
/wp-json/wp/v2/posts?perpage=6&embed— latest posts with embedded author and featured image data./wp-json/wp/v2/categories— full category list for building filter tabs./wp-json/wp/v2/posts?categories=5&page=2— paginated, category-filtered posts./wp-json/wp/v2/posts/42?_embed— a single post by ID (useful for a preview modal).
The _embed flag is important: it bundles the featured image URL and author name into a single request, saving two extra round-trips per post. Confirm your WordPress site has the REST API enabled (it is on by default since WordPress 4.7) and that CORS headers allow requests from your static domain — add the following to your theme’s functions.php or a lightweight mu-plugin:
// functions.php — allow CORS from your static front-end
addaction( 'restapi_init', function() {
removefilter( 'restpreserverequest', 'restsendcors_headers' );
addfilter( 'restpreserverequest', function( $value ) {
header( 'Access-Control-Allow-Origin: https://www.yourdomain.com' );
header( 'Access-Control-Allow-Methods: GET' );
return $value;
});
});
Building the Blog Grid in Your Static Template
Create a placeholder section in your Bootstrap 5 page. The data- attributes act as hooks for the JavaScript renderer — keeping your HTML semantic and your script decoupled from the layout.
<section id="blog-feed" class="py-5">
<div class="container">
<div class="row g-4" id="posts-grid">
<!-- Posts injected here by wp-blog.js -->
</div>
<nav class="d-flex justify-content-center mt-5" id="posts-pagination"></nav>
</div>
</section>
The card template you render into #posts-grid can match whatever Bootstrap card style your template already uses — just build the inner HTML string inside JavaScript rather than hard-coding it. This keeps your CSS and component system consistent across the static and dynamic portions of the page.

Fetching and Rendering Posts with Vanilla JavaScript
No library is required. A single fetch() chain handles retrieval and DOM injection. Place this script at the bottom of your page or load it as a deferred module — a technique explored in depth in How to Implement Lazy Loading and Defer Scripts in HTML Templates.
<script type="module">
const API = 'https://blog.yourdomain.com/wp-json/wp/v2';
const grid = document.getElementById('posts-grid');
const pagination = document.getElementById('posts-pagination');
let currentPage = 1;
async function loadPosts(page = 1) {
const res = await fetch(${API}/posts?perpage=6&page=${page}&embed);
const totalPages = parseInt(res.headers.get('X-WP-TotalPages'), 10);
const posts = await res.json();
grid.innerHTML = posts.map(post => {
const img = post.embedded?.['wp:featuredmedia']?.[0]?.sourceurl
?? 'assets/images/placeholder.jpg';
const author = post._embedded?.author?.[0]?.name ?? 'Unknown';
const date = new Date(post.date).toLocaleDateString('en-GB', {
day: 'numeric', month: 'short', year: 'numeric'
});
return `
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm">
<img src="${img}" class="card-img-top" alt="${post.title.rendered}" loading="lazy">
<div class="card-body">
<p class="text-muted small mb-1">${date} · ${author}</p>
<h5 class="card-title">${post.title.rendered}</h5>
<p class="card-text">${post.excerpt.rendered.replace(/<[^>]+>/g,'').slice(0,120)}…</p>
<a href="${post.link}" class="btn btn-sm btn-outline-primary">Read More</a>
</div>
</div>
</div>`;
}).join('');
renderPagination(totalPages, page);
}
function renderPagination(total, active) {
pagination.innerHTML = Array.from({ length: total }, (_, i) => `
<button class="btn btn-sm ${i + 1 === active ? 'btn-primary' : 'btn-outline-secondary'} mx-1"
data-page="${i + 1}">${i + 1}</button>
`).join('');
pagination.querySelectorAll('[data-page]').forEach(btn =>
btn.addEventListener('click', () => loadPosts(parseInt(btn.dataset.page, 10)))
);
}
loadPosts();
</script>
The loading="lazy" attribute on the featured image ensures browser-native lazy loading for any posts below the fold — consistent with the lazy-loading strategy recommended across the rest of your template assets.
Schema Markup for Dynamically Rendered Posts
Search engines can index JavaScript-rendered content, but helping them with explicit structured data is always worthwhile. After inserting each post card, inject a BlogPosting JSON-LD block into the <head> for any post that serves as the canonical detail page. For the blog feed page itself, an ItemList schema listing the post URLs signals the page’s purpose to crawlers. The implementation pattern follows the same principles discussed in How to Add Schema Markup to a Bootstrap 5 HTML Template.
<script>
// Call after loadPosts() resolves to inject ItemList schema
function injectBlogListSchema(posts) {
const schema = {
"@context": "https://schema.org",
"@type": "ItemList",
"itemListElement": posts.map((post, i) => ({
"@type": "ListItem",
"position": i + 1,
"url": post.link
}))
};
const el = document.getElementById('blog-list-schema')
?? Object.assign(document.createElement('script'), { id: 'blog-list-schema', type: 'application/ld+json' });
el.textContent = JSON.stringify(schema);
if (!document.getElementById('blog-list-schema')) document.head.appendChild(el);
}
</script>
Performance Considerations for a WordPress Blog Static Site
Mixing a live API call into a static page introduces a network dependency that does not exist for fully static content. Mitigate this with three practices:
- Cache the API response in
sessionStorageso repeat visitors on the same session avoid redundant requests. - Show a skeleton loader in
#posts-gridwhile the fetch resolves — this prevents layout shift and improves perceived performance. - Set WordPress REST API cache headers using a plugin such as WP REST Cache, or configure your CDN to cache
/wp-json/responses for five to ten minutes.
Because the blog section loads asynchronously, the static HTML shell reaches First Contentful Paint before any API data arrives, keeping Core Web Vitals healthy. The WordPress origin can sit behind its own CDN edge without affecting your static host at all.
Handling Post Links and Navigation
Each post card links to its canonical WordPress URL (post.link). If you want a fully seamless single-page feel, you have two options: render the full post detail in a Bootstrap modal by fetching the single-post endpoint on click, or configure WordPress at blog.yourdomain.com with a theme that inherits your static site’s header and footer via shared CSS and partial includes. The second option is covered in detail in How to Convert a Bootstrap 5 HTML Template into a WordPress Theme — a natural next step once the blog feed integration is working.
Frequently Asked Questions
Yes. The REST API is bundled with WordPress core and requires no special hosting. You do need to ensure pretty permalinks are enabled (Settings → Permalinks) and that your server is not blocking /wp-json/ routes at the firewall level. Shared hosts occasionally do this; switching to a plain permalink structure or contacting support resolves it.
Googlebot does render JavaScript, but there is a crawl budget cost and a potential delay between deploy and indexation. For the best SEO on individual posts, ensure each post’s canonical URL on WordPress is indexable. The static blog feed page benefits most from the schema markup and internal links rather than expecting Google to index every card directly from the API-rendered HTML.
Fetch your category list from /wp-json/wp/v2/categories on page load and render filter buttons. When a category button is clicked, pass its ID to the posts endpoint: /wp-json/wp/v2/posts?categories=ID&perpage=6&embed. Reset the pagination to page one each time a new category is selected.
The default REST API only exposes publicly published content without authentication. If you need to display draft or private posts, you must authenticate requests using Application Passwords (WordPress 5.6+) and pass a Bearer token in the Authorization header. Exposing that token in client-side JavaScript is a security risk, so this pattern is better suited to a server-side proxy or a build-time static generator for non-public content.
That is the static site generation approach: a build script (Node, Eleventy, or a custom fetch pipeline) hits the REST API endpoints and writes out static HTML files. You get the SEO and performance benefits of fully static pages at the cost of a rebuild step every time content changes. The runtime fetch approach described in this article is better suited to teams who publish frequently and prefer not to manage a build pipeline.
Looking for a production-ready Bootstrap 5 HTML template? Browse Canvas Template demos and find the perfect starting point for your next project.
If you’re building with the Canvas HTML Template and want to ship production-ready Bootstrap 5 layouts faster, try Canvas Builder free — the visual builder that exports clean Canvas-ready markup in minutes.
Skip the setup — build it free
Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.
Canvas Team
Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.
More from the Canvas Blog