Skip to content

v2.1.0

Choose a tag to compare

@github-actions github-actions released this 01 Sep 14:51
· 7 commits to main since this release

What's Changed

Added

  • ViteManifest resolves a content-hashed JS entry through .vite/manifest.json. A library opts in with vite_entry (the manifest key, or true for the default src/js/script.js) and keeps declaring its real dist path; hook_library_info_alter() swaps in the hashed filename when a usable manifest sits beside it. Ported from StarterBase::themeScriptFile() in parisek/timber-kit.

    Why the entry needs a hash: lazy chunks always carried one, the entry did not, because *.libraries.yml names it by a fixed path and cache-busting came from Drupal's ?v= instead. That covers the reference in the HTML but not the one the bundler emits inside a chunk — a module reachable from the entry graph and from a lazy chunk is hoisted into the entry, and the chunk imports it back as ./script.js, unhashed and unqueried. Measured on the WordPress sibling (sloneek, 2026-08-17): 5 of 52 chunks imported the entry, max-age was 31536000, and a form silently stopped rendering with does not provide an export named 'n' — minified export names are positions in a table, so a stale entry can also answer with the wrong binding and no error.

    This closes the correctness defect, not the double fetch. JsCollectionRenderer appends a query to every unaggregated asset unconditionally, so the tag's URL and a chunk's own import remain two module identities — now with identical content. See ADR 0002, which also records why drupal/vite was not taken.

    vite_entry also accepts a map of asset path to manifest key, which a library declaring more than one JS asset needs; a bare key covering several assets is refused with a logged warning instead of rewriting one of them.

    Rewrites keep their declared position (Drupal emits a library's JS in array order, and an unset-and-append moved the rewritten asset last), and a map whose entries resolve to one built filename is refused whole rather than dropping an asset.

    Two constraints worth stating: only the asset whose filename matches the key's is rewritten (the property names one entry, and applying the key to every JS file made the rewrites overwrite each other), and the resolved name is cached with Drupal's library info, so a deploy shipping new assets must run drush cr.

    Backwards compatible by construction: no vite_entry, or no manifest, and the declared path is served unchanged. Four guards on the manifest value — resolvable inside the built directory, free of URL-significant characters, .js suffix, present on disk — each answering a reproduction rather than a hypothesis and each pinned by a mutation-verified test. Every rejection also logs: an opt-in that cannot do its job says so, instead of silently serving a declared path that may 404.

||||||| 682a6f1

Changed

  • |typography now typesets per languageTypographyExtension hands the upstream Parisek\Twig\TypographyExtension a locale resolver, so the languages: tables shipped by parisek/twig-typography ^1.3 (quote style, dash convention, single-character word spacing, …) actually apply. Without a resolver the upstream localeCandidates() returns [] and that whole layer is inert: only the language-neutral house defaults ever ran, so Czech content was typeset with English curled quotes (“ahoj”, not „ahoj“) and lost the non-breaking space after single-letter prepositions that Czech typography requires. Ported from StarterBase::typography_locale_resolver() in parisek/timber-kit, the WordPress-side sibling.

    The resolver reports the content language (LanguageInterface::TYPE_CONTENT), not the interface language. The two diverge exactly where it matters — an editor whose account language is Czech previewing an English node would otherwise get Czech typography applied to English prose. Drupal falls back to the interface language when content language negotiation is not configured, so monolingual sites are unaffected. Same distinction timber-kit documents for get_locale() vs determine_locale().

    FilterTypography now forwards its own $langcode. Drupal hands a text filter the language of the exact text being processed, and process() was discarding it — harmless while no language layer existed, wrong the moment one did: the filter would have typeset with the negotiated content language instead, which differs on mixed-language views, an explicitly rendered translation, mail and cron. applyTypography() takes an optional fourth argument for it; an empty langcode falls back to negotiation. A pinned language gets its own cache entry, so the negotiated path is unaffected.

    Note for direct instantiators: the constructor takes a fourth required argument. Container consumers are unaffected; the release doctrine excludes container-wired constructor signatures from the public API.

    Exposed as the overridable protected TypographyExtension::localeResolver() for sites whose language detection does not go through language_manager. The closure is evaluated per applyTypography() call rather than once at construction, so one cached upstream instance still serves every language in a request and the per-theme cache needs no language component.

    This changes rendered output on multilingual sites and on any monolingual site whose language has a languages: entry — review pages before deploying. Two regression tests pin the contract (Czech low-9 quotes reach the output; one instance typesets Czech and English differently across consecutive calls); both fail against the pre-fix constructor call.

  • Bumped the parisek/twig-typography floor from ^1.2 to ^1.3 — the languages: layer this change depends on does not exist before 1.3, where the resolver argument would be accepted and silently ignored.

  • Docs: Packagist is the distribution channel — the package is now published as parisek/drupal-kit on Packagist with the GitHub auto-sync webhook. README gains Packagist version + downloads badges and an Installation section (composer require parisek/drupal-kit); RELEASING.md drops the vcs repository entry instructions in favour of a Packagist sync-verification step and documents that the auto-created GitHub release must not be duplicated manually. Packagist serves every tag (including 1.x) under the canonical package name, so the vcs route is obsolete for all versions.

Fixed

  • The sitemap no longer lists the front page twicesystem.site points page.front at a node, and simple_sitemap listed that page as both / and the node's own URL. Where the redirect module's route normalizer is enabled the second answers 301, so the file handed crawlers a redirect to a page it already contained. drupal_kit_simple_sitemap_links_alter() drops the duplicate and keeps /, which is what Drupal itself declares canonical on the front page.

    page.front is translatable, so the filter is per language. A site can point each language at its own node; asking system.site once and applying that answer to every link deleted the node matching the generation-time language while leaving the other language's duplicate in place. The hook now builds a langcode → front-page map, judges each link by its own langcode, and prunes alternate_urls per language — a surviving link must not re-advertise the withheld URL through its hreflang block.

    Which entry survives is decided by what the page declares canonical, not by preference. Metatag ships canonical_url: '[site:url]' for the front page as its own default, and a site that disables that group falls back to the global group's [current-page:url-with-query:…], which on / resolves to / as well — so on any consumer running Metatag, keeping / agrees with the page. On a site without Metatag core declares the node's alias instead, and the two disagree; that limitation is recorded in ADR 0003 rather than argued away.

    The hook is unconditional, against AGENTS.md's opt-in default. It only runs where simple_sitemap is installed and only fires on the configured front page, so "always on" means "on exactly where the defect is"; and a flag defaulting to off would not reach the nineteen sites that have the defect and have not noticed. Reasoning, and the narrow precedent it sets, in ADR 0003.

  • merge_resizer() supports optional per-viewport images — ported from timber-kit's StarterBase::twig_merge_resizer() after the OPOP page-header case (optional mobile mascot variant) exposed two defects in the original implementation. (1) Empty groups are now dropped before the last-group detection: an unfilled optional image field makes Resizer return [], and keeping it as the "last" group filtered the remaining (desktop) group down to media-qualified variants — producing a <picture> with no unconditional <img> fallback at all. (2) The non-last-group filter switches isset($image['media'])!empty(...): Resizer sets media to '' for tuples without a breakpoint (and omits the key on the appended original image), so isset() leaked fallback-shaped desktop entries into the merged set ahead of the mobile entries, shadowing the mobile image on every viewport. Net effect: merge_resizer(desktop, mobile) keeps one call shape whether or not the optional image is filled — no {% if %} branching in templates. Two regression unit tests pin the contract (empty-group drop preserves the fallback; empty-media desktop entries are filtered when a mobile group follows). Consumers with a hand-mirrored copy in their theme's static/index.php (styleguide runs without Drupal) must apply the same change — done in opopcz and drupal-base.

  • PHPStan drift: DependencySerializationTrait vs promoted private readonly plugin propertiesFilterLinks and FilterTypography injected their services as constructor-promoted private readonly properties; FilterBase carries DependencySerializationTrait, which supports neither private nor readonly properties (#3110266), and a newer phpstan-drupal rule now fails the analysis (4 errors) on every fresh install — exactly the drift the no-composer.lock policy (ADR 0001) is meant to surface. Both properties are now plain protected, with an inline comment explaining the constraint. Full suite (347 tests), PHPStan level 8 and phpcs green.

Pull Requests

  • #107 — docs: switch distribution docs to Packagist
  • #108 — docs: correct Packagist 1.x tag availability note
  • #109 — fix: drop empty groups and use !empty(media) in mergeResizer for optional per-viewport images
  • #110 — fix: use protected instead of private readonly for DI properties in FilterLinks/FilterTypography
  • #111 — feat(typography): apply per-language tables via a content-language resolver
  • #112 — feat(assets): resolve a content-hashed JS entry from the Vite manifest
  • #116 — fix(sitemap): drop the front page's duplicate node entry from the sitemap

Full Changelog: v2.0.0...v2.1.0