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.