Building the Headless PHP Frontend: Turning WordPress Data Into a Fast Website
In the first article, we looked at the architecture behind the project.
In the second, we moved into the WordPress backend and built a custom plugin that exposes our content and applications through an API.
Now we arrive at the part visitors actually see.
The frontend.
Our objective was not to build another WordPress theme. We wanted to create a lightweight PHP application that could take responsibility for the public website while WordPress remained behind the scenes as the CMS and data store.
The technology stack was deliberately modest:
PHP
HTML
CSS
JavaScript
No large frontend framework.
No client-side rendering engine.
No unnecessary build pipeline.
Just a small server-rendered application sitting in front of WordPress.
The result is a frontend that is easy to understand, easy to modify and designed around the content we actually need to present.
1. The Frontend Has One Job
The backend already knows how to manage content.
The frontend does not need to become another CMS.
Its job is to turn content into a website.
Conceptually:
WordPress
β
β content
βΌ
PHP Frontend
β
βββ Layout
βββ HTML
βββ CSS
βββ JavaScript
βββ URLs
β
βΌ
Browser
This separation lets us think about the frontend as an application rather than a theme.
The backend asks:
What data do we have?
The frontend asks:
How should visitors experience that data?
That distinction guided almost every decision we made.
2. Keeping the Frontend Small
The frontend package is intentionally compact.
Its core structure looks like this:
frontend/
β
βββ index.php
βββ article.php
βββ application.php
βββ page.php
βββ config.php
β
βββ includes/
β βββ header.php
β βββ footer.php
β
βββ assets/
βββ style.css
βββ script.js
There is something quite deliberate about this structure.
There isn’t a giant application framework hidden underneath it.
Each file has a recognizable responsibility.
index.php
Home / listing
article.php
Individual article
application.php
Individual application
page.php
CMS pages
header.php
Shared document header
footer.php
Shared document footer
style.css
Presentation
script.js
Browser interaction
For a small content-oriented application, this simplicity is valuable.
Someone opening the project for the first time can understand the basic architecture in minutes.
3. PHP Is Our Rendering Layer
The frontend uses PHP’s server-side rendering capabilities.
A typical request follows this path:
Browser
β
βΌ
PHP entry point
β
βΌ
WordPress data
β
βΌ
PHP template
β
βΌ
HTML
β
βΌ
Browser
The browser ultimately receives ordinary HTML.
That means the frontend does not have to send a large JavaScript application to the browser before the visitor can see the content.
For a site consisting primarily of articles, applications and informational pages, this is a very sensible starting point.
4. Connecting PHP to WordPress
The current frontend package uses a particularly practical approach.
Each PHP entry point loads WordPress without loading its theme:
define('WP_USE_THEMES', false);
require_once __DIR__ . '/dashboard/wp-load.php';
This is an important detail.
We are not asking WordPress to render the page.
We are using WordPress as the application’s content and data layer.
The WP_USE_THEMES setting tells WordPress not to load the normal theme rendering system.
The PHP frontend can then use WordPress’s APIs and query system while remaining in control of the final HTML.
Conceptually:
WordPress bootstrap
β
βββ Database
βββ WP_Query
βββ Posts
βββ Pages
βββ Custom Post Types
βββ Media
β
βΌ
PHP Frontend
This approach is particularly useful in the current implementation because it keeps the frontend very lightweight while still allowing it to use WordPress’s mature content infrastructure.
5. A Note About the API Architecture
The backend we built exposes a custom REST API.
The frontend package also contains configuration for that API:
define(
'WP_API_URL',
'https://sanjibsinha.in/dashboard/wp-json/fullstack/v1/apps'
);
This gives us an explicit API boundary and allows the frontend package to evolve toward consuming the backend entirely through HTTP.
In the initial implementation, however, the PHP frontend also bootstraps WordPress directly with wp-load.php.
That is an intentional stage in the evolution of the project.
The architecture gives us two useful possibilities:
Current implementation
PHP Frontend
β
βββ WordPress bootstrap
β
βΌ
WordPress
and the more completely decoupled model:
PHP Frontend
β
β HTTP / JSON
βΌ
REST API
β
βΌ
WordPress
The second model provides stronger physical separation between the two applications.
The first model keeps deployment and implementation extremely simple.
Rather than pretending that these are identical, it is more useful to recognize the current frontend as the first practical implementation of the architecture.
6. The Homepage
The main entry point is:
index.php
The homepage has two primary modes:
Applications
Articles
The current tab is determined from the query string:
$tab = $_GET['tab'] ?? 'apps';
So:
/?tab=apps
shows applications, while:
/?tab=posts
shows articles.
This is deliberately simple.
We don’t need a client-side router merely to switch between two server-rendered collections.
7. Querying Applications
When the Applications tab is active, the frontend queries the WordPress custom post type:
$query_args = [
'post_type' => 'app_repository',
'post_status' => 'publish',
'posts_per_page' => $per_page,
'paged' => $paged,
];
The important part is:
post_type = app_repository
That is the same content type created by our backend plugin.
The frontend therefore understands applications as a first-class resource.
A visitor sees an application card.
WordPress sees a custom post.
The backend API sees an application resource.
Those are three different representations of the same thing.
8. Querying Articles
When the Articles tab is selected, the same index.php file switches to:
'post_type' => 'post'
The rest of the pagination logic remains essentially the same.
This is one advantage of keeping the frontend small.
The application doesn’t need a complicated component architecture to display two collections of content.
It simply changes the query.
The resulting page can show:
Article title
Excerpt
Publication date
Read Article
while the Applications view shows:
Application title
Version
Operating system
Description
Updated date
Read
Download
The same overall layout can therefore accommodate two different kinds of content.
9. Pagination
The homepage displays six items per page:
$per_page = 6;
The current page comes from:
$paged = isset($_GET['page'])
? max(1, intval($_GET['page']))
: 1;
WordPress then tells us how many pages exist:
$total_pages = $query->max_num_pages;
If more than one page exists, the frontend generates a simple pagination navigation:
1 2 3 4 5
Each link preserves the selected tab:
?tab=apps&page=2
or:
?tab=posts&page=2
There is no JavaScript pagination layer.
The server does the work, and the browser receives the resulting HTML.
10. Building the Application Cards
Applications are displayed as cards.
Each card contains:
Title
Version
Operating System
Description
Updated date
Read
Download
The version comes from post metadata:
$version = get_post_meta(
get_the_ID(),
'app_version',
true
) ?: '1.0.0';
The operating system is retrieved similarly:
$os = get_post_meta(
get_the_ID(),
'app_os',
true
) ?: 'Linux / Ubuntu';
And the downloadable file is resolved through its attachment ID:
$file_id = get_post_meta(
get_the_ID(),
'app_file_id',
true
);
$file_url = $file_id
? wp_get_attachment_url($file_id)
: '';
This gives the frontend everything it needs to build the card.
11. Clean Application URLs
We did not want application URLs to look like:
application.php?slug=my-application
That is an implementation detail.
The public URL is:
/applications/my-application
Internally, the server can route that request to:
application.php?slug=my-application
The visitor never needs to know.
This distinction is important.
The application file is an implementation detail.
The URL is part of the public interface.
12. Reading an Application
The application.php file retrieves the application slug:
$slug = isset($_GET['slug'])
? sanitize_title(wp_unslash($_GET['slug']))
: '';
It then queries WordPress:
$query = new WP_Query([
'post_type' => 'app_repository',
'post_status' => 'publish',
'name' => $slug,
'posts_per_page' => 1,
]);
If the application does not exist, we return:
HTTP 404
and display an application-not-found page.
If it exists, the frontend extracts:
title
version
operating system
description
content
download URL
and renders the application page.
The visitor therefore sees a proper application detail page rather than a raw WordPress post.
13. Article URLs Follow the Same Pattern
Articles use the same architectural idea.
The public URL becomes:
/articles/my-first-article
The frontend extracts:
$slug = isset($_GET['slug'])
? sanitize_title(wp_unslash($_GET['slug']))
: '';
and queries:
'post_type' => 'post'
The result is rendered using:
article.php
This gives us a clean separation:
/applications/... β application.php
/articles/... β article.php
The content remains in WordPress.
The URL and presentation remain in the frontend.
14. Handling Missing Content
A custom frontend should not assume that every requested URL exists.
Both articles and applications explicitly handle missing resources.
For example:
if (!$query->have_posts()) {
http_response_code(404);
...
}
The frontend then renders a small, human-readable error page.
This matters for both usability and search engines.
A missing article should not silently return a successful page.
The server should say what actually happened.
15. WordPress Pages Become CMS Pages
Articles and applications are not the only content.
We also need ordinary pages such as:
About
Contact
Privacy Policy
Terms
The page.php entry point handles these.
It first restricts the allowed page slugs:
$allowed_pages = [
'about',
'contact',
'privacy-policy',
'terms',
];
This is a useful security and routing boundary.
We don’t want arbitrary values being treated as public CMS pages.
The frontend then queries the WordPress page post type and renders the selected page.
So WordPress remains responsible for editing the content while PHP remains responsible for displaying it.
16. Shared Header and Footer
One of the simplest but most useful decisions was to avoid duplicating the HTML document structure.
Instead, we created:
includes/
header.php
footer.php
A page can therefore do:
require __DIR__ . '/includes/header.php';
and later:
require __DIR__ . '/includes/footer.php';
The header contains:
- document metadata
- title
- description
- canonical URL
- stylesheet
- site heading
- theme toggle
- navigation
The footer contains:
- copyright
- legal links
- JavaScript
This keeps the page-specific PHP files focused on their actual content.
17. Metadata Is Generated Server-Side
The frontend also takes responsibility for basic SEO metadata.
For an article:
$page_title = $title;
$page_description = $excerpt ?: wp_trim_words(...);
$canonical_url = 'https://sanjibsinha.in/articles/' . rawurlencode($slug);
The shared header then renders:
<title>...</title>
<meta name="description" content="...">
<link rel="canonical" href="...">
The same pattern is used for applications and CMS pages.
This is another benefit of controlling the frontend.
We can decide exactly how the public HTML document is constructed.
18. The CSS Layer
The stylesheet is intentionally self-contained:
assets/style.css
It defines the visual language of the application through CSS custom properties:
:root {
--bg-color: #f8fafc;
--text-main: #0f172a;
--text-muted: #64748b;
--card-bg: #ffffff;
--primary: #2563eb;
--border: #e2e8f0;
}
This gives us a simple design system.
Instead of scattering colors throughout the stylesheet, the main values have names.
For example:
--bg-color
--text-main
--text-muted
--card-bg
--primary
--border
Changing the visual identity of the application therefore becomes much easier.
19. Responsive Design Without a Framework
The frontend does not use Bootstrap, Tailwind or another CSS framework.
The responsive layout is handled directly with CSS media queries.
For example:
@media (max-width: 640px) {
.app-grid {
grid-template-columns: 1fr;
}
}
On larger screens, application cards can appear in a grid.
On smaller screens, they become a single column.
The same principle is applied to:
- header layout
- navigation
- footer
- article typography
- application pages
The result is a responsive interface without introducing another dependency.
20. The Application Grid
The main application listing uses:
.app-grid {
display: grid;
gap: 24px;
grid-template-columns:
repeat(auto-fill, minmax(320px, 1fr));
}
This is a small example of why modern CSS is often enough.
We don’t need a JavaScript layout library.
We don’t need a frontend component framework.
The browser’s own CSS Grid system can handle the layout.
The cards automatically adapt to the available width.
21. The Article Reading Experience
The article page gets its own reading-oriented layout.
The content area is constrained:
.article-content,
.application-content {
max-width: 760px;
}
The typography is deliberately more generous than the listing cards:
.article-body,
.application-body {
font-size: 1.08rem;
line-height: 1.85;
}
Headings, lists, links, images and blockquotes also receive dedicated styling.
This is important because content coming from WordPress can contain ordinary HTML elements.
The frontend therefore provides a visual system capable of presenting those elements consistently.
22. Dark Mode With a Tiny JavaScript Layer
The JavaScript portion of the project is intentionally small.
The first feature is the theme toggle.
The browser checks:
const currentTheme = localStorage.getItem('theme');
If the user previously selected dark mode, the body receives:
body.classList.add('dark-theme');
The CSS variables then change automatically:
body.dark-theme {
--bg-color: #0f172a;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--card-bg: #1e293b;
--border: #334155;
}
This is a good example of keeping JavaScript in its proper place.
JavaScript manages the user’s preference.
CSS manages the visual result.
23. JavaScript Is Enhancement, Not the Foundation
The frontend does not depend on JavaScript to display its primary content.
Articles can render without a client-side application.
Applications can render without a client-side application.
Navigation can work without a JavaScript router.
Pagination can work without JavaScript.
The JavaScript layer enhances the experience with things such as:
Theme switching
Download notification
This produces a useful rule for the project:
If the browser can do something naturally with HTML and CSS, we don’t need JavaScript merely for the sake of having JavaScript.
That keeps the frontend lighter and easier to maintain.
24. Download Feedback
The JavaScript file also contains a small toast notification function:
function trackDownload(appName) {
const toast = document.getElementById('toast');
if (!toast) return;
toast.textContent = `Downloading ${appName}...`;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3500);
}
This is a tiny enhancement.
The download itself does not depend on JavaScript.
The browser simply follows the download URL.
The JavaScript notification merely tells the visitor that the download has started.
Again, progressive enhancement is doing the heavy lifting.
25. Security at the Frontend Boundary
Even a small PHP frontend needs to treat incoming values carefully.
When extracting slugs, we use WordPress’s sanitization:
sanitize_title(
wp_unslash($_GET['slug'])
)
When outputting text, we use escaping such as:
esc_html()
For URLs:
esc_url()
And for attributes:
esc_attr()
This distinction is important.
Data coming from WordPress should not automatically be considered safe merely because it came from our own database.
Data crosses boundaries.
It should be sanitized and escaped at the appropriate points.
26. The Frontend and the Backend Now Have Clear Responsibilities
At this stage the project has a fairly clean division.
WordPress
Content
Posts
Pages
Categories
Applications
Media
Administration
Database
Custom Plugin
API routes
Validation
Data transformation
Application resources
API responses
API errors
PHP Frontend
URLs
Templates
HTML
Metadata
Navigation
Rendering
CSS
Layout
Typography
Responsive design
Themes
Visual states
JavaScript
Theme persistence
Small UI enhancements
Download feedback
That separation is what makes the whole project manageable.
27. The Complete Request Flow
An article request illustrates the entire system.
Suppose a visitor opens:
/articles/building-a-headless-website
The request eventually reaches the article entry point.
The frontend extracts:
building-a-headless-website
It then asks WordPress for the corresponding published post.
WordPress retrieves the content.
PHP prepares the metadata.
The shared header creates the document structure.
The article template renders the content.
The shared footer closes the document.
The browser receives HTML.
The complete flow is:
Visitor
β
βΌ
/articles/building-a-headless-website
β
βΌ
article.php
β
βΌ
WordPress
β
βΌ
WP_Query
β
βΌ
Published Post
β
βΌ
PHP Template
β
βββ HTML
βββ CSS
βββ JavaScript
β
βΌ
Browser
The same basic pattern applies to applications and CMS pages.
28. Why We Didn’t Build a Giant Frontend Framework
It would have been easy to introduce a large JavaScript framework.
We could have built:
React
β
API
β
State management
β
Components
β
Client-side routing
That would certainly be a valid architecture.
But it would solve problems we did not actually have.
Our application is primarily:
Content
+
Applications
+
Pages
For that workload, server-rendered PHP is perfectly capable.
The frontend therefore stays deliberately close to the platform on which it runs.
That makes the application:
- easier to inspect
- easier to deploy
- easier to debug
- easier to customize
- less dependent on external tooling
Sometimes the most useful abstraction is the one we decide not to introduce.
29. From Website to Reusable Frontend
The most interesting consequence of this approach is that the frontend is no longer tied conceptually to one WordPress theme.
We can package it separately.
That package contains the pieces needed to build a similar headless website:
PHP entry points
Shared templates
CSS
JavaScript
Configuration
Routing conventions
Someone could take the frontend and adapt:
branding
colors
navigation
content types
layouts
URLs
application cards
article presentation
while retaining the underlying approach.
That is where the project starts becoming reusable.
30. The First Version Is Intentionally Simple
It is worth emphasizing that this frontend is not intended to compete with Laravel, Symfony or a large application framework.
It is a lightweight application layer.
The purpose of the project was to establish a clean foundation first.
From here, we could eventually extract reusable concepts such as:
Routing
Controllers
Services
API clients
View components
Configuration
Caching
Authentication
But those abstractions should emerge from real requirements.
We don’t need to build a framework simply because frameworks exist.
The first version should remain understandable.
31. What We Have Built
The final frontend is surprisingly small considering what it does.
It provides:
- a public application repository
- article listings
- article detail pages
- application detail pages
- CMS pages
- pagination
- clean URLs
- metadata
- canonical URLs
- responsive design
- dark mode
- downloadable applications
- shared navigation
- shared footer
- lightweight JavaScript enhancement
And it does this using:
PHP
HTML
CSS
JavaScript
WordPress
There is no requirement for a massive client-side runtime.
There is no WordPress theme controlling the public presentation.
There is simply a small PHP application sitting between the visitor and the WordPress backend.
32. The Three Layers Together
We can now put the entire project together.
VISITOR
β
βΌ
βββββββββββββββββ
β Browser β
βββββββββ¬ββββββββ
β
βΌ
βββββββββββββββββ
β PHP Frontend β
β β
β HTML β
β CSS β
β JavaScript β
β Templates β
βββββββββ¬ββββββββ
β
β Data
βΌ
βββββββββββββββββ
β Custom Plugin β
β β
β REST API β
β Validation β
β Transformationβ
βββββββββ¬ββββββββ
β
βΌ
βββββββββββββββββ
β WordPress β
β β
β CMS β
β Database β
β Media β
βββββββββββββββββ
This is the architecture we set out to build.
WordPress manages the content.
The plugin defines the application’s backend interface.
PHP controls the public-facing application.
CSS controls presentation.
JavaScript adds small interactive improvements.
Each layer has a job.
33. From One Website to Two Reusable Packages
The most useful outcome of this project is that we did not stop after making sanjibsinha.in work.
We ended up with two distinct pieces of software:
Backend
β
WordPress + Full Stack App Plugin
Frontend
β
PHP + HTML + CSS + JavaScript
The backend can potentially serve more than one client.
The frontend can potentially consume more than one compatible backend.
That is a much more interesting place to be than a conventional WordPress installation.
The website becomes an implementation of an architecture rather than the architecture itself.
34. Where We Go From Here
The three articles in this series describe the project from three different angles.
The first article explained why we designed the architecture.
The second explained how we turned WordPress into an application backend.
This article explained how we built the public-facing PHP application.
Together, they tell the story:
Architecture
β
Backend
β
API
β
Frontend
β
Website
But there is one final step.
We now have reusable pieces of software.
The natural question is:
How can another developer actually use them?
That leads to the next phase of the project.
We will create a practical manual for the backend package, showing how to install the WordPress plugin, configure it, create application content and expose the API.
After that, we will document the frontend package separately, showing how to configure it and use it as the foundation for another headless PHP website.
At that point, this project stops being merely a record of how we built one website.
It becomes a set of reusable tools and documentation for building the next one.