To część 4 serii o przygotowaniu notACMS do wydania jako open-source. Część 3 opisuje luki bezpieczeństwa. Oryginalna seria o migracji z WordPressa opisuje samą migrację.


Chcesz udostępnić szablon projektu Symfony. Każdy użytkownik musi go dostosować — inne kolory, inna strona główna, inna nawigacja. Forkowanie oznacza że nie może pobierać aktualizacji z upstreama. Pliki konfiguracyjne nie obsłużą zmian w szablonach. Motywy są zbyt sztywne.

Rozwiązanie które zbudowałem dla notACMS to katalog local/ który scala się na projekt bazowy w czasie buildu. Użytkownicy customizują local/, reszta zostaje nienaruszona, a gdy upstream się zmienia — pull i merge jak każda inna operacja gita.

Problem

Trzy scenariusze których pliki konfiguracyjne nie rozwiążą:

  • Użytkownik A chce inne kolory CSS i customowy layout strony głównej
  • Użytkownik B chce nadpisać tylko komponent nawigacji
  • Użytkownik C chce dodać własne stringi tłumaczeń

Forkowanie to tradycyjna odpowiedź. Ale forkowanie oznacza że każda aktualizacja upstreama to ręczny merge. Dla projektu który dostaje regularne usprawnienia, to obciążenie utrzymaniowe które zabija adopcję.

Rozwiązanie: Warstwa scalania

local/ leży obok projektu bazowego. W czasie buildu ścieżki rozwiązują się z priorytetem: local/ pierwsze, baza druga. Trzy typy nadpisań:

Pełne nadpisanie — Zamiana całego pliku. local/templates/base.html.twig zastępuje templates/base.html.twig kompletnie.

Nadpisanie bloków — Rozszerzenie i nadpisanie konkretnych bloków Twig:

{% extends 'base.html.twig' %}

{% block stylesheets %}
    {{ parent() }}
    <link rel="stylesheet" href="{{ asset('styles/custom.css') }}">
{% endblock %}

Nadpisanie tłumaczeńlocal/translations/messages.en.yaml scala się z bazowymi tłumaczeniami, dodając lub zastępując klucze:

# local/translations/messages.en.yaml
site.title: "Moja Własna Strona"
nav.home: "Strona główna"

Kolejność CSS: Importmap z dwoma entrypointami

Najtrudniejsza część. Lokalne style muszą ładować się po stylach bazowych żeby je nadpisać przez kaskadę:

// importmap.php
->add('app', 'assets/app.js')           // baza: importuje app.scss
->add('local', 'assets/local/app.js')   // local: importuje local.scss

local.scss importowany po app.scss, więc kaskada CSS działa poprawnie. Bez !important. Importmap rejestruje oba entrypointy; przeglądarka ładuje je w kolejności.

Rozwiązywanie szablonów

Kernel::build() sprawdza pliki w local/templates/ i kopiuje je do outputu, nadpisując bazowe szablony. Katalog local/ jest gitignorowany w projekcie użytkownika, ale placeholdery .gitkeep zapewniają że świeże klony mają strukturę:

local/
├── assets/
│   └── styles/
│       └── .gitkeep
├── templates/
│   └── .gitkeep
└── translations/
    └── .gitkeep

Boilplate'y w docs/examples/

Gotowe szablony startowe do różnych poziomów customizacji:

Boilerplate Poziom customizacji Kiedy użyć
starter-extend/ Najlżejszy Chcesz rozszerzyć bazę drobnymi poprawkami
block-override/ Średni Chcesz zastąpić konkretne komponenty
full-override/ Pełny Chcesz pełną kontrolę nad layoutem
material-cards/ Motyw Kompletny przykład ciemnego motywu z local.scss
translation-override/ Stringi Tylko customowe stringi tłumaczeń

Co jest śledzone w Gicie

Pliki .gitkeep w katalogach placeholder local/ żeby struktura istniała w świeżych klonach. Faktyczne pliki nadpisań są gitignorowane — to customizacje użytkownika. docs/examples/ zawiera szablony copy-paste od których użytkownicy zaczynają.

Dlaczego nie framework?

notACMS nie jest frameworkiem. Użytkownicy nie robią composer install notacms/core. Klonują repo, customizują local/ i deployują. Wzorzec jest na tyle prosty że da się go zrozumieć w 5 minut, i na tyle potężny że obsłuży każdą customizację. Cała powierzchnia customizacji to jeden katalog.

W następnym poście opisuję pełną listę kontrolną wydania open-source — wszystko z poprzednich czterech postów prowadzące do finałowego launchu.