Contensio themes vs WordPress themes
A side-by-side comparison for WordPress theme developers — same mental model, familiar hierarchy, with a few deliberate improvements.
If you've built WordPress themes, building a Contensio theme will feel familiar. The template hierarchy follows the same most-specific-first logic. The file names map almost 1:1. The mental model — create a file, it gets used for that page type — is identical.
This page walks through what's the same, what's different, and what's actually better.
The hierarchy is the same
WordPress and Contensio both use a candidate-list resolution strategy. For each request, a list of templates is built in order of specificity, and the first file that exists is used. Everything else falls through to a final fallback.
WordPress — for a blog post with slug hello-world:
single-post-hello-world.php
single-post.php
single.php
singular.php
index.php
Contensio — same post:
single-post-hello-world.blade.php
single-post.blade.php
single.blade.php
index.blade.php
The difference is .blade.php instead of .php. The logic is identical.
File name mapping
| WordPress | Contensio |
|---|---|
index.php |
index.blade.php |
front-page.php |
front-page.blade.php |
home.php |
home.blade.php |
single.php |
single.blade.php |
single-post.php |
single-post.blade.php |
single-{type}.php |
single-{type}.blade.php |
page.php |
page.blade.php |
page-{slug}.php |
page-{slug}.blade.php |
archive.php |
archive.blade.php |
archive-{type}.php |
archive-{type}.blade.php |
taxonomy.php |
taxonomy.blade.php |
taxonomy-{slug}.php |
taxonomy-{slug}.blade.php |
category.php |
category.blade.php |
tag.php |
tag.blade.php |
author.php |
author.blade.php |
search.php |
search.blade.php |
404.php |
404.blade.php |
header.php + get_header() |
layout.blade.php + @extends('theme::layout') |
get_template_part() |
@include('theme::partials.name') |
If you have a WordPress theme you want to port, rename files and replace WordPress template tags with Contensio's Blade variables.
Template tags → Blade variables
In WordPress, you call functions to get data. In Contensio, data is passed directly to the view as variables.
WordPress
<?php get_header(); ?>
<h1><?php the_title(); ?></h1>
<div><?php the_content(); ?></div>
<p>By <?php the_author(); ?></p>
<time><?php the_date(); ?></time>
<?php get_footer(); ?>
Contensio
@extends('theme::layout')
@section('content')
<h1>{{ $translation->title }}</h1>
<div class="prose">
@foreach($content->blocks ?? [] as $block)
@include('theme::partials.block', ['block' => $block, 'langId' => $lang?->id])
@endforeach
</div>
<p>By {{ $content->author?->name }}</p>
<time>{{ $content->published_at?->format('M d, Y') }}</time>
@endsection
No global state, no function calls to get data — it's all there in the variable.
The Loop → $posts variable
WordPress uses The Loop to iterate posts. You call have_posts() and the_post() to move a global pointer.
WordPress
<?php if (have_posts()) : ?>
<?php while (have_posts()) : the_post(); ?>
<h2><?php the_title(); ?></h2>
<p><?php the_excerpt(); ?></p>
<?php endwhile; ?>
<?php endif; ?>
Contensio
@forelse($posts as $post)
@php $trans = $post->translations->first(); @endphp
<h2>{{ $trans?->title }}</h2>
<p>{{ $trans?->excerpt }}</p>
@empty
<p>No posts found.</p>
@endforelse
$posts is a Laravel paginator — it's already filtered, sorted, and paginated. No query manipulation needed in templates.
get_template_part() → @include
WordPress uses get_template_part() to load reusable pieces. Contensio uses standard Blade includes.
WordPress
get_template_part('template-parts/content', 'post');
// loads template-parts/content-post.php
Contensio
@include('theme::partials.post-card', ['post' => $post])
Blade's @include also has @includeIf (include only if exists), @includeWhen (conditional), and @includeFirst (try multiple). All standard — no custom functions needed.
wp_head / wp_footer → do_action
WordPress themes call wp_head() and wp_footer() in their templates to let plugins inject markup.
WordPress
<head>
<?php wp_head(); ?>
</head>
<body>
<?php wp_body_open(); ?>
...
<?php wp_footer(); ?>
</body>
Contensio
<head>
@php do_action('contensio/frontend/head') @endphp
@stack('head')
</head>
<body>
@php do_action('contensio/frontend/body-open') @endphp
...
@stack('scripts')
@php do_action('contensio/frontend/footer') @endphp
</body>
The hook names are different but the mechanism is the same. @stack('head') and @stack('scripts') are Blade's built-in push stacks, which work alongside the action hooks.
the_content() → apply_filters
WordPress's the_content() runs the content through a filter so plugins can modify it (auto-embed, shortcodes, etc.).
WordPress
the_content();
// internally: echo apply_filters('the_content', $content);
Contensio — same pattern, explicit:
@php ob_start(); @endphp
@foreach($content->blocks ?? [] as $block)
@include('theme::partials.block', ['block' => $block, 'langId' => $lang?->id])
@endforeach
@php $body = ob_get_clean(); @endphp
{!! apply_filters('contensio/frontend/content-body', $body, $content) !!}
Being explicit means you know exactly what gets filtered. No hidden magic in a template tag.
functions.php → service provider
WordPress themes load functions.php automatically — it's where you register menus, image sizes, sidebars, and hook callbacks.
WordPress
// functions.php
function mytheme_setup() {
register_nav_menus(['primary' => 'Primary Menu']);
add_theme_support('post-thumbnails');
}
add_action('after_setup_theme', 'mytheme_setup');
add_filter('the_title', function ($title) {
return '— ' . $title . ' —';
});
Contensio — hooks go in a service provider. Themes can ship a service provider in their package:
// src/MyThemeServiceProvider.php
class MyThemeServiceProvider extends ServiceProvider
{
public function boot(): void
{
add_filter('contensio/frontend/page-title', function (string $title) {
return '— ' . $title . ' —';
});
}
}
If your theme doesn't need PHP logic, you don't need a service provider at all. Most themes are just Blade files.
Enqueuing scripts and styles
WordPress — use wp_enqueue_scripts:
add_action('wp_enqueue_scripts', function () {
wp_enqueue_style('mytheme', get_stylesheet_uri());
wp_enqueue_script('mytheme-js', get_template_directory_uri() . '/js/app.js', [], null, true);
});
Contensio — link assets directly in layout.blade.php:
<link rel="stylesheet" href="{{ asset('packages/themes/acme/mytheme/assets/style.css') }}">
<script src="{{ asset('packages/themes/acme/mytheme/assets/app.js') }}" defer></script>
No queue, no dependencies array, no priority management. Direct links. If a plugin needs to inject its own styles, it uses the contensio/frontend/head action hook.
Sidebars → none (by design)
WordPress sidebars are dynamic widget areas registered in functions.php and rendered with dynamic_sidebar(). Contensio doesn't have a widget/sidebar system.
Instead, you hardcode your sidebar Blade partials, or use menu models if navigation is what you need. For truly dynamic sidebar content, use Hook::render() — a UI-render hook that lets plugins insert HTML into named slots:
{{-- layout.blade.php --}}
<aside>
{!! \Contensio\Support\Hook::render('contensio/theme/sidebar') !!}
</aside>
// In a plugin — add a widget to the sidebar
\Contensio\Support\Hook::add('contensio/theme/sidebar', function () {
return view('myplugin::widgets.recent-posts')->render();
});
Theme options → theme.json customise
WordPress theme options typically use the Customizer API or an options page with get_theme_mod().
WordPress
// functions.php
add_action('customize_register', function ($wp_customize) {
$wp_customize->add_setting('accent_color', ['default' => '#2563eb']);
$wp_customize->add_control(new WP_Customize_Color_Control($wp_customize, 'accent_color', [
'label' => 'Accent color',
]));
});
// In template
$color = get_theme_mod('accent_color', '#2563eb');
Contensio — define options in theme.json and read via $themeOptions:
{
"customise": {
"accent_color": {
"type": "color",
"label": "Accent color",
"default": "#2563eb"
}
}
}
<style>
:root { --accent: {{ $themeOptions['accent_color'] ?? '#2563eb' }}; }
</style>
What Contensio doesn't have
A few WordPress theme concepts that don't exist in Contensio:
| WordPress | Contensio | Notes |
|---|---|---|
functions.php |
Service provider | Optional — most themes don't need it |
| Widgets / sidebars | Hook::render() |
Use named render hooks instead |
the_excerpt() |
$translation->excerpt |
Just a variable |
the_permalink() |
route('contensio.post', $slug) |
Standard Laravel routing |
bloginfo() |
$site['name'], $site['tagline'] |
Array keys |
get_post_meta() |
$fieldValues[$key] |
Passed directly to the view |
| Child themes | Not applicable | Override via template hierarchy |
comments_template() |
@include('contensio::frontend.partials.comments', ...) |
Provided by core |
wp_link_pages() |
Not implemented | Block-based content has no page breaks |
Summary
| Concept | WordPress | Contensio |
|---|---|---|
| Template engine | PHP | Blade |
| Template resolution | File scan | ThemeTemplateResolver::resolve() |
| Data in templates | Template tag functions | Variables passed by controller |
| Content loop | have_posts() / the_post() |
@forelse($posts as $post) |
| Layout wrapper | get_header() / get_footer() |
@extends('theme::layout') |
| Partials | get_template_part() |
@include('theme::partials.name') |
| Hooks | wp_head(), wp_footer(), the_content() |
do_action() / apply_filters() |
| Assets | wp_enqueue_scripts |
Direct <link> / <script> in layout |
| PHP logic | functions.php |
Service provider (optional) |
| Theme options | Customizer + get_theme_mod() |
theme.json customise + $themeOptions |
If you know WordPress themes, you can build a Contensio theme in an afternoon. The hierarchy is familiar, the override mechanism is the same, and the Blade syntax is more readable than raw PHP tags.
See also
- Creating a theme — full guide with variable reference and hook integration
- Template hierarchy — resolver order for every page type
- Contensio hooks vs WordPress hooks — same comparison, for the hook system