Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Ahondev/portfolio-v2/llms.txt

Use this file to discover all available pages before exploring further.

This guide walks you through every step required to go from zero to a running WP SSR site. You will install a fresh Bedrock project, configure environment variables, drop in the mu-plugin, boot the Kernel with service providers, register web routes, and finally build the React/Vite frontend. The whole process takes about ten minutes on a local machine with PHP 8.1, Composer, and Node.js already installed.
1

Install Roots Bedrock

Bedrock is the Composer-managed WordPress boilerplate that provides the web/app/mu-plugins/ autoloader, vlucas/phpdotenv, and a clean folder structure. Create a new project with:
composer create-project roots/bedrock my-site
cd my-site
The resulting directory tree looks like this:
my-site/
├── composer.json
├── .env                  # ← copied from .env.example
├── web/
   ├── app/
   ├── mu-plugins/   # ← WP SSR goes here
   ├── plugins/
   └── themes/
   └── wp/               # ← WordPress core (managed by Composer)
└── config/
    └── application.php
2

Configure the environment

Copy .env.example to .env and fill in all required values. WP SSR reads several custom variables at boot time in addition to the standard Bedrock ones:
# Database
DB_NAME='my_db'
DB_USER='root'
DB_PASSWORD='secret'

# WordPress URLs
WP_ENV='development'
WP_HOME='http://localhost'
WP_SITEURL="${WP_HOME}/wp"

# Telegram notifications (ContactService)
TELEGRAM_BOT_TOKEN='123456:ABC-DEF...'
TELEGRAM_CHAT_ID='987654321'

# SSG remote render token (api.ahon.dev)
WEB_RENDER_TOKEN='your-render-token'

# Health endpoint bearer token
HEALTH_ENDPOINT_TOKEN='your-health-token'

# WordPress security salts — generate at https://roots.io/salts.html
AUTH_KEY=''
SECURE_AUTH_KEY=''
LOGGED_IN_KEY=''
NONCE_KEY=''
AUTH_SALT=''
SECURE_AUTH_SALT=''
LOGGED_IN_SALT=''
NONCE_SALT=''
Never commit .env to version control. It contains database credentials and security salts.
WP_ENV='development' enables the Vite dev server detection and auto-creates WordPress pages for every registered route so you can see them in the admin immediately.
3

Install the mu-plugin

Copy the wp-ssr directory into web/app/mu-plugins/. Bedrock’s autoloader will pick it up automatically — no manual plugin activation required.
cp -r /path/to/wp-ssr web/app/mu-plugins/wp-ssr
Then install the plugin’s PHP dependencies:
cd web/app/mu-plugins/wp-ssr
composer install
cd ../../../../
Your web/app/mu-plugins/ directory should now look like:
web/app/mu-plugins/
├── bedrock-autoloader.php   # Bedrock built-in
└── wp-ssr/
    ├── wp-ssr.php            # Plugin bootstrap (loaded by Bedrock)
    ├── src/                  # Framework core
    ├── app/                  # Application layer (providers, controllers…)
    ├── configuration/        # Routes and options
    └── vendor/               # Composer dependencies
ACF Pro must be installed and activated before this step. The Kernel checks for acf_add_options_page() at construction time and calls wp_die() if it is absent.
4

Boot the Kernel in wp-ssr.php

The plugin bootstrap file (wp-ssr.php) is the single entry point for the framework. It first boots Roots Acorn for the theme, then calls Kernel::configure() to register all service providers in order:
<?php

/**
 * Plugin Name: WP SSR Framework
 * Description: Headless CMS core for WordPress (SSR mode)
 */

use Ahon\WPCMS\Kernel;
use Roots\Acorn\Application;

require_once __DIR__.'/vendor/autoload.php';

// Boot Acorn (Roots theme layer)
add_action('after_setup_theme', function () {
    Application::configure()
        ->withProviders([
            App\Providers\ThemeServiceProvider::class,
        ])
        ->boot();
}, 0);

// Bootstrap the WP SSR Kernel
Kernel::configure()
    ->withProviders([
        Ahon\WPCMS\App\Providers\AppServiceProvider::class,
        Ahon\WPCMS\App\Providers\RateLimitServiceProvider::class,
        Ahon\WPCMS\App\Providers\CacheServiceProvider::class,
        Ahon\WPCMS\App\Providers\AnalyticsServiceProvider::class,
        Ahon\WPCMS\App\Providers\SEOServiceProvider::class,
        Ahon\WPCMS\App\Providers\RoutingServiceProvider::class,
        Ahon\WPCMS\App\Providers\SSGServiceProvider::class,
    ])
    ->boot();
Each provider’s register() method runs first (across all providers), then each boot() method fires in the order the providers are listed. The Kernel also hooks WebRouter::init() onto wp_loaded and ApiRouter::init() onto rest_api_init — you do not need to do this manually.
You can create your own providers by extending Ahon\WPCMS\Providers\BaseServiceProvider and adding them to the withProviders() array.
5

Register web routes

Open configuration/routes/web.php. This file is required by WebRouter::init() on every request. Define routes using the Route facade:
<?php

use Ahon\WPCMS\App\Controllers\Web\HomeController;
use Ahon\WPCMS\App\Controllers\Web\BlogController;
use Ahon\WPCMS\App\Controllers\Web\ServiceController;
use Ahon\WPCMS\App\PostTypes\Article;
use Ahon\WPCMS\App\PostTypes\Service;
use Ahon\WPCMS\Routing\Web\WebRoute as Route;

/**
 * Web Routes
 * Use this to register your frontend routes.
 */

// Static page routes — map a URL path to a controller method
Route::get('/', 'Accueil', [HomeController::class, 'home']);
Route::get('/agence', 'Agence', [HomeController::class, 'agence']);
Route::get('/devis', 'Devis', [HomeController::class, 'devis']);
Route::get('/contact', 'Contact', [HomeController::class, 'contact']);

// CPT routes — handle both archive (/services) and single (/services/my-service)
Route::CPT('/services', Service::class, ServiceController::class);
Route::CPT('/blog', Article::class, BlogController::class);
The corresponding controller methods call $this->view() to render a response:
<?php

namespace Ahon\WPCMS\App\Controllers\Web;

use Ahon\WPCMS\App\PostTypes\Service;
use Ahon\WPCMS\Controllers\WebController;

class HomeController extends WebController
{
    public function home(): string
    {
        $services = $this->remember('composer_services_v1', 300, function () {
            return Service::query()->all();
        });

        return $this->view('home', [
            'services' => $services,
        ]);
    }

    public function contact(): string
    {
        return $this->view('contact');
    }
}
The string passed as the first argument to $this->view() (e.g. 'home', 'services') becomes window.__wp_view__ in the browser. Your React SPA uses this string to resolve which view component to mount.You can also register API routes in configuration/routes/api.php using the ApiRoute facade:
<?php

use Ahon\WPCMS\App\Controllers\Api\ContactController;
use Ahon\WPCMS\Routing\Api\ApiRoute as Route;

Route::post('/contact', [ContactController::class, 'contact']);
Route::post('/devis', [ContactController::class, 'devis']);
API routes are mounted under /api/v1/ (rewritten from /wp-json/api/v1/ by AppServiceProvider).
6

Build the React frontend

The React/Vite SPA lives in web/client/. Install dependencies and produce a production build:
cd web/client
yarn install
yarn build
This writes the compiled bundle and a dist/.vite/manifest.json to web/client/dist/. The Vite class in the framework reads this manifest to inject cache-busted <script> and <link> tags into the HTML shell.During development, start the Vite dev server:
yarn dev
Vite writes a web/client/public/hot file containing the dev server URL. The framework detects this file and switches to injecting HMR assets automatically — no configuration needed.
cd web/client
yarn build
# Output: web/client/dist/
The package.json includes React 19, TanStack Query, React Router DOM, Radix UI primitives, Tailwind CSS v4, and Zod — a complete modern frontend stack ready to use.
7

Visit the site

Navigate to your WordPress install (e.g. http://localhost). If everything is configured correctly you will see the React SPA rendered inside the HTML shell produced by src/SSR/client.php.Open the browser developer tools and check the <head> — you should see:
<script>
  window.__wp_data__  = { /* your page data */ };
  window.__wp_view__  = "home";
  window.__wp_seo__   = { "site_title": "My Site", "page_title": "Accueil", ... };
</script>
If window.__wp_view__ is present and matches a view name in your React router, the SPA has successfully booted. The WordPress admin is available at /admin (rewritten from /wp/wp-login.php by AppServiceProvider).
Check the Pages list in the WordPress admin — in development mode the router auto-creates a WordPress page for every Route::get() declaration so you can populate SEO metadata immediately.

Build docs developers (and LLMs) love