wintercms / wintercms/winter

Proposal: Winter.Head — wrap laravel/head for centralised document head management

Open
#1,537 1 comment 1 reaction 0 assignees View on GitHub

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:

  1. A {% head %} Twig tag — the CMS equivalent of the @head Blade directive, rendering the resolved tags in a theme layout:

    <head>
        <meta charset="utf-8">
        {% head %}
    </head>
    
  2. A Head facade 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: plugin boot() sets defaults, a CMS page's onStart() or a component sets runtime values, and the later layer wins field by field rather than wholesale.

  3. Backend head management on the same API, replacing backend.layout.extendHead for 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 / Link classes overlap almost entirely with HeadBuilder. Ideally Winter.SEO becomes a thin compatibility layer over Winter.Head, keeping Meta::set() / Link::set() working while fixing the property vs name issue underneath. Its SeoableModel behaviour 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() and Schema::article() give posts JSON-LD for free.
  • Winter.Sitemapfeed() for RSS/Atom discovery and alternates() for locale variants, which today are hand-written in theme partials.
  • Winter.Translatealternates() for hreflang, driven by the locale set rather than hand-maintained.
  • Winter.Redirectcanonical() 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 in Winter.SEO directly — 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.