c975l / site-bundle
Groups common files and settings to create a website
Package info
Language:Twig
Type:symfony-bundle
pkg:composer/c975l/site-bundle
This package is auto-updated.
Last update: 2026-06-27 18:34:36 UTC
README
Symfony bundle that provides a complete foundation for building websites — layout, pages, SEO, admin, sitemap, legal templates, and more.
Features
- Base layout with SEO-optimized meta tags (OpenGraph, robots, canonical, favicon, Apple touch icon)
- Page display from Twig templates (file-based) or from the database via the
Pageentity - Page redirects and 410 Gone handling
- Admin CRUD for database pages via EasyAdmin
- Sitemap generation from both filesystem templates and database pages
- Error page templates for 401, 403, 404, 410, and 500
- Legal model templates for France (French): cookies, copyright, legal notice, privacy policy, terms of sales, terms of use
- Matomo analytics integration
- CookieConsent integration
- Alternate language hreflang meta tags
- Open Graph image support
- Email templates with CSS inlining
- Asset serving controller (inline display, access-protected)
- File download controller (forced download, access-protected)
- Twig extensions:
route_exists,template_exists,asset_exists,nl2br - CSS animations stylesheet
- File lists:
extensions.txtandbots.txt
Requirements
- PHP >= 8.1
- c975L/ConfigBundle
- c975L/UiBundle
- Doctrine ORM
- EasyAdmin
- symfony/ux-twig-component
- twig/cssinliner-extra
Installation
Download
composer require c975l/site-bundle
Load configuration values
This bundle uses c975L/ConfigBundle to manage its settings. Load the default configuration keys into the database:
php bin/console c975l:config:load-all
Then open the ConfigBundle dashboard to set values for the keys
Enable routes
Add the bundle routes to config/routes.yaml:
c975_l_site: resource: "@c975LSiteBundle/Controller/" type: attribute prefix: / # For multilingual websites: # prefix: /{_locale} # defaults: # _locale: '%locale%' # requirements: # _locale: en|fr|es
Install assets
php bin/console assets:install --symlink
Register Stimulus controllers
This bundle ships Stimulus controllers (basic, matomo, cookieConsent). They are exposed via AssetMapper under the @c975l/site-bundle namespace.
Add one entry to importmap.php (one-time, at installation):
'@c975l/site-bundle/controllers.js' => [ 'path' => './vendor/c975l/site-bundle/assets/controllers.js', ],
Add two lines to assets/bootstrap.js (or assets/stimulus_bootstrap.js):
import { startStimulusApp } from '@symfony/stimulus-bundle'; import { register as registerc975lSite } from '@c975l/site-bundle/controllers.js'; const app = startStimulusApp(); registerc975lSite(app);
After that, all controllers are loaded with hashed filenames (cache busting). Adding or removing controllers in a future bundle update requires no change in your app.
Usage
Creating your layout
Create templates/layout.html.twig in your project and extend the bundle's layout:
{% extends '@c975LSite/layout.html.twig' %}
Page-specific variables
Declare these variables in each page template to populate meta tags and the page title:
{% set title = 'My Page Title' %}
{% set description = 'A short description of this page.' %}
Template blocks
The layout exposes the following Twig blocks for you to override or extend:
| Block | Description |
|---|---|
head |
Entire <head> element |
meta |
Meta tags (charset, viewport, robots, og:*, etc.) |
stylesheets |
CSS links |
preconnect |
<link rel="preconnect"> hints |
body |
Entire <body> element |
header |
Site header |
navigation |
Main navigation |
main |
Main content wrapper |
title |
Page <h1> title |
flashes |
Flash messages |
container |
Container div wrapping content |
content |
Page-specific content |
share |
Sharing widgets |
navigationBottom |
Bottom navigation |
footer |
Site footer |
javascripts |
JavaScript includes |
Override a block:
{% block share %}
{{ parent() }}
{# your additional content #}
{% endblock %}
Disable a block:
{% block share %}{% endblock %}
Display mode
Use the display variable to conditionally include templates (defaults to html):
{% if display == 'pdf' %}
{% include 'header-pdf.html.twig' %}
{% else %}
{% include 'header.html.twig' %}
{% endif %}
Pages
File-based pages
Place Twig templates in templates/pages/. They are served at /pages/{slug} via the page_display route.
To hint the sitemap generator, add metadata in a Twig comment at the top of the file:
{# changeFrequency="monthly" priority="8" #}
Redirects and deleted pages
| Location | Effect |
|---|---|
templates/pages/redirected/{slug}.html.twig |
Redirects to the slug written inside the file |
templates/pages/deleted/{slug}.html.twig |
Throws a 410 Gone exception |
Database pages
Use the Page entity to manage pages through the database. Each page supports:
- Title, slug (unique), description
- Published status and display position
- Sitemap fields: change frequency and priority (0–10)
- Blocks (content blocks from c975L/UiBundle)
- Creation / modification timestamps and author reference
Database pages are rendered with the bundle's @c975LSite/pages/page.html.twig template, which displays the page title, description, and its associated blocks.
Admin management
Pages are managed in the EasyAdmin dashboard via PageCrudController. The menu entry is registered automatically through MenuProvider. Access is controlled by the site-role-needed key in ConfigBundle.
SEO
Sitemap generation
Run the following command to generate public/sitemap-pages.xml:
php bin/console site:sitemaps:create
The command aggregates URLs from:
- Twig files in
templates/pages/(readschangeFrequencyandpriorityfrom comments) - Published database pages (uses their
changeFrequencyandpriorityfields)
A sitemap index template is also available at @c975LSite/sitemap-index.xml.twig.
Alternate languages (hreflang)
Define languagesAlt to add <link rel="alternate" hreflang="..."> tags and enable a language switcher navbar component:
{% set languagesAlt = {
en: { title: 'English' },
fr: { title: 'Français' },
es: { title: 'Español' }
} %}
URLs are built as https://example.com/{locale}/pages/{slug}.
Open Graph image
Set a per-page OG image:
{% set ogImage = absolute_url(asset('images/my-og-image.jpg')) %}
General components
All components below read their data from ConfigBundle. No props are needed — just include the tag and set the corresponding keys via the ConfigBundle dashboard.
Matomo
Set site-matomo-url and site-matomo-id in ConfigBundle, then place the component wherever you want the tracking snippet (typically just before </body>):
<twig:c975LSite:General:Matomo/>
The component renders nothing if either config value is missing.
CookieConsent
Set url-cookies-policy in ConfigBundle (optional — links the banner to your cookies page), then place the component in your layout:
<twig:c975LSite:General:CookieConsent/>
The message, dismiss, and link texts are loaded from the site translation domain.
HostedBy / MadeBy
Set site-hosted-by-url + site-hosted-by-logo and/or site-made-by-url + site-made-by-logo in ConfigBundle, then include the components (typically in the footer):
<twig:c975LSite:General:HostedBy/> <twig:c975LSite:General:MadeBy/>
Each component renders nothing if either its URL or logo config value is missing.
Error templates
Pre-built error templates are available for: error, error401, error403, error404, error410, and error500.
Follow the Symfony guide on customizing error pages, then include the bundle templates in your own error files:
{% extends 'layout.html.twig' %}
{% block content %}
{% include '@c975LSite/Exception/error404.html.twig' %}
{% endblock %}
{% block share %}{% endblock %}
Legal models
Pre-built legal templates are available for France in French (fr). Available models:
| Model | Path |
|---|---|
| Cookies policy | @c975LSite/models/france/fr/cookies.html.twig |
| Copyright | @c975LSite/models/france/fr/copyright.html.twig |
| Legal notice | @c975LSite/models/france/fr/legal-notice.html.twig |
| Privacy policy | @c975LSite/models/france/fr/privacy-policy.html.twig |
| Terms of sales | @c975LSite/models/france/fr/terms-of-sales.html.twig |
| Terms of use | @c975LSite/models/france/fr/terms-of-use.html.twig |
Each model is also available in Markdown format (.md).
Feel free to contribute translations or add templates for other countries.
Include the whole model
{% extends 'layout.html.twig' %}
{% trans_default_domain 'site' %}
{% set title = 'label.terms_of_sales'|trans %}
{% block content %}
{% set latestUpdate = '2024-01-01' %}
{% include '@c975LSite/models/france/fr/terms-of-sales.html.twig' %}
{% endblock %}
Select specific blocks (embed)
{% extends 'layout.html.twig' %}
{% trans_default_domain 'site' %}
{% set title = 'label.terms_of_sales'|trans %}
{% block content %}
{% set latestUpdate = '2024-01-01' %}
{% embed '@c975LSite/models/france/fr/terms-of-sales.html.twig' %}
{# Disable a block #}
{% block acceptation %}{% endblock %}
{# Or extend a block #}
{% block acceptation %}
{{ parent() }}
Additional content here.
{% endblock %}
{% endembed %}
{% endblock %}
Asset and Download controllers
AssetController
Serves a file inline (e.g., images, PDFs). Useful for serving files only to authenticated users.
{{ path('asset_file', { file: 'path/to/your_file.pdf' }) }}
To restrict access, add an entry to config/packages/security.yaml:
access_control: - { path: ^/asset/protected/, roles: ROLE_USER }
DownloadController
Forces a file download.
{{ path('download_file', { file: 'path/to/your_file.csv' }) }}
File names may contain letters (including accented), digits, -, _, /, and up to two extensions. Spaces are not allowed.
Twig extensions
| Function / Filter | Description |
|---|---|
route_exists('route_name') |
Returns true if the named route exists |
template_exists('template.html.twig') |
Returns true if the template file exists |
asset_exists('path/to/file') |
Returns true if the asset exists in public/ or assets/ |
|nl2br |
Applies PHP's nl2br() with HTML output safe |
Email templates
Pre-built email templates are available at @c975LSite/emails/:
| Template | Description |
|---|---|
layout.html.twig |
Base email layout |
fullLayout.html.twig |
Full email layout |
footer.html.twig |
Email footer |
CSS is inlined automatically via twig/cssinliner-extra. Minified stylesheets (emails.min.css, styles.min.css, animations.min.css) are embedded.
CSS animations
Link the animations stylesheet to use scroll-triggered CSS animations:
<link rel="stylesheet" href="{{ asset('bundles/c975lsite/css/animations.min.css') }}">
Commands
| Command | Description |
|---|---|
php bin/console site:sitemaps:create |
Generates public/sitemap-pages.xml from filesystem and database pages |
php bin/console site:backup |
Backs up the database and public/ files (replaces BackupServer.sh) |
php bin/console models:twig2md |
Converts Twig model templates to their Markdown equivalent |
Scheduler
The bundle provides site:sitemaps:create and site:backup as schedulable commands. The schedule itself is defined in your app so each project controls its own timing.
1. Create the schedule class
// src/Scheduler/SiteSchedule.php namespace App\Scheduler; use Symfony\Component\Console\Messenger\RunCommandMessage; use Symfony\Component\Scheduler\Attribute\AsSchedule; use Symfony\Component\Scheduler\RecurringMessage; use Symfony\Component\Scheduler\Schedule; use Symfony\Component\Scheduler\ScheduleProviderInterface; use Symfony\Contracts\Cache\CacheInterface; #[AsSchedule('site')] class MaintenanceSchedule implements ScheduleProviderInterface { public function __construct( private readonly CacheInterface $cache, ) {} public function getSchedule(): Schedule { return (new Schedule()) ->stateful($this->cache) // Sitemap: daily at 00:05 ->add(RecurringMessage::cron('5 0 * * *', new RunCommandMessage('site:sitemaps:create'))) // Partial backup: every 6 hours (DB regular tables + modified files only) ->add(RecurringMessage::cron('7 */6 * * *', new RunCommandMessage('site:backup'))) // Full backup + report: every Monday at 03:07 (archive tables + whole DB + all user files) ->add(RecurringMessage::cron('7 3 * * 1', new RunCommandMessage('site:backup --full --report'))); } }
The stateful() call persists the last-run time via Symfony Cache so tasks are not re-run if the worker restarts.
2. Start the worker
Run the consumer as a long-lived process (supervised by Supervisor or systemd):
php bin/console messenger:consume scheduler_site
You may keep a cron entry that restarts the worker daily (e.g., at 00:25) to recover from crashes without monitoring the process continuously:
25 0 * * * systemctl --user start messenger-worker@your-site.service
Lists
Two plain-text lists are available for validation purposes:
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; $extensions = file( $this->parameterBag->get('kernel.project_dir') . '/../vendor/c975l/site-bundle/Lists/extensions.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); $bots = file( $this->parameterBag->get('kernel.project_dir') . '/../vendor/c975l/site-bundle/Lists/bots.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
Full layout example
{% extends '@c975LSite/layout.html.twig' %}
{% set languagesAlt = {
en: { title: 'English' },
fr: { title: 'Français' },
es: { title: 'Español' }
} %}
{% block meta %}
{{ parent() }}
<meta property="fb:app_id" content="YOUR_FACEBOOK_APP_ID">
{% endblock %}
{% block stylesheets %}
{{ parent() }}
{% endblock %}
{% block navigation %}
{{ include('navbar.html.twig') }}
{% endblock %}
{% block title %}
{% if app.request.get('_route') is not null %}
<h1>{{ title }}</h1>
{% endif %}
{% endblock %}
{% block container %}
<div class="container">
{% block content %}{% endblock %}
</div>
{% endblock %}
{% block share %}
{# your sharing widget #}
{% endblock %}
{% block footer %}
{{ include('footer.html.twig') }}
<twig:c975LSite:General:HostedBy/>
<twig:c975LSite:General:MadeBy/>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<twig:c975LSite:General:CookieConsent/>
<twig:c975LSite:General:Matomo/>
{% endblock %}
If this project helps you save development time, consider sponsoring via the Sponsor button at the top of the GitHub page. Thank you!