v0.1.1 - Bootstrap frontend rendering via Twig templates
All checks were successful
Create Release Package / PHP Lint (push) Successful in 49s
Create Release Package / Build Release (push) Successful in 1m18s

Replace FSE block markup on the frontend with proper Bootstrap 5 HTML
rendered through Twig templates. The Site Editor remains functional for
admin editing while the public site outputs Bootstrap navbar, cards,
pagination, grid layout, and responsive components.

New PHP classes: TemplateController, ContextBuilder, NavWalker
New Twig templates: 20 files (base, pages, partials, components)
Enhanced TwigService with WordPress functions and globals

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-02-08 15:11:00 +01:00
parent d069a203b4
commit cb288d6e74
32 changed files with 1439 additions and 29 deletions

View File

21
views/base.html.twig Normal file
View File

@@ -0,0 +1,21 @@
<!doctype html>
<html {{ language_attributes() }}>
<head>
<meta charset="{{ charset }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{ wp_head() }}
</head>
<body {{ body_class() }}>
{{ wp_body_open() }}
{% include 'partials/header.html.twig' %}
<main id="main-content" class="{% block main_class %}py-4{% endblock %}">
{% block content %}{% endblock %}
</main>
{% include 'partials/footer.html.twig' %}
{{ wp_footer() }}
</body>
</html>

View File

@@ -0,0 +1,17 @@
<article class="card h-100">
{% if post.thumbnail %}
<a href="{{ post.url }}">
<img src="{{ post.thumbnail }}" class="card-img-top"
alt="{{ post.title|e('html_attr') }}"
style="aspect-ratio: 3/2; object-fit: cover;">
</a>
{% endif %}
<div class="card-body">
<h3 class="card-title h6">
<a href="{{ post.url }}" class="text-decoration-none text-body">{{ post.title }}</a>
</h3>
<p class="card-text text-body-secondary small">
<time datetime="{{ post.date_iso }}">{{ post.date }}</time>
</p>
</div>
</article>

View File

@@ -0,0 +1,21 @@
<article class="card mb-4 border-0 border-bottom rounded-0 pb-4">
{% if post.thumbnail %}
<a href="{{ post.url }}">
<img src="{{ post.thumbnail }}" class="card-img-top rounded" alt="{{ post.title|e('html_attr') }}">
</a>
{% endif %}
<div class="card-body px-0">
<h2 class="card-title h4">
<a href="{{ post.url }}" class="text-decoration-none text-body">{{ post.title }}</a>
</h2>
<div class="text-body-secondary small mb-2">
<time datetime="{{ post.date_iso }}">{{ post.date }}</time>
<span class="mx-1">&middot;</span>
<a href="{{ post.author.url }}" class="text-body-secondary text-decoration-none">{{ post.author.name }}</a>
</div>
<p class="card-text">{{ post.excerpt|raw }}</p>
<a href="{{ post.url }}" class="btn btn-outline-primary btn-sm">
{{ post.read_more }}
</a>
</div>
</article>

View File

@@ -0,0 +1,11 @@
{% if posts|length > 0 %}
{% for post in posts %}
{% include 'components/card-post.html.twig' with {'post': post} only %}
{% endfor %}
{% include 'partials/pagination.html.twig' %}
{% else %}
<div class="alert alert-secondary" role="alert">
{{ __('No posts were found.') }}
</div>
{% endif %}

16
views/pages/404.html.twig Normal file
View File

@@ -0,0 +1,16 @@
{% extends 'base.html.twig' %}
{% block content %}
<div class="container text-center py-5">
<h1 class="display-1 fw-bold text-body-secondary">404</h1>
<h2 class="mb-3">{{ __('Page not found') }}</h2>
<p class="lead text-body-secondary mb-4">
{{ __('The page you are looking for does not exist, or it has been moved. Please try searching using the form below.') }}
</p>
<div class="row justify-content-center">
<div class="col-md-6">
{% include 'partials/search-form.html.twig' %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends 'base.html.twig' %}
{% block content %}
<div class="container">
<h1 class="mb-2">{{ archive.title|raw }}</h1>
{% if archive.description %}
<div class="lead text-body-secondary mb-4">{{ archive.description|raw }}</div>
{% endif %}
{% include 'components/post-loop.html.twig' %}
</div>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends 'base.html.twig' %}
{% block content %}
<div class="container">
<h1 class="mb-4">{{ __('Blog') }}</h1>
{% if layout == 'sidebar' %}
<div class="row">
<div class="col-lg-8">
{% include 'components/post-loop.html.twig' %}
</div>
<div class="col-lg-4">
{% include 'partials/sidebar.html.twig' %}
</div>
</div>
{% else %}
{% include 'components/post-loop.html.twig' %}
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends 'base.html.twig' %}
{% block content %}
<div class="container">
<article class="py-4">
{% if post.thumbnail %}
<figure class="mb-4">
<img src="{{ post.thumbnail }}" class="img-fluid rounded"
alt="{{ post.title|e('html_attr') }}">
</figure>
{% endif %}
<h1>{{ post.title }}</h1>
<div class="post-content">
{{ post.content|raw }}
</div>
</article>
</div>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'base.html.twig' %}
{% block content %}
<div class="container">
<h1 class="mb-4">
{{ __('Search results for: %s')|format('<em>' ~ search_query ~ '</em>')|raw }}
</h1>
{% include 'partials/search-form.html.twig' %}
<div class="mt-4">
{% include 'components/post-loop.html.twig' %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,50 @@
{% extends 'base.html.twig' %}
{% block content %}
<div class="container">
<article class="py-4">
<header class="mb-4">
<h1>{{ post.title }}</h1>
{% include 'partials/meta.html.twig' %}
</header>
{% if post.thumbnail %}
<figure class="mb-4">
<img src="{{ post.thumbnail }}" class="img-fluid rounded"
alt="{{ post.title|e('html_attr') }}"
style="aspect-ratio: 16/9; object-fit: cover; width: 100%;">
</figure>
{% endif %}
<div class="post-content">
{{ post.content|raw }}
</div>
{% if post.tags|length > 0 %}
<div class="mt-4 mb-4">
{% for tag in post.tags %}
<a href="{{ tag.url }}" class="badge bg-secondary text-decoration-none me-1">
{{ tag.name }}
</a>
{% endfor %}
</div>
{% endif %}
{% include 'partials/post-navigation.html.twig' %}
{% include 'partials/comments.html.twig' %}
</article>
{% if more_posts is defined and more_posts|length > 0 %}
<section class="py-5 border-top">
<h2 class="h4 mb-4">{{ __('More posts') }}</h2>
<div class="row row-cols-1 row-cols-md-3 g-4">
{% for post in more_posts %}
<div class="col">
{% include 'components/card-post-grid.html.twig' with {'post': post} only %}
</div>
{% endfor %}
</div>
</section>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,39 @@
<div class="comment d-flex gap-3 mb-4{% if depth > 0 %} ms-5{% endif %}" id="comment-{{ comment.id }}">
<div class="flex-shrink-0">
<img src="{{ comment.avatar_url }}" alt="{{ comment.author }}"
class="rounded-circle" width="40" height="40">
</div>
<div class="flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-1">
<strong class="small">
{% if comment.author_url %}
<a href="{{ comment.author_url }}" class="text-decoration-none text-body" rel="nofollow">
{{ comment.author }}
</a>
{% else %}
{{ comment.author }}
{% endif %}
</strong>
<time class="text-body-secondary small" datetime="{{ comment.date_iso }}">
{{ comment.date }}
</time>
{% if comment.edit_url %}
<a href="{{ comment.edit_url }}" class="text-body-secondary small">{{ __('Edit') }}</a>
{% endif %}
</div>
<div class="comment-content small">
{{ comment.content|raw }}
</div>
{% if comment.reply_url %}
<div class="mt-1">
{{ comment.reply_url|raw }}
</div>
{% endif %}
{% if comment.children|length > 0 %}
{% for child in comment.children %}
{% include 'partials/comment-item.html.twig' with {'comment': child, 'depth': depth + 1} only %}
{% endfor %}
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,19 @@
{% if comments is defined and (comments.is_open or comments.count > 0) %}
<section class="comments-section border-top pt-5 mt-5" id="comments">
<h2 class="h4 mb-4">{{ comments.title }}</h2>
{% if comments.list|length > 0 %}
<div class="comment-list mb-4">
{% for comment in comments.list %}
{% include 'partials/comment-item.html.twig' with {'comment': comment, 'depth': 0} only %}
{% endfor %}
</div>
{% endif %}
{% if comments.is_open %}
<div class="comment-form mt-4">
{{ comments.form|raw }}
</div>
{% endif %}
</section>
{% endif %}

View File

@@ -0,0 +1,21 @@
<button type="button" class="wp-bootstrap-dark-mode-toggle ms-2"
data-bs-theme-toggle
aria-label="{{ __('Switch to dark mode') }}"
data-label-dark="{{ __('Switch to dark mode') }}"
data-label-light="{{ __('Switch to light mode') }}"
aria-pressed="false">
<svg class="wp-bootstrap-sun-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;" aria-hidden="true">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<svg class="wp-bootstrap-moon-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>

View File

@@ -0,0 +1,34 @@
<footer class="bg-body-tertiary mt-auto">
<div class="container py-5">
<div class="row">
<div class="col-md-6">
<h5 class="fw-bold">{{ site.name }}</h5>
<p class="text-body-secondary">{{ site.description }}</p>
</div>
<div class="col-md-6 text-md-end">
{% if footer_menu|length > 0 %}
<ul class="list-unstyled">
{% for item in footer_menu %}
<li>
<a href="{{ item.url }}" class="text-body-secondary text-decoration-none">
{{ item.title }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
<hr>
<div class="row align-items-center">
<div class="col-md-6">
<p class="text-body-secondary small mb-0">&copy; {{ current_year }} {{ site.name }}</p>
</div>
<div class="col-md-6 text-md-end">
<p class="text-body-secondary small mb-0">
{{ __('Powered by %s')|format('<a href="https://wordpress.org" rel="nofollow" class="text-body-secondary">WordPress</a>')|raw }}
</p>
</div>
</div>
</div>
</footer>

View File

@@ -0,0 +1,56 @@
<header>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container">
<a class="navbar-brand fw-bold" href="{{ site.url }}">
{{ site.name }}
</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarMain"
aria-controls="navbarMain" aria-expanded="false"
aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarMain">
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
{% for item in menu %}
{% if item.children|length > 0 %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle{{ item.active ? ' active' : '' }}"
href="{{ item.url }}" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
{{ item.title }}
</a>
<ul class="dropdown-menu">
{% for child in item.children %}
<li>
<a class="dropdown-item{{ child.active ? ' active' : '' }}"
href="{{ child.url }}"
{% if child.target %}target="{{ child.target }}"{% endif %}>
{{ child.title }}
</a>
</li>
{% endfor %}
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link{{ item.active ? ' active' : '' }}"
href="{{ item.url }}"
{% if item.active %}aria-current="page"{% endif %}
{% if item.target %}target="{{ item.target }}"{% endif %}>
{{ item.title }}
</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% if dark_mode %}
{% include 'partials/dark-mode-toggle.html.twig' %}
{% endif %}
</div>
</div>
</nav>
</header>

View File

@@ -0,0 +1,11 @@
<div class="text-body-secondary small mb-3">
<time datetime="{{ post.date_iso }}">{{ post.date }}</time>
<span class="mx-1">&middot;</span>
<a href="{{ post.author.url }}" class="text-body-secondary text-decoration-none">{{ post.author.name }}</a>
{% if post.categories is defined and post.categories|length > 0 %}
<span class="mx-1">&middot;</span>
{% for cat in post.categories %}
<a href="{{ cat.url }}" class="text-body-secondary text-decoration-none">{{ cat.name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
{% endif %}
</div>

View File

@@ -0,0 +1,26 @@
{% if pagination and pagination.pages is defined and pagination.pages|length > 1 %}
<nav aria-label="{{ __('Page navigation') }}" class="my-5">
<ul class="pagination justify-content-center">
<li class="page-item{{ pagination.prev_url is null ? ' disabled' : '' }}">
<a class="page-link" href="{{ pagination.prev_url ?? '#' }}"
{% if pagination.prev_url is null %}tabindex="-1" aria-disabled="true"{% endif %}>
{{ pagination.prev_text }}
</a>
</li>
{% for page in pagination.pages %}
<li class="page-item{{ page.is_current ? ' active' : '' }}">
<a class="page-link" href="{{ page.url }}"
{% if page.is_current %}aria-current="page"{% endif %}>
{{ page.number }}
</a>
</li>
{% endfor %}
<li class="page-item{{ pagination.next_url is null ? ' disabled' : '' }}">
<a class="page-link" href="{{ pagination.next_url ?? '#' }}"
{% if pagination.next_url is null %}tabindex="-1" aria-disabled="true"{% endif %}>
{{ pagination.next_text }}
</a>
</li>
</ul>
</nav>
{% endif %}

View File

@@ -0,0 +1,22 @@
{% if post_navigation is defined and post_navigation|length > 0 %}
<nav class="border-top border-bottom py-4 my-4" aria-label="{{ __('Post navigation') }}">
<div class="row">
<div class="col-6">
{% if post_navigation.previous is defined %}
<small class="text-body-secondary d-block mb-1">{{ __('Previous') }}</small>
<a href="{{ post_navigation.previous.url }}" class="text-decoration-none">
&larr; {{ post_navigation.previous.title }}
</a>
{% endif %}
</div>
<div class="col-6 text-end">
{% if post_navigation.next is defined %}
<small class="text-body-secondary d-block mb-1">{{ __('Next') }}</small>
<a href="{{ post_navigation.next.url }}" class="text-decoration-none">
{{ post_navigation.next.title }} &rarr;
</a>
{% endif %}
</div>
</div>
</nav>
{% endif %}

View File

@@ -0,0 +1,11 @@
<form role="search" method="get" action="{{ site.url }}" class="mb-4">
<div class="input-group">
<input type="search" class="form-control" name="s"
placeholder="{{ __('Search...') }}"
value="{{ search_query is defined ? search_query : '' }}"
aria-label="{{ __('Search') }}">
<button class="btn btn-primary" type="submit">
{{ __('Search') }}
</button>
</div>
</form>

View File

@@ -0,0 +1,44 @@
<aside>
{% if sidebar.recent_posts is defined and sidebar.recent_posts|length > 0 %}
<div class="mb-4">
<h3 class="h6 text-uppercase fw-semibold" style="letter-spacing: 1.6px">
{{ __('Recent Posts') }}
</h3>
<ul class="list-unstyled">
{% for post in sidebar.recent_posts %}
<li class="mb-2">
<a href="{{ post.url }}" class="text-decoration-none">{{ post.title }}</a>
<br>
<small class="text-body-secondary">{{ post.date }}</small>
</li>
{% endfor %}
</ul>
</div>
<hr>
{% endif %}
<div class="mb-4">
<h3 class="h6 text-uppercase fw-semibold" style="letter-spacing: 1.6px">
{{ __('Search') }}
</h3>
{% include 'partials/search-form.html.twig' %}
</div>
<hr>
{% if sidebar.tags is defined and sidebar.tags|length > 0 %}
<div class="mb-4">
<h3 class="h6 text-uppercase fw-semibold" style="letter-spacing: 1.6px">
{{ __('Tags') }}
</h3>
<div>
{% for tag in sidebar.tags %}
<a href="{{ tag.url }}" class="badge bg-secondary text-decoration-none me-1 mb-1">
{{ tag.name }}
</a>
{% endfor %}
</div>
</div>
{% endif %}
</aside>