Tema ufficiale Riseact per i siti pubblici delle organizzazioni (<slug>.riseact.site
o dominio custom). Template Jinja2 + Tailwind, nessun framework JS, nessun bundler.
Slug della famiglia: launch (vedi manifest.json). La versione nel manifest è
quella che riseact theme release pubblica: va incrementata a ogni release.
- Avvio rapido
- Struttura del repository
- Come Riseact renderizza una pagina
- Il linguaggio dei template
- Anatomia di una sezione
- CSS
- JavaScript
- Convenzioni
- Pubblicazione
- Test
- Debito tecnico noto
yarn install
yarn watch # ricompila assets/main.css a ogni salvataggio
riseact auth login
riseact theme dev # sincronizza questa cartella con il tema di svilupporiseact theme dev apre una sessione: un tema in un'organizzazione di sviluppo,
creato al primo avvio e riusato dopo, memorizzato in .riseact/dev.json. La
sincronizzazione è bidirezionale — le modifiche locali salgono al salvataggio, quelle
fatte nell'editor visuale scendono con un poll ogni ~2s. In caso di conflitto vince
la modifica più recente; se entrambi i lati sono cambiati mentre la CLI era spenta,
vince il server.
Due cose che sorprendono sempre:
- Le cancellazioni non si sincronizzano all'avvio. Un file rimosso da disco e un file che il server ha in più sono indistinguibili, quindi la CLI lo riscarica. Rimuovi una sezione da entrambi i lati, o mentre la CLI è in esecuzione.
- Tailwind non conosce le classi che non hai ancora compilato. Se aggiungi una
classe e non gira
yarn watch, la pagina non cambia e non c'è nessun errore.
| Percorso | Cos'è |
|---|---|
manifest.json |
nome, slug, versione, autore della famiglia di temi |
layout/*.html |
il documento HTML completo. Riceve content_for_header e content_for_layout |
sections/*.html |
blocchi componibili, ognuno si autodescrive con una coda {% schema %} |
snippets/*.html |
frammenti inclusi con {% include %}. Niente schema, invisibili all'editor |
templates/*.json |
composizione della pagina: quali sezioni, in che ordine, con quali impostazioni |
templates/supporters/*.html |
pagine statiche non componibili (area sostenitore). La chiave del template è il percorso, quindi un file nuovo qui serve una rotta del core che lo renda |
config/settings_schema.json |
dichiara le impostazioni globali del tema |
config/settings_data.json |
i valori di quelle impostazioni, più le sezioni globali |
assets/* |
CSS, JS, font. Serviti su /assets/<nome> |
test/ |
suite Playwright. Non committata, non caricata come asset |
I temi sono solo testo: non esiste supporto per asset binari, le immagini stanno in
Media e si scelgono con un image_picker nello schema.
Alla piattaforma servono due categorie di file, e la distinzione decide chi vince a ogni aggiornamento del tema:
config/settings_data.json -> ORGANIZZAZIONE
templates/**/*.json -> ORGANIZZAZIONE
tutto il resto -> RELEASE
I file di release vengono sostituiti dall'aggiornamento. Quelli dell'organizzazione vengono migrati, mai sovrascritti: le chiavi mancanti si aggiungono con i valori della release, quelle esistenti restano come sono.
Nota che dentro templates/ la discriminante è l'estensione: i .json sono
composizione di pagina e appartengono all'organizzazione, gli .html sono codice.
Conseguenza pratica: config/settings_data.json e templates/*.json in questo repo
sono il sito di esempio di un'installazione nuova, non la configurazione di un
cliente. Un cliente che ha già il tema non li riceverà mai più.
- Sceglie il tema:
?__preview=<uuid>se presente, altrimenti quello corrente dell'organizzazione.?__draft=1sovrappone anche la bozza dell'editor visuale. - Carica
config/settings_data.json→settings(esposto ai template cometheme) esections(le sezioni globali dichiarate nei layout). - Carica
templates/<chiave>.json→ quale layout, quali sezioni, in che ordine. - Renderizza ogni sezione da
sections/<type>.html, unendo i default dello schema con le impostazioni dell'organizzazione, e avvolge ognuna in<div class="__theme_section">perché l'editor visuale sappia dove si clicca. - Renderizza il layout passando la concatenazione come
content_for_layout.
Una sezione che solleva un'eccezione non rompe la pagina. L'errore viene
iniettato nel markup: la pagina risponde 200 con Error rendering section X visibile
nel corpo. La suite test/specs/smoke.spec.js cerca esattamente quella stringa,
perché altrimenti nessuno se ne accorge.
È Jinja2, non Liquid. Chi arriva da Shopify sbaglia sistematicamente qui: la
sintassi si assomiglia ma la semantica no. Le chiamate a metodo esistono
(form.non_field_errors()), l'accesso per chiave esiste (form.errors[nome]),
{% macro %} esiste, i filtri sono quelli di Jinja.
Gira in un SandboxedEnvironment, quindi non tutti gli attributi Python sono
raggiungibili, ma per il resto è Jinja standard.
| Tag | Cosa fa |
|---|---|
{% schema %}…{% endschema %} |
coda di una sezione: JSON che ne dichiara impostazioni, blocchi e template compatibili |
{% section 'nome' %} |
renderizza sections/nome.html (usato nei layout per header, footer, cookie) |
{% style 'main.css' %} |
emette <link rel="stylesheet" href="/assets/main.css">, con il ?__preview= giusto |
{% asset 'nome.js' %} |
emette <script src="/assets/nome.js">, con il ?__preview= giusto |
{% nav item %} |
risolve l'URL di una voce di menu |
{% style %} e {% asset %} aggiungono da soli il parametro __preview: è l'unico
modo di caricare un asset che funzioni anche in anteprima. Non scrivere mai
/assets/... a mano.
Oltre a tutti i filtri Jinja standard, il motore ne aggiunge due:
format_money— formatta un importo nella valuta dell'organizzazioneformat_date— formatta una data ISOYYYY-MM-DDinDD/MM/YYYY, per esempio il valore di un custom field di tipodate. Un valore vuoto o non valido rende stringa vuota
Questa è la cosa più importante del documento.
Ogni {{ }} di un template è HTML grezzo. Il motore esegue Jinja con
autoescape off, perché i contenuti rich text (campaign.content,
section.settings.content) devono poter emettere markup. Non c'è nessuna protezione
automatica: un valore inserito da una persona e stampato senza filtro è una XSS.
Da qui la risposta alla domanda che tutti fanno: |e è l'alias di |escape, il
filtro Jinja che converte < > & " ' nelle entità HTML. Non ha niente di specifico a
Riseact, è Jinja puro.
La regola del tema:
{{ supporter.full_name|escape }} {# dato inserito da qualcuno: sempre escape #}
{{ campaign.content }} {# rich text del back office: volutamente HTML #}
{{ campaign.goal|int }} {# numero: nessun rischio #}Scrivi sempre |escape per esteso, mai |e: la forma breve è ambigua a colpo
d'occhio e sparisce nella lettura di una riga lunga. Nel tema non è più usata.
Dentro un attributo HTML |escape è comunque il filtro giusto, perché il tema usa
sempre le virgolette doppie. Dentro una stringa JavaScript non basta: lì il dato non
va messo affatto, si passa da un data- attribute (vedi JavaScript).
Disponibili su ogni pagina:
| Variabile | Cosa contiene |
|---|---|
theme |
le impostazioni globali del tema, cioè settings di settings_data.json |
section |
le impostazioni della sezione corrente, e section.blocks / section.block_order |
organization |
l'organizzazione proprietaria del sito |
supporter |
il sostenitore loggato, o falsy |
supporter_urls |
login, logout, dashboard, profile, donations, campaigns |
csrf_token |
da mettere in ogni form POST come csrfmiddlewaretoken |
current_url |
URL assoluto della pagina, usato dai pulsanti di condivisione |
pages, campaigns, blogs, articles, menus, menuitems |
elenchi per costruire liste e menu |
Aggiunte dalla vista che serve la pagina:
| Variabile | Dove |
|---|---|
campaign |
campaign, peer_campaign, thankyou |
peer_campaign, stats |
peer_campaign, peer_campaign_form, templates/supporters/peer_campaign.html |
peer_campaign_updated, peer_campaign_public_url, peer_campaign_update_url_v2 |
pagina di gestione (peer_campaign_form servito su .../update/v2/) |
peer_campaign_updated, peer_campaign_public_url, peer_campaign_manage_url |
dettaglio campagna dell'area (templates/supporters/peer_campaign.html, servito su /my/campaigns/<id>/) |
form, custom_field_definitions, captcha, lead_form_success |
campagne di tipo lead |
profile |
area sostenitore |
Le viste che proteggono un form mettono captcha in contesto. Sono tre e solo tre:
lead box, checkout, login sostenitore. I form dentro l'area sostenitore non lo
portano, e un tema che ci disegna un widget non protegge niente a spese del donatore.
Quando captcha.enabled è falso il tema non renderizza nulla. Altrimenti il dict
porta challenge_url, script_url, worker_url, algorithm, i18n_url,
language, field_name, honeypot_field. Endpoint e percorsi degli asset si
leggono da lì, non si scrivono a mano: il core può cambiarli.
Il tema deve anche renderizzare form.non_field_errors() da qualche parte: un
honeypot scattato è un errore di form, perché il campo da cui arriva è invisibile e un
errore attaccato a quel campo rifiuterebbe l'invio senza dirlo a nessuno.
L'implementazione di riferimento è in snippets/campaign-lead-box.html.
<div class="container">
{{ section.settings.title|escape }}
</div>
{# djlint:off #}
{% schema %}
{
"name": "Rich text",
"settings": [
{ "id": "title", "type": "text", "label": "Titolo", "default": "" }
],
"blocks": [],
"presets": { "settings": {}, "blocks": [] },
"locales": {},
"templates": ["homepage", "page", "article"]
}
{% endschema %}{# djlint:off #}prima dello schema è obbligatorio: senza, il formattatore reindenta il JSON e lo rompe.settingsgenera i campi nell'editor visuale. Tipi in uso nel tema:text,richtext,checkbox,select,slider,color_picker,image_picker,menu_picker,campaign_picker,header.blockssono elementi ripetibili. Si iterano consection.block_order(ordinati) oppure consection.blocks.items().presetsserve perché la sezione sia aggiungibile a una pagina dall'editor. Una sezione senza presets si può usare solo se untemplates/*.jsonla nomina.templateslimita in quali pagine la sezione compare. Le sezionimain_*dichiarano il proprio template e non vanno altrove; per quelle componibili l'elenco è ancora da verificare caso per caso, vedi il debito tecnico.limitfissa quante istanze può avere una pagina, per le sezioni principali è 1.
Lo schema viene validato al momento della release: un JSON non valido blocca la
pubblicazione. Non viene validato durante theme dev.
Un solo foglio di stile: assets/tailwind.css è la sorgente, assets/main.css è il
build. Il build è committato, perché è il file che viene caricato come asset del
tema: se lo dimentichi, in produzione va la versione precedente.
yarn build # una volta
yarn watch # in sviluppoI colori primari arrivano dalle impostazioni del tema e sono esposti come custom
properties CSS su :root in layout/theme.html:
--primary-bg-color— colore di sfondo primario (bottoni pieni)--primary-fg-color— colore del testo sopra il primario
Si usano attraverso le classi componente btn-primary, btn-outline, btn-sm,
input, richtext, definite in assets/tailwind.css. Se ti serve il colore diretto,
la sintassi Tailwind è bg-[color:var(--primary-bg-color)] /
text-[var(--primary-fg-color)]. Non esistono altre variabili di colore: ogni
var(--qualcosa) che non sia una delle due qui sopra rende la dichiarazione invalida
e l'elemento resta trasparente, senza errori da nessuna parte.
Nel tema c'è ancora del CSS scritto a mano dentro <style> di sezione. Tre regole se
devi toccarlo:
- Preferisci sempre una utility Tailwind. Il
<style>serve solo quando il valore viene da un'impostazione (background-color: {{ section.settings.bg_color }}). - Ogni selettore va agganciato a una classe della sezione. Un
<style>di sezione finisce nel documento globale:p { font-size: 1.2rem }dentrosections/cookie.htmlcambia la dimensione di ogni paragrafo del sito. - Il CSS con interpolazione Jinja non va mai passato a un formattatore JS/CSS —
spezza
{{ }}in{ { } }e il template smette di funzionare senza dirlo.
Stato attuale: assets/ non contiene nessun file JS. Tutto il comportamento vive
in <script> sparsi in 8 fra sezioni e snippet, più 17 handler onclick inline. È il
problema principale del tema (vedi il debito tecnico) e la direzione è: codice
condiviso in assets/theme.js, caricato con {% asset 'theme.js' %}.
Nel frattempo, le regole che vale la pena rispettare in ogni script nuovo:
- Niente handler inline.
onclick="..."non passa una Content Security Policy, non si testa, non si trova con una ricerca. Aggancia gli eventi conaddEventListenersu undata-attribute. - Aggancia sui
data-attribute, non sugliid. Uno snippet può essere incluso più volte nella stessa pagina —campaign-lead-box.htmlecampaign-donate-box.htmllo sono sempre, colonna mobile e colonna desktop — e in quel caso gliidsono duplicati.document.getElementByIdrestituisce il primo, che sulla vista sbagliata è quello nascosto. - Lo script di uno snippet incluso due volte viene eseguito due volte. Rendi
l'inizializzazione idempotente, o vincola la logica al form più vicino
(
element.closest('form')). - I dati dal template si passano con un
data-attribute, non interpolando Jinja dentro una stringa JavaScript.|escapeprotegge l'HTML, non il contesto JS. - Niente
document.onkeydown = ...e simili: assegnare la proprietà cancella l'handler di chiunque altro.addEventListener. - Niente
console.lognel codice che va in release.
Lingua. Il testo visibile è in italiano. I commenti sono misti oggi; per il nuovo codice usa l'italiano nei commenti, coerente con il resto del prodotto.
Commenti. Un commento spiega perché, mai cosa. {# relative z-40 mette l'header davanti alla pagina #} sopra class="relative z-40" non aggiunge niente;
{# senza questo il menu utente finisce sotto il donate box, che è sticky #} sì. Se
il commento riformula il codice, cancellalo.
Usa i commenti Jinja {# … #} e non <!-- … -->: i primi non arrivano al browser.
Nomi. File in kebab-case per gli snippet, snake_case per le sezioni (è la
chiave che l'editor usa). Attributi di aggancio JS con prefisso esplicito:
data-lead-submit, data-custom-select, data-supporter-type-toggle.
Formattazione. djlint con il profilo jinja, configurato in .djlintrc.
djlint . --reformatNon passare mai questi file a Prettier o a un formattatore CSS/JS: non conoscono
Jinja e spezzano {{ }} e {% %} in modo silenzioso. È già successo — vedi
layout/theme.html nel debito tecnico.
Segreti. riseact.yml contiene un access token. È in .gitignore e in
.riseactignore: non deve né essere committato né finire caricato come asset del
tema. Lo stesso vale per test/, che contiene credenziali.
riseact theme releasePubblica una versione immutabile. Prima valida tutto: manifest presente e semver,
ogni file Jinja che compila, ogni {% schema %} che è JSON valido, e riporta tutti
gli errori insieme, non il primo. Poi rifiuta versioni duplicate, versioni più vecchie
dell'ultima, changelog vuoto.
Le versioni sono un sottoinsieme stretto di semver: esattamente tre componenti numeriche, niente pre-release e niente build metadata. Gli aggiornamenti vanno solo avanti: per tornare indietro si installa la versione vecchia come tema separato.
Il changelog si compone nell'editor e non esiste un flag per passarlo, quindi la pubblicazione non può partire da CI. È voluto.
Prima di rilasciare: yarn build, versione in manifest.json incrementata, suite
e2e verde.
Suite Playwright in test/, eseguita contro l'anteprima live. La cartella è ignorata
sia da .gitignore sia da .riseactignore: non è committata e non viene caricata.
cd test
npx playwright test
npx playwright test --grep-invert @write # sola lettura, non crea nienteDocumentazione completa in test/README.md (cosa copre, fixture e loro limiti),
test/AGENTS.md (credenziali, come ottenere un token API) e test/LOCAL.md (come
girare contro un riseact-core locale, l'unico posto con custom field select e
date da testare).
Vale la pena conoscere due convenzioni della suite:
- i test taggati
@writeinviano davvero il form lead e creano Supporter reali; - i bug trovati e non ancora corretti stanno in
specs/known-issues.spec.jsmarcatitest.fail(). La suite resta verde finché il bug c'è e diventa rossa il giorno in cui qualcuno lo sistema, che è il promemoria per cancellare la voce.
Ordinato per impatto. Quello che è già stato corretto sta in fondo.
- Nessun asset JS. Portare in
assets/theme.jsquello che è condiviso — modali, dropdown, toggle descrizione, chiusura con ESC — e caricarlo dal layout con{% asset 'theme.js' %}. Il resto resta accanto alla sua sezione, ma agganciato conaddEventListener, nononclick. Sono 8<script>inline e 16 handleronclickda riportare a casa. snippets/campaign-donate-box.html:102assegnadocument.onkeydown, quindi cancella qualunque altro handler globale di tastiera. DiventaaddEventListenerquando il codice si sposta intheme.js.- Lo snippet del donate box viene incluso due volte (colonna mobile e colonna
desktop) e il suo
<script>viene quindi eseguito due volte:openModal,closeModaletoggleDescriptionsono ridefinite e l'handler ESC riassegnato. Innocuo oggi, ma è la stessa trappola che rende fragili gliidduplicati. --primary-colornon esiste ma è usata insections/banner.html:2: la dichiarazione è invalida e l'elemento resta trasparente. È--primary-bg-color. Le due occorrenze insections/main_campaign.htmlsono state corrette.
- L'escaping è applicato a macchia di leopardo. Il giro peer-to-peer
(
peer_campaign_form.html,peer_campaign.html, il blocco delle raccolte personali dimain_campaign.html),user-menu.htmle l'area sostenitore usano|escape; il resto del tema non escapa niente — compresicomment.messageedonation.message, che sono testo scritto dai donatori. Con l'autoescape disattivato ognuno di quei punti è una XSS. Serve un passaggio completo. "templates"negli schema resta approssimativo.sections/cookie.htmldichiara[""], e diverse sezioni componibili elencano["article","homepage","page"]senza che nessuno abbia verificato che sia l'elenco giusto.- Indentazione mista: 2 e 4 spazi, più un tab in
sections/header.html. Da uniformare condjlint --reformatin un commit dedicato, che non tocchi altro. - Commenti in due lingue nello stesso file (
campaign-lead-box.htmlalterna inglese e italiano). Scegliere l'italiano e allineare. sections/cookie.htmlcalcolapreferencese non lo usa, esetCookie()non imposta nessun cookie: nasconde due elementi. Nomi da correggere.
layout/theme.html— un formattatore JS passato sul file nel commit3aea482aveva spezzato{{ theme.primary_fg_color }}in{ { … } }. Le due custom property non venivano più emesse e ognibtn-primarydel sito era senza colore.sections/cookie.html— il pulsante "Declina" non deselezionava niente (!form.elements[i].name === "technical"è sempre falso) e salvava il consenso com'era. Problema di conformità, non di stile.sections/cookie.html—p,h2,hr,form inpute una regolalabelvuota erano selettori globali dentro il<style>della sezione: cambiavano la tipografia di tutto il sito a partire da 405px. Ora sono agganciati a.cookie-banner,.cookie-preferences-modale#cookiePreferencesForm.sections/contact_form.html— il campo telefono usavaidenamedell'email e ne sovrascriveva il valore;requiredera invertito, quindi il campo era obbligatorio proprio quando eratype="hidden"e bloccava l'invio con un errore che il browser non riesce a mostrare. Ora i campi opzionali non vengono renderizzati affatto. Corretti anche ilfordelle label,full_witdh→full_widthe la lettura didata.errorinvece didata.errors, che ringraziava anche quando il server aveva rifiutato il form.sections/main_campaign.html— rimosso il codice morto:toggleDescriptionduplicata dacampaign-donate-box.htmle unDOMContentLoadedche cercava#show-all-btn, che su quel template non esiste, con la condizione per giunta invertita."product"rimosso da tutti gli schema: non è un template Riseact.main_404,main_500,main_projectemain_project_listora dichiarano il proprio.
Copyright (c) 2023-present Riseact. Vedi LICENSE.