Proposal: Winter.Head — wrap laravel/head for centralised document head management
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 1.5k
- Forks
- 246
- Avg merge
- 19h 2m
- Merged PRs (30d)
- 7
Description
Summary
Wrap the first-party laravel/head package as a Winter plugin (Winter.Head) to give the platform one fluent, resolvable API for everything in <head>, plus a {% head %} Twig tag for themes.
Head management in Winter is currently spread across several mechanisms that don't compose, and the seams produce real bugs.
Why
backend.layout.extendHead concatenates, so contributions collide
EventEmitter::fireViewEvent() implodes every listener's return value:
// modules/system/traits/EventEmitter.php:86
if ($result = Event::fire($event, $params)) {
return implode(PHP_EOL.PHP_EOL, (array) $result);
}
Two plugins that each contribute a favicon set therefore leave both in the document, and the browser arbitrates. Listener priority only reorders them — it doesn't make one authoritative. I hit this with two plugins on a multi-domain install, and the only clean fix was to make one plugin the sole listener and have the other override a config value it reads. That works, but it's a pattern every plugin author has to reinvent, and it only holds while exactly one plugin is willing to own the tag.
There's no way for a plugin to say "replace the favicon set" rather than "append another one". A resolver with precedence layers solves this class of problem outright.
Winter.SEO renders og: tags with the wrong attribute
plugins/winter/seo/components/seotags/default.htm renders a scalar value as:
<meta name="{{ tagName }}" content="{{ tagContent }}">
So the documented Meta::set('og:image', $url) emits name="og:image". The Open Graph protocol specifies property, and Facebook, LinkedIn and Slack read property. Getting a conformant tag today requires knowing to pass an array so the other branch of that template fires — which is easy to miss and silently produces cards that don't render.
laravel/head handles exactly this, switching attribute by key and letting it be forced:
Head::meta('description', 'About') // name="description"
->meta('og:title', 'About'); // property="og:title"
Themes hand-roll the rest
Every theme ends up with its own partials/meta/seo.htm assembling titles, canonicals, OG tags, favicons and JSON-LD by hand, with no shared notion of precedence between site defaults, page values and runtime overrides.
Proposal
Winter.Head wrapping laravel/head, providing:
-
A
{% head %}Twig tag — the CMS equivalent of the@headBlade directive, rendering the resolved tags in a theme layout:<head> <meta charset="utf-8"> {% head %} </head> -
A
Headfacade usable from plugins, components and page PHP sections, with the package's five-layer precedence (page defaults → route group → route → runtime → error). That model maps cleanly onto Winter: pluginboot()sets defaults, a CMS page'sonStart()or a component sets runtime values, and the later layer wins field by field rather than wholesale. -
Backend head management on the same API, replacing
backend.layout.extendHeadfor tag contributions. Favicons, theme colour, application name and manifest all have first-class methods (favicon(),appleTouchIcon(),manifest(),themeColor(),pwa()), so a plugin overriding an icon set replaces it rather than appending a competing one.
First-party plugin integrations
- Winter.SEO — the biggest one. Its
Meta/Linkclasses overlap almost entirely withHeadBuilder. IdeallyWinter.SEObecomes a thin compatibility layer overWinter.Head, keepingMeta::set()/Link::set()working while fixing thepropertyvsnameissue underneath. ItsSeoableModelbehaviour maps onto route/runtime metadata. - Winter.Pages / Winter.Blog — per-page and per-post metadata (
meta_title,meta_description,meta_image) resolved as a layer instead of each theme wiring them manually.Schema::blogPosting()andSchema::article()give posts JSON-LD for free. - Winter.Sitemap —
feed()for RSS/Atom discovery andalternates()for locale variants, which today are hand-written in theme partials. - Winter.Translate —
alternates()forhreflang, driven by the locale set rather than hand-maintained. - Winter.Redirect —
canonical()so a redirect target's canonical URL is consistent with the rule set.
Blocker: framework version
laravel/head v0.2.2 requires:
"php": "^8.3",
"illuminate/contracts": "^13.17.0",
"illuminate/routing": "^13.17.0",
"illuminate/support": "^13.17.0",
"illuminate/view": "^13.17.0"
Winter currently requires laravel/framework ^12.30.1 (winter/storm), so this can't be adopted until Winter moves to Laravel 13. Worth noting it's also pre-1.0, so the API may still move.
That makes this a design-direction issue rather than something immediately actionable. Two things could still happen ahead of the framework bump:
- Fix the
og:attribute bug inWinter.SEOdirectly — it's a real, user-visible bug today and shouldn't wait on this. - Decide whether backend head contributions should move to a resolver-style API regardless, since the concatenation problem exists independently of which package backs it.
References
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing modules/system/traits/EventEmitter.php:86 and plugins/winter/seo/components/seotags/default.htm, then compare Winter's Laravel 12.30.1 constraint with laravel/head v0.2.2 requirements. Done would require an agreed implementation direction for the proposed Winter.Head plugin, its integrations, and the framework-version blocker; the issue does not define a bounded patch or tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- laravel, php
- Domain
- backend, web-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100