vinou / site-builder
PHP library to generate template based webpages with dynamic content from the Vinou Plattform
Requires
- php: >=8.2
- bramus/router: *
- erusev/parsedown: ^1.7
- gumlet/php-image-resize: ^1.9
- phpmailer/phpmailer: ^6.0
- s1syphos/php-simple-captcha: ^2.3
- symfony/yaml: ^6.0|^7.0
- thepixeldeveloper/sitemap: ^5.1
- twig/twig: ^3.0
- vinou/api-connector: ^3.0.0
- vinou/translations: 4.*
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- 4.6.1
- 4.6.0
- 4.5.0
- 4.4.0
- 4.3.2
- 4.3.1
- 4.3.0
- 4.2.1
- 4.2.0
- 4.1.9
- 4.1.8
- 4.1.7
- 4.1.6
- 4.1.5
- 4.1.4
- 4.1.3
- 4.1.2
- 4.1.1
- 4.1.0
- 4.0.1
- 3.1.5
- 3.1.4
- 3.1.3
- 3.1.2
- 3.1.1
- 3.1.0
- 3.0.6
- 3.0.5
- 3.0.4
- 3.0.3
- 3.0.2
- 3.0.1
- 3.0.0
- 2.9.11
- 2.9.10
- 2.9.9
- 2.9.8
- 2.9.7
- 2.9.6
- 2.9.5
- 2.9.4
- 2.9.3
- 2.9.2
- 2.9.1
- 2.9.0
- 2.8.4
- 2.8.3
- 2.8.2
- 2.8.1
- 2.8.0
- 2.7.2
- 2.7.1
- 2.7.0
- 2.6.5
- 2.6.4
- 2.6.3
- 2.6.2
- 2.6.1
- 2.6.0
- 2.5.2
- 2.5.1
- 2.5.0
- 2.4.3
- 2.4.2
- 2.4.1
- 2.4.0
- 2.3.3
- 2.3.2
- 2.3.1
- 2.3.0
- 2.2.1
- 2.2.0
- 2.1.1
- 2.1.0
- 2.0.5
- 2.0.4
- 2.0.3
- 2.0.2
- 2.0.1
- 2.0.0
- 1.3.5
- 1.3.4
- 1.3.3
- 1.3.2
- 1.3.1
- 1.3.0
- 1.2.12
- 1.2.11
- 1.2.10
- 1.2.9
- 1.2.8
- 1.2.7
- 1.2.6
- 1.2.5
- 1.2.4
- 1.2.3
- 1.2.2
- 1.2.1
- 1.2.0
- 1.1.1
- 1.1.0
- 1.0.9
- 1.0.8
- 1.0.7
- 1.0.6
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- dev-release/2.X
- dev-develop
- dev-optional_mail_delivery
- dev-optimized_captcha
- dev-+548_optimize_external_order_payment_methods
- dev-development
This package is auto-updated.
Last update: 2026-11-11 22:13:39 UTC
README
The Vinou Site-Builder is a PHP library that combines PHP routing configured in YAML files with Twig template rendering. It provides a data processing pipeline to call registered processors and pipe results directly into Twig templates.
Table of contents
- Typical project structure
- Installation
- Route configuration
- Settings
- Template override hierarchy
- Twig filters
- Structured data (JSON-LD)
- Classlist
- Provider
Typical project structure
| File | Description |
|---|---|
composer.json |
Composer configuration |
config/settings.yml |
SiteBuilder and ApiConnector settings |
config/routes.yml |
Project-specific routes |
config/mail.yml |
SMTP credentials and form definitions |
public/index.php |
Application entry point |
public/.htaccess |
Routes all requests to index.php |
public/Resources/Layouts/ |
Twig layout overrides |
public/Resources/Partials/ |
Twig partial overrides |
public/Resources/Templates/ |
Page templates |
public/Resources/Sass/ |
SCSS source files |
The complete example project is available in Examples/Project/.
Installation
composer require vinou/site-builder cp -R vendor/vinou/site-builder/Examples/Project/config ./ cp -R vendor/vinou/site-builder/Examples/Project/web ./
Required after installation:
- Set Vinou
authidandtokeninconfig/settings.yml— find them in Vinou Office - Set SMTP credentials in
config/mail.yml
Typical optional setup:
- Add project routes to
config/routes.yml - Configure Vinou constants in
public/index.php
Minimal public/index.php:
<?php require_once __DIR__ . '/../vendor/autoload.php'; define('VINOU_ROOT', realpath('./')); define('VINOU_MODE', 'Shop'); define('VINOU_CONFIG_DIR', '../config/'); $session = new \Vinou\ApiConnector\Session\Session(); $session::setValue('language', 'de'); $site = new \Vinou\SiteBuilder\Site(); $site->setRouteFile('routes.yml'); $site->loadTheme('my-theme', 'Theme/MyTheme/'); $site->run();
Route configuration
1. General route parameters
| Parameter | Default | Options | Description |
|---|---|---|---|
type |
page |
page |
Render a Twig template |
redirect |
Redirect to internal or external URL | ||
namespace |
Group sub-routes under a URL prefix | ||
sitemap |
Render a sitemap.xml | ||
method |
get |
get | post | all |
Accepted HTTP method |
template |
— | Pages/start.twig |
Twig template to render |
redirect |
— | /other/page or https://… |
Redirect target (requires type: redirect) |
pageTitle |
— | string |
Page title, available as {{ pageTitle }} in templates |
public |
true |
true | false |
false requires a Vinou client login |
sitemap |
false |
true | false | Array |
Include in sitemap; array for dynamic entry generation |
twig |
— | cache: false |
Override Twig settings for this route |
urlKeys |
— | [slug, id] |
Named aliases for URL wildcard segments |
dataProcessing |
— | Array | Data loading steps, results available in template |
postProcessing |
— | Array | Runs after dataProcessing, same syntax |
extend |
false |
true |
Deep-merge with parent route instead of replacing it |
excludeContent |
— | [key1, key2] |
Skip specific additionalContent items for this route |
2. Sitemap configuration for a route
Use sitemap as an array on routes with a URL wildcard to generate one sitemap entry per item:
weine/{path_segment}: template: 'Wines/Detail.twig' pageTitle: 'Wein-Details' public: true sitemap: function: getWinesAll params: lazy: false pageSize: 500 dataKey: 'wines' dataProcessing: wine: getWine
| Parameter | Description |
|---|---|
function |
API-Connector function to fetch the list of items |
params |
Parameters passed to the function |
params/lazy |
Load items in recurring pages (recommended for large datasets) |
params/pageSize |
Items per page when using lazy loading |
dataKey |
Key in the API response containing the items array |
3. Use dataProcessing
Each entry in dataProcessing calls a function in a processor and stores the result under the given key in the Twig context.
Shorthand — calls an API-Connector function directly:
dataProcessing: wines: getWinesAll wineries: getWineriesAll
Full syntax:
dataProcessing: wines: function: getWinesAll params: pageSize: 50 orderBy: topseller DESC cluster: - type - taste_id - vintage
Combining data with formatter/mergeData:
dataProcessing: wines: function: getWinesAll bundles: function: getBundlesAll items: processor: formatter function: mergeData useRouteData: false useData: - wines - bundles
mergeData flattens sub-arrays into one list and sets object_type per entry (e.g. wines, bundles).
Full parameter reference:
| Parameter | Example | Description |
|---|---|---|
processor |
formatter |
Registered processor identifier (default: Vinou API) |
class |
\Vendor\Ns\Processor |
Instantiate a class directly as processor |
function |
getWinesAll |
Function name to call on the processor |
params |
Array | Static parameters passed to the function |
useRouteData |
false |
Set to false to exclude URL wildcard segments from function arguments |
useData |
[wines, bundles] |
Keys already in renderArr to pass as input |
key |
wines |
Key to extract from the function's return value |
forceLoadAll |
true |
Use the complete return value instead of extracting by key |
loadOnlyFirst |
true |
Use only the first element of the result array |
postParams |
query,page |
Comma-separated POST field names to forward (whitelist) |
getParams |
query,page |
Comma-separated GET parameter names to forward (whitelist) |
stopProcessing |
true |
Abort remaining dataProcessing steps if result is empty |
4. Extend a parent route
Setting extend: true on a project route causes it to deep-merge (array_replace_recursive) with the existing route definition from the theme instead of replacing it entirely. Only the keys you define are overridden; all other settings are inherited.
# config/routes.yml weine: extend: true pageTitle: 'Unser Weinshop' # overrides theme value dataProcessing: text: # added on top of theme dataProcessing function: getText params: identifier: 'weine'
Without extend: true, the project route replaces the theme route completely.
5. Global content (additionalContent)
additionalContent is defined in settings.yml (typically in the theme) and runs on every page before route-specific dataProcessing. Results are available in all templates.
# Theme/MyTheme/Configuration/settings.yml additionalContent: categories: function: getCategoriesAll params: orderBy: sorting ASC client: getClient paymentMethods: function: getAvailablePayments key: payments
To skip specific items on a route, use excludeContent:
# config/routes.yml kontakt: template: 'Pages/kontakt.twig' excludeContent: [categories] dataProcessing: captcha: processor: mailer function: loadCaptcha
6. Registered processors
| Identifier | Class | Description |
|---|---|---|
| (default) | \Vinou\ApiConnector\Api |
All Vinou API calls (get*, search*, etc.) |
shop |
\Vinou\SiteBuilder\Processors\Shop |
Basket, billing, delivery, payments, campaigns |
mailer |
\Vinou\SiteBuilder\Processors\Mailer |
Form handling, captcha, email dispatch |
files |
\Vinou\SiteBuilder\Processors\Files |
Read local files with metadata |
external |
\Vinou\SiteBuilder\Processors\External |
Fetch external URLs / JSON |
sitemap |
\Vinou\SiteBuilder\Processors\Sitemap |
Sitemap XML generation |
formatter |
\Vinou\SiteBuilder\Processors\Formatter |
Merge and format loaded data |
7. Register your own processor
Create a processor class:
<?php namespace YourVendor\YourNamespace\Processors; class YourProcessor extends \Vinou\SiteBuilder\Processors\AbstractProcessor { public function dataMagic($data = null) { // transform data here return $data; } }
Register it in index.php before calling $site->run():
$site = new \Vinou\SiteBuilder\Site(); $site->render->loadProcessor('myprocessor', new \YourVendor\YourNamespace\Processors\YourProcessor()); $site->run();
Use it in a route:
my-route: template: 'Pages/template.twig' dataProcessing: result: processor: myprocessor function: dataMagic params: foo: bar
Settings
Settings are loaded from YAML files and merged in this order (later wins):
- Theme:
YourTheme/Configuration/settings.yml - Project:
config/settings.yml
The theme controls whether SiteBuilder built-in defaults are loaded:
system: load: defaultRoutes: false # disable SiteBuilder's bundled routes defaultSettings: false # disable SiteBuilder's bundled settings
Global settings reference (settings key):
| Key | Description |
|---|---|
defaults.payment |
Default payment method |
allowedPayments |
Comma-separated list of accepted payment methods |
minBasketSize |
Minimum number of bottles required to check out |
packageSteps |
Allowed basket sizes as array (e.g. [6,12,18]) |
maxItemQuantity |
Maximum quantity per basket item |
enableClickAndCollect |
Enable click & collect option |
useStockDistribution |
Distribute stock across positions |
deliveryCountries |
Array of ISO country codes for delivery |
defaultCountry |
Pre-selected country code |
speechStyle |
formal or informal — controls salutation in templates |
seo.titleAdd |
String appended to every page title |
seo.logo |
Logo for the structured data (structuredData('site')), webroot-relative or absolute |
permissionRedirect |
URL to redirect unauthenticated users to |
pages.* |
Internal URLs for basket, checkout, login, etc. |
images.* |
Image dimensions for list and detail views |
geoBlocking.* |
Block shop paths by visitor country, see below |
Geo blocking (settings.geoBlocking):
settings: geoBlocking: allowedCountries: [de] # allow list: every other known country is blocked countries: [] # block list, e.g. [dk] (wins if a code is in both lists) paths: [weine, weinpakete, produkte, kategorie, suche, warenkorb, checkout, client] # '*' = whole site exceptions: [impressum, datenschutz, agb, widerruf, system] # always reachable countryHeader: ~ # optional country header of the host/CDN, e.g. HTTP_CF_IPCOUNTRY
Both lists empty = off. Blocked pages answer with HTTP 451 (GeoBlocked.twig), adding items to the basket via
ajax with 403. The country comes from countryHeader or the local DB-IP database in config/geoip/
(vendor/bin/vinou geoip:update, e.g. in the CI build). Fail-open: without database, for private IPs or unknown
countries nothing is blocked.
Template override hierarchy
SiteBuilder uses Twig's FilesystemLoader with an ordered list of directories. The first matching template wins.
Default search order:
public/Resources/Templates/— projectpublic/Resources/Partials/— projectpublic/Resources/Layouts/— projectTheme/Resources/Layouts/— themeTheme/Resources/Partials/— themeTheme/Resources/Templates/— theme
A template in public/Resources/Templates/Wines/List.twig will silently override Theme/Resources/Templates/Wines/List.twig.
The same priority applies to route loading:
- SiteBuilder built-in routes (lowest priority, loaded as base)
- Theme routes (override SiteBuilder)
- Project
config/routes.yml(highest priority)
Twig filters
All filters are registered by \Vinou\SiteBuilder\Tools\Render.
| Filter | Signature | Description |
|---|---|---|
image |
src|image(chstamp, dimension) |
Download and cache API image; optionally resize |
pdf |
src|pdf(chstamp) |
Download and cache API PDF |
src |
path|src |
Append file modification timestamp for cache busting |
region |
id|region |
Resolve wine region ID to name |
taste |
id|taste |
Resolve taste ID to label |
grapetypes |
array|grapetypes |
Resolve grape type IDs to name map |
link |
label|link(url, params) |
Render an <a> tag; adds active class on current URL |
language |
value|language(translations, key, current) |
Render language switch link |
currency |
decimal|currency |
Format number as price: 1234.5 → 1.234,50 |
netto |
decimal|netto |
Convert gross to net (÷1.19) |
basePrice |
price|basePrice(unit) |
Format base price with unit label |
price |
items|price |
Sum price × quantity across a basket items array |
nl2p |
string|nl2p |
Convert newlines to <p> tags |
pageTitle |
object|pageTitle |
Build a page title from name, articlenumber, vintage |
groupBy |
array|groupBy(key) |
Group array by a property value |
sortBy |
array|sortBy(prop, dir) |
Sort array by property (ASC|DESC) |
ksort |
array|ksort |
Sort array by key |
withAttribute |
array|withAttribute(attr, val) |
Filter: keep entries where attr == val |
withoutAttribute |
array|withoutAttribute(attr, val) |
Filter: exclude entries where attr == val |
subArray |
array|subArray(index) |
Extract one property from each entry |
addProperty |
array|addProperty(key, val) |
Add a fixed property to every entry |
sum |
array|sum |
Sum an array of numbers |
wines |
items|wines |
Filter basket items to type wine |
packages |
items|packages |
Filter basket items to type package |
getBundle |
id|getBundle |
Fetch a single bundle from the API |
getWinery |
id|getWinery |
Fetch a single winery from the API |
quantityIsAllowed |
qty|quantityIsAllowed |
Check if quantity meets basket rules |
cast_to_array |
obj|cast_to_array |
Cast stdClass to associative array |
arraytocsv |
array|arraytocsv |
Join array to comma-separated string |
filesize |
file|filesize |
Human-readable file size (KB, MB, …) |
bytes |
bytes|bytes(precision) |
Format raw byte count |
cleanup |
string|cleanup |
Strip leading space, commas, @, quotes |
http |
url|http |
Prepend http:// if no protocol present |
base64image |
url|base64image |
Encode image as base64 data URI |
Structured data (JSON-LD)
The Twig function structuredData() renders schema.org data as a <script type="application/ld+json"> tag. The data is
built in \Vinou\SiteBuilder\Tools\StructuredData; the theme only decides where it is placed (the Vinou shop theme
uses Partials/General/Head.twig).
| Call | Output |
|---|---|
{{ structuredData('site') }} |
Winery + WebSite (name from customer.company, logo from settings.seo.logo) |
{{ structuredData('product', wine) }} |
Product + Offer for a wine, bundle or product: name, url, description (meta_description, else description without HTML), image, sku, gtin, brand, category, price (client price when logged in), availability |
Request values (domain, path, client, customer) come from the template context. Merchants (customer.type == 'merchant') use the winery as brand. Texts are encoded with JSON_HEX_TAG, so HTML from the API cannot close the tag.
Optional setting:
settings: seo: logo: /Assets/Images/logo.png # webroot-relative or absolute URL
Classlist
| Class | Description |
|---|---|
\Vinou\SiteBuilder\Site |
Main class; combines router, renderer, and settings loader |
\Vinou\SiteBuilder\Loader\Settings |
Collects and merges settings YAML files |
\Vinou\SiteBuilder\Tools\Render |
Initialises Twig, registers filters, executes dataProcessing |
\Vinou\SiteBuilder\Tools\StructuredData |
Builds schema.org JSON-LD (Product/Offer, Winery/WebSite) for the structuredData() Twig function |
\Vinou\SiteBuilder\Tools\BasketCookie |
Keeps the basket UUID in the HttpOnly cookie vinou_basket (30 days) and restores it into a new session; removed after an order |
\Vinou\SiteBuilder\Router\DynamicRoutes |
Reads YAML route files and configures the Bramus router |
\Vinou\SiteBuilder\Processors\AbstractProcessor |
Base class for custom processors; provides loadApi() |
\Vinou\SiteBuilder\Processors\Shop |
Shop-specific processing (basket, checkout, campaigns) |
\Vinou\SiteBuilder\Processors\Mailer |
Form dispatch and captcha |
\Vinou\SiteBuilder\Processors\Formatter |
Data combination utilities (mergeData) |
\Vinou\SiteBuilder\Processors\Files |
Local file reading |
\Vinou\SiteBuilder\Processors\External |
External HTTP requests |
\Vinou\SiteBuilder\Processors\Sitemap |
Sitemap XML generation |
Admin panel
The SiteBuilder ships with a built-in admin panel at /system. It is activated automatically when the project settings contain admin credentials and provides a HTMX-driven single-page interface for managing settings, users, redirects, routes, mail configuration, and system state.
Activation
Site::loadAdminPanel() registers the panel routes when either of the following keys is present in config/settings.yml:
# Option A — legacy single-password (useful for initial bootstrap) system: password: 'my-password' # Option B — multi-user (production) system: users: - email: admin@example.com password: '$2y$10$…' # bcrypt hash, auto-generated on first save via the panel
Both keys can coexist temporarily during the bootstrap process. Once system.users is populated, system.password can be removed.
Login flow
Single-password mode (no system.users):
- Enter password.
- If system-level TOTP is configured (
system.totp_secrets): redirect to/system/totp. - Otherwise: panel access granted.
Multi-user mode (system.users present):
- Enter email + password.
- Account has no
totp_secrets→ redirected to/system/totp/first-setup(forced; cannot be skipped). - Account has
totp_secrets→ redirected to/system/totp(TOTP verification). - Correct TOTP code → panel access granted.
Session keys used during the flow:
| Key | Value | Purpose |
|---|---|---|
vinou_admin_auth |
true |
Full authentication complete |
vinou_admin_pw_ok |
true |
Password verified; awaiting TOTP |
vinou_admin_user_email |
email string | Identifies which user is logging in |
admin_csrf_token |
hex string | Per-session CSRF token |
admin_login_attempts |
array | Brute-force counter and lockout timestamp |
Security
| Feature | Implementation |
|---|---|
| Password hashing | password_hash(PASSWORD_BCRYPT) — plaintext passwords in settings.yml are auto-upgraded to bcrypt on first successful login |
| Brute-force protection | 5 failed attempts (password or TOTP) → 15-minute session lockout |
| CSRF | Per-session token via random_bytes(16); HTMX injects it as X-CSRF-Token header via htmx:configRequest; login forms use a hidden _csrf_token field |
| TOTP 2FA | RFC 6238 native implementation — no external library; base32 decode, HMAC-SHA1, dynamic truncation, ±1 window for clock drift; multiple apps per user |
Bootstrap workflow for new projects
- Add
system.password: 'bootstrap'toconfig/settings.ymlto activate the panel. - Log in (single-password mode, no email field shown).
- Open the Benutzer tab → create the first user (email + password).
- Log out, log in as the new user.
- The forced first-time 2FA screen appears — scan the QR code and enter the confirmation code.
- Delete
system.passwordfromconfig/settings.yml. The panel stays active becausesystem.usersis now populated.
URL structure
| URL | Handler | Description |
|---|---|---|
GET/POST /system |
Admin::init() |
Login form or redirect to settings |
GET /system?action=logout |
Admin::init() |
Destroy session |
POST /system/totp |
Admin::verifyTotpStep() |
TOTP code verification |
GET/POST /system/totp/first-setup |
Admin::firstTotpSetup() |
Forced initial 2FA registration |
POST /system/totp/setup |
Admin::setupTotp() |
Add a 2FA app to the current user |
POST /system/totp/disable |
Admin::disableTotp() |
Remove a 2FA app from the current user |
GET /system/settings |
Admin::page() |
Settings editor |
GET /system/users |
Admin::page() |
User management + own 2FA apps |
GET /system/redirects |
Admin::page() |
Redirect management |
GET /system/routes |
Admin::page() |
Route overview |
GET /system/mail |
Admin::page() |
Mail configuration |
GET /system/system |
Admin::page() |
Cache management + PHP environment + Vinou packages |
POST /system/users/save |
Admin::saveUser() |
Create user |
POST /system/users/update |
Admin::updateUser() |
Update user email / password |
POST /system/users/delete |
Admin::deleteUser() |
Delete user |
POST /system/settings/save |
Admin::saveSetting() |
Save a setting value |
POST /system/settings/delete |
Admin::deleteSetting() |
Delete a setting key |
POST /system/settings/add |
Admin::addSetting() |
Append to a settings array |
POST /system/redirects/save |
Admin::saveRedirect() |
Save a redirect rule |
POST /system/redirects/delete |
Admin::deleteRedirect() |
Delete a redirect rule |
GET /system/assets/{file} |
PHP closure | Static assets (CSS, JS) |
Panel tabs
| Tab | URL | Content |
|---|---|---|
| Settings | /system/settings |
Edit whitelisted settings keys (additionalContent, settings, vinou) |
| Benutzer | /system/users |
User list with edit/delete + own TOTP app management |
| Redirects | /system/redirects |
HTTP redirect rules from config/redirects.yml |
| Routes | /system/routes |
Loaded route configuration |
/system/mail |
SMTP config + form definitions from config/mail.yml |
|
| System | /system/system |
Cache clearing + PHP/extension checks + installed Vinou packages |
Settings whitelist
The Settings panel only displays and allows editing of keys listed in SETTINGS_WHITELIST:
private const SETTINGS_WHITELIST = ['additionalContent', 'settings', 'vinou'];
All other top-level keys (system, shop, etc.) are never exposed or writable through the panel UI.
User data structure
system: users: - email: admin@example.com password: '$2y$10$…' # bcrypt totp_secrets: - name: 'My iPhone' secret: 'ABCDEFGHIJKLMNOP' # base32-encoded TOTP secret - name: 'Backup key' secret: 'QRSTUVWXYZ234567'
Each user manages their own totp_secrets. The system-level totp_secrets / totp_secret keys are only used in legacy single-password mode.
Template structure
Admin/Panel.twig Shell: <head> with CSS, HTMX, qrcodejs; navigation; #section-content
Admin/Sections/
Settings.twig Settings editor (section.settings, section.projectPaths)
Users.twig User list + edit forms + own TOTP management
(section.users, section.currentEmail, section.totpApps, …)
Redirects.twig Redirect CRUD (section.redirects)
Routes.twig Route tree (section.routes)
Mail.twig SMTP + form config (section.mailConfig)
System.twig Cache cards + PHP checks + Vinou packages
(section.environment, section.vinouPackages)
qrcodejs is loaded once in Panel.twig's <head> to avoid HTMX script-loading race conditions.
Static assets
CSS (admin.css) and JS (htmx.min.js) are served via a PHP closure from vendor/vinou/site-builder/Resources/Public/. SCSS source: Resources/Sass/Admin/panel.scss.
Provider
This library is developed by Vinou GmbH.
Vinou GmbH
Justus-von-Liebig-Straße 9e
55232 Alzey
E-Mail: kontakt@vinou.de
Phone: +49 6131 6245390