The framework implements a hybrid routing system that combines Laravel's powerful routing capabilities with WordPress's template hierarchy. A request that reaches the Laravel router follows a clear priority order:
-
Laravel Routes First: All routes defined in your
routes/web.phpfile take precedence. This gives you full control over specific URLs and allows you to leverage Laravel's routing features when needed. -
WordPress Template Hierarchy as Fallback: If no Laravel route matches the current URL, a catch-all fallback route hands the request to
FrontendController, which asks WordPress which template the request needs and renders the matching Blade view.
This "Laravel-first, WordPress-fallback" approach provides the best of both worlds:
- Full control and flexibility of Laravel routing when you need it
- Automatic handling of standard WordPress content through the template hierarchy
- No need to define routes for every WordPress page or post
Some requests never reach the router: WordPress answers them itself (the admin, wp-login.php, the REST API, robots.txt, the favicon, feeds and trackbacks). See How Pollora runs WordPress inside Laravel.
Incoming Request
│
▼
┌────────────────┐
│ Admin, login, │ Yes ┌─────────────────┐
│ REST, robots, ├──────────►│ WordPress │
│ favicon, feed? │ │ answers it │
└──────┬─────────┘ └─────────────────┘
│ No
▼
┌──────────────┐
│ Match Laravel│ Yes ┌─────────────────┐
│ Route? ├──────────►│ Execute Laravel │
└──────┬───────┘ │ Route │
│ └─────────────────┘
│ No
▼
┌──────────────┐
│ Fallback to │ ┌─────────────────┐
│ Frontend ├──────────►│ Render the │
│ Controller │ │ template found │
└──────────────┘ └─────────────────┘
In Laravel, routes are defined in the routes/web.php file. A route associates a URL with a controller action or a closure that returns a view. For example:
// routes/web.php
use App\Http\Controllers\HomeController;
Route::get('/', [HomeController::class, 'index']);Here, when the URL /home is accessed, the index method of the HomeController is invoked.
You can group routes to apply shared attributes, such as middleware, prefixes, or namespaces. Middleware can help perform actions before or after a request reaches the controller. For example:
// Group routes with middleware and prefix
Route::middleware('auth')->prefix('admin')->group(function () {
Route::get('dashboard', 'AdminController@dashboard');
Route::get('settings', 'AdminController@settings');
});Route naming, or aliasing, allows you to refer to routes by a name instead of their URL. This is particularly useful when generating URLs or redirecting. Here's how you can name a route:
// Named route for the home page
Route::get('/', [HomeController::class, 'index'])->name('home');You can find more about Laravel's routes in the official documentation.
Pollora Framework provides WordPress-specific routes that integrate with WordPress's conditional tags system.
There are two main methods to define routes in your application:
-
Standard Laravel Routes (
Route::get(),Route::post(),Route::any(), etc.):- These routes follow Laravel's standard routing behavior
- They do not interact with WordPress's conditional tags system
- They do not automatically receive WordPress-specific middleware
-
WordPress-Specific Routes (
Route::wp()andRoute::wpMatch()):- These routes integrate with WordPress's conditional tags system
- They automatically receive WordPress-specific middleware:
WordPressBindings: Adds WordPress objects (post, query) to the routeWordPressHeaders: Manages HTTP headers for WordPress responsesWordPressShutdown: Runs WordPress's shutdown hooks before the response is sentWordPressTemplateEnhancement: Runs WordPress's template enhancement filter on the page, which moves block styles into the<head>
- WordPress's body classes and
is_404()are left as WordPress resolved them; only routes WordPress does not answer are adjusted (see Middleware) - They are processed through WordPress's conditional logic
Route::wp()accepts all HTTP verbsRoute::wpMatch()allows specifying specific HTTP verbs
WordPress routes use conditions that can be specified in two ways:
- Using predefined aliases (shorter syntax)
- Using actual WordPress conditional function names (more explicit)
The framework provides a default set of aliases in the config/wordpress.php file. These aliases map WordPress conditional tags to route URIs:
return [
'conditions' => [
// Core WordPress conditions
'is_404' => '404',
'is_archive' => 'archive',
'is_attachment' => 'attachment',
'is_author' => 'author',
'is_category' => ['category', 'cat'],
'is_date' => 'date',
'is_day' => 'day',
'is_front_page' => ['/', 'front'],
'is_home' => ['home', 'blog'],
'is_month' => 'month',
'is_page_template' => 'template',
'is_page' => 'page',
'is_paged' => 'paged',
'is_post_type_archive' => ['post-type-archive', 'postTypeArchive'],
'is_search' => 'search',
'is_single' => 'single',
'is_singular' => 'singular',
'is_sticky' => 'sticky',
'is_subpage' => ['subpage', 'subpageof'],
'is_tag' => 'tag',
'is_tax' => 'tax',
'is_time' => 'time',
'is_year' => 'year',
// WooCommerce conditions
'is_shop' => 'shop',
'is_product' => 'product',
'is_cart' => 'cart',
'is_checkout' => 'checkout',
'is_account_page' => 'account',
'is_product_category' => 'product_category',
'is_product_tag' => 'product_tag',
'is_wc_endpoint_url' => 'wc_endpoint',
],
// ... other WordPress configuration options
];The framework provides a default set of aliases in its config/wordpress.php file. You can override or extend these aliases in your application's config/wordpress.php file.
The WooCommerce conditions (shop, product, cart, checkout, account…) are added by pollora/woocommerce: composer require pollora/woocommerce in a project that runs WooCommerce.
To customize WordPress route conditions, you can publish the framework's configuration file to your application:
php artisan vendor:publish --tag=wordpressThis command will copy the framework's configuration file to your application's config/ directory, allowing you to customize it according to your needs.
Each alias maps a WordPress conditional function (like is_page()) to a route URI or array of URIs. For example, 'is_page' => 'page' means that when you use Route::wp('page', ...) or Route::wp('is_page', ...), it will check if the current request matches the is_page() WordPress condition.
To define a WordPress-specific route, you can use either the wp or wpMatch method. You have two ways to specify the WordPress condition:
- Using the predefined aliases (shorter syntax)
- Using the actual WordPress conditional function names (more explicit)
Here are examples of both approaches:
// Using aliases (shorter syntax)
Route::wp('single', function () {
return view('post');
});
// Using actual WordPress function names (more explicit)
Route::wp('is_single', function () {
return view('post');
});
// Using wpMatch with aliases
Route::wpMatch('GET', 'page', 'landing-page', function() {
return view('pages.landing');
});
// Using wpMatch with actual WordPress function names
Route::wpMatch('GET', 'is_page', 'landing-page', function() {
return view('pages.landing');
});
// Using wpMatch with multiple HTTP verbs and aliases
Route::wpMatch(['GET', 'POST'], 'page', ['contact', 'request-a-quote'], [FormController::class, 'index']);
// Using wpMatch with multiple HTTP verbs and actual WordPress function names
Route::wpMatch(['GET', 'POST'], 'is_page', ['contact', 'request-a-quote'], [FormController::class, 'index']);Both methods accept a variable number of arguments:
-
For
wp:- First argument is the WordPress condition (either an alias or the actual function name)
- Last argument is the callback function or controller action
- Any arguments in between are passed as parameters to the WordPress condition function
-
For
wpMatch:- First argument is the HTTP method(s) ('GET', 'POST', etc. or an array of methods)
- Second argument is the WordPress condition (either an alias or the actual function name)
- Last argument is the callback function or controller action
- Any arguments in between are passed as parameters to the WordPress condition function
For example:
// Route for GET requests to a specific page using alias
Route::wpMatch('GET', 'page', 'landing-page', function() {
return view('pages.landing');
});
// Route for GET requests to a specific page using actual function name
Route::wpMatch('GET', 'is_page', 'landing-page', function() {
return view('pages.landing');
});
// Route for POST requests to a form page using alias
Route::wpMatch('POST', 'page', 'contact', [FormController::class, 'submit']);
// Route for POST requests to a form page using actual function name
Route::wpMatch('POST', 'is_page', 'contact', [FormController::class, 'submit']);
// Route accepting both GET and POST for a product using alias
Route::wpMatch(['GET', 'POST'], 'singular', 'product', 123, function() {
return view('product.show');
});
// Route accepting both GET and POST for a product using actual function name
Route::wpMatch(['GET', 'POST'], 'is_singular', 'product', 123, function() {
return view('product.show');
});
// Route for all HTTP verbs using wp shortcut with alias
Route::wp('tax', 'product_cat', 'electronics', function() {
return view('taxonomy.product-category');
});
// Route for all HTTP verbs using wp shortcut with actual function name
Route::wp('is_tax', 'product_cat', 'electronics', function() {
return view('taxonomy.product-category');
});When using Route::wp() or Route::wpMatch() instead of Route::any() or other standard methods:
- The route will be processed through WordPress's conditional tags system
- WordPress-specific middleware will be automatically applied
- WordPress objects like
$postand$wp_querywill be available in the route parameters - The route will only match if the WordPress condition is met
When working with WordPress custom templates (defined in themes/[theme-name]/config/templates.php), you can create specific routes to handle these templates. For example:
// This will match any page using the "contact" template
Route::wp('template', 'contact', function () {
return view('page');
});
// This will match any page using the "landing" template and pass data to the view
Route::wp('template', 'landing', function () {
return view('page.landing', [
'features' => Feature::all(),
]);
});Route::wp('page')), otherwise they won't be taken into account.
You can extend the WordPress route conditions by adding your own entries to the conditions array in your application's config/wordpress.php file:
// config/wordpress.php
return [
'conditions' => [
// Add your custom conditions
'is_custom_post_type' => 'custom-post-type',
'is_special_page' => ['special-page', 'special'],
// Override existing conditions
'is_page' => ['page', 'static-page'],
],
];You can also create your own WordPress conditional functions and use them in your routes:
// functions.php or a custom plugin file
function is_special_page() {
// Your custom logic to determine if this is a special page
return is_page() && has_term('special', 'page_category');
}
// routes/web.php
Route::wp('special-page', function() {
return view('pages.special');
});This approach allows you to create highly customized routing logic while maintaining the clean syntax of WordPress-style routes.
When no route matches, WordPress decides which template the request needs (single, page, archive, 404...) and Pollora looks for the matching Blade view: it swaps .php for .blade.php in each candidate of WordPress's template hierarchy and searches the theme's view paths. The mapping is described in Theme Structure. To add candidates of your own, use WordPress's {type}_template_hierarchy filters, as shown in Extending the Template Hierarchy.
The framework implements a cascading routing system that works as follows:
-
Requests WordPress answers itself: the admin,
wp-login.php, the REST API and the special requests below (robots.txt, favicon, feeds, trackbacks) are answered by WordPress. The Laravel router does not run for them. -
Laravel Custom Routes: The framework checks if a matching route exists in your
routes/web.phpfile. These routes have high priority and will be executed if they match the requested URL. -
WordPress Template Hierarchy: If no custom route matches, the fallback route calls
FrontendController, which:- Determines the appropriate template hierarchy based on WordPress conditional tags
- Sequentially searches for matching Blade views in your views directory
- Returns the first view found in the hierarchy order
-
Fallback Handling: If the hierarchy gives no Blade view, a template file WordPress resolved (a block theme's
template-canvas.php, for example) is included as it is. A request for a page that does not exist, with no 404 template in the theme, gets Laravel's 404 page.
For example, for an "About" page, the process would be as follows:
// 1. First checks if a custom route exists in routes/web.php
Route::wp('page', 'about-us', function() {
return view('pages.about');
});
// 2. If no route matches, FrontendController checks the template hierarchy:
// - page-about.blade.php
// - page.blade.php
// - singular.blade.php
// - index.blade.php
// 3. If no view is found, a block theme's template file is included, or Laravel's 404 page is returnedThis approach offers several benefits:
- Allows fine-grained customization via Laravel routes when needed
- Maintains compatibility with WordPress template hierarchy
- Provides automatic fallback to more generic templates
- Reduces boilerplate code by automatically handling standard cases
- Ensures that all requests are handled appropriately, even special WordPress requests
routes/web.php always take precedence over the automatic template system. Use this advantage to:
- Add custom logic to specific pages
- Inject specific data into your views
- Handle special cases requiring processing before display
The framework hands these WordPress request types back to WordPress:
- robots.txt: Requests for
/robots.txtare handled by WordPress's built-indo_robotsaction - favicon.ico: Requests for
/favicon.icoare handled by WordPress's built-indo_faviconaction - Feeds: RSS and Atom feed requests are handled by WordPress's built-in
do_feedfunction - Trackbacks: Trackback requests are handled by WordPress's built-in trackback handler
They are answered once template_redirect has run, and the request ends there: the Laravel router never sees them, so a Route::wp() declared for them is never reached. To change what they return, use WordPress's own hooks, for example the robots_txt filter for robots.txt:
add_filter('robots_txt', fn (string $output): string => $output."\nDisallow: /private/");After all other routes, the framework registers a catch-all fallback route, {any}, which delegates to the FrontendController. It matches every path except those starting with api/. This controller:
- Determines the appropriate template based on WordPress's template hierarchy
- Renders the corresponding Blade view
- Includes the template file WordPress resolved when there is no Blade view (a block theme's template), and returns Laravel's 404 page when nothing fits
This ensures that all requests are handled appropriately, even if no explicit route is defined.
Themes and plugins can define their own routes by placing route files in a routes/ directory at their root. The framework automatically discovers and loads these files during registration.
| File | Prefix | Use case |
|---|---|---|
routes/api.php |
/api |
JSON endpoints, AJAX handlers, headless APIs |
routes/web.php |
none | Custom pages, form handlers, redirects |
// themes/my-theme/routes/api.php
use Illuminate\Support\Facades\Route;
use Theme\MyTheme\Http\Controllers\Api\ProductSearchController;
Route::get('/products/search', ProductSearchController::class);
// → accessible at GET /api/products/search// themes/my-theme/routes/web.php
use Illuminate\Support\Facades\Route;
Route::get('/custom-page', function () {
return view('pages.custom');
});API routes are standard Laravel routes — you can use controllers, middleware, route groups, rate limiting, and all other Laravel routing features.
API routes (/api/*) automatically benefit from lightweight mode: WordPress loads without plugins, reducing response times from ~1.3s to ~100ms. The theme is still loaded, so routes, controllers, and services work normally.
This is controlled by the wordpress.api_plugins configuration key:
// config/wordpress.php
// No plugins loaded (fastest — default)
'api_plugins' => [],
// Selective: only these plugins are loaded
'api_plugins' => ['woocommerce', 'advanced-custom-fields-pro'],
// Glob patterns: load all plugins matching a prefix
'api_plugins' => ['woocommerce*'],
// → matches woocommerce, woocommerce-subscriptions, woocommerce-payments, etc.
// Load all plugins (disable lightweight mode)
'api_plugins' => ['*'],
// or
'api_plugins' => null,Since plugins are not loaded by default, API route handlers should use Eloquent / DB facade instead of WordPress or plugin functions (e.g., get_permalink(), wc_get_product()). When a handler needs a specific plugin, add it to the api_plugins array or use the function_exists() pattern to provide fallbacks:
// Works with or without WooCommerce loaded
if (function_exists('get_woocommerce_currency_symbol')) {
$currency = get_woocommerce_currency_symbol();
} else {
$currency = DB::table('options')
->where('option_name', 'woocommerce_currency')
->value('option_value') ?? 'EUR';
}Plugins follow the same convention. Place a routes/api.php or routes/web.php in the plugin root:
my-plugin/
├── app/
│ └── Http/Controllers/Api/
│ └── StatsController.php
├── routes/
│ └── api.php ← GET /api/stats
├── config/
└── my-plugin.php
// plugins/my-plugin/routes/api.php
use Illuminate\Support\Facades\Route;
use Plugin\MyPlugin\Http\Controllers\Api\StatsController;
Route::get('/stats', StatsController::class);| Feature | Theme/Plugin API routes | WordPress REST API |
|---|---|---|
| Prefix | /api/ |
/wp-json/ |
| Framework | Laravel router | WP REST infrastructure |
| Plugins loaded | Configurable (none by default) | All |
| Response time | ~100ms | ~1.3s |
| Authentication | Laravel middleware | WP nonce / application passwords |
| ORM | Eloquent / DB facade | WP_Query / wpdb |
Use theme/plugin API routes for performance-critical endpoints (search suggestions, autocomplete, dashboards). Use the WordPress REST API when you need full WordPress context (Gutenberg editor, third-party plugin integrations).