Contensio logo

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_footerdo_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