plugin-escal
Use when working on a SPIP site that uses the Escal squelette — installing or activating it, placing or configuring its noisettes (blocs latéraux, onglets « À la une »), reading or writing its escal/c
Files5
View on GitHubSKILL.mdskills/plugin-escal/SKILL.md
GitHubEscal (squelette SPIP)
Overview
Escal is a jeu de squelettes (full theme) for SPIP, prefix escal, category squelette.
Current release read here: 5.6.2 (de508fb, 2026-10-15), schema 1.0.17, GNU/GPL, compatible [4.1.0;4.*].
Author: Jean-Christophe Villeneuve. Docs: https://contrib.spip.net/Escal-4077.
The one thing to understand: Escal is a composition engine driven by one meta.
A single SPIP meta named escal, casier escal/config, decides both
- which block goes where — every slot in every page is an
<INCLURE>whose fond name is read from the meta, and - what the site looks like — six stylesheets are SPIP squelettes (
*.css.html,#CACHE{0}) recompiled per request with#CONFIGvalues injected.
[(#CONFIG{escal/config/blocnav3}|=={rien}|non)
<div class="cadre-couleur">
<INCLURE {fond=inclusions/inc-#CONFIG{escal/config/blocnav3,edito}}{env}{ajax}>
</div>
]
Nothing about the composition is hard-coded in the page squelettes. Changing a setting changes which file gets included.
SPIP 5.0
This skill describes SPIP 4.x. Check the site's version first and name the marker you relied on. A site is 5.0 if
any of: ecrire/src/SpipVersion.php declares VERSION = '5.…', bin/spip exists, vendor/spip/framework/
exists, plugins live in plugins-dist/spip/.
- Escal does not run on 5.0: its latest release (5.6.2, 2026-10-15) declares
compatibilite="[4.1.0;4.*]", which SPIP 5.0 refuses, so it is never activated (a site upgraded to 5.0 falls back to the default squelettes; theescalmeta stays in base). Read thepaquet.xmlof the release at hand before any advice. Never force it (editingcompatibilite,_DEV_VERSION_SPIP_COMPAT). The options: keep the site on 4.4, wait for a release whosecompatibilitereaches 5.x, or choose another squelette. - Going back to 4.4 means restoring the pre-migration backup, files and database. Never offer to reinstall 4.4
over the migrated database: 5.0 rewrites array settings as JSON, which 4.4 (
unserialize) does not decode, so the Escal settings would not come back. Without that backup, say there is no verified way back with the settings. - The spip-cli recipes below do not apply to a 5.0 site: spip-cli is a 4.x tool, and 5.0's
bin/spiphas no download, activation orphp:evalcommand. The commands useful here:config:lire/config:ecrireandspip:cache:clear.bin/spip config:lire escal --jsonshows the kept settings. Its JSON is pretty-printed ("cle": null): to test whether Escal was ever installed, read the one keyescal_base_versionand matchgrep -Eq '": ?null'(not':null}'). Never run that test on the wholeescalmeta, where anynullsetting would match.
Details: spip-migration-5 (references/audit-outils.md: bin/spip commands); 4.4 → 5.0 upgrade and rollback of
the site: spip5-instance (references/upgrade-4-4.md).
Vocabulary traps — verify before answering
These are the things agents reliably get wrong about Escal. Every row was an actual hallucination in baseline testing.
| Wrong (do not say) | Correct |
|---|---|
noisettes/noisette_*.html |
Noisettes live in inclusions/inc-*.html. There is no noisettes/ directory. |
spip plugin:install escal |
spip-cli uses plugins:svp:telecharger then plugins:activer. There is no plugin:install. |
Meta is config_escal |
Meta is escal; settings sit in casier escal/config; read with #CONFIG{escal/config/‹cle›} or lire_config('escal/config/‹cle›'). |
bloc_gauche_2, bloc_droite_1 |
Slot keys are blocnav‹n› (colonne navigation) and blocextra‹n› (colonne extra), with a page-family suffix: blocnavart3, blocextrarub2, blocnavpages7, blocnavforumsite4. |
"plugins:svp:telecharger said it worked, so the plugin is there" |
On unpatched spip-cli it prints successes while downloading nothing (MR !91). Always confirm with plugins:lister --short, never with the download command's own output. |
| Escal has a "paste your CSS here" field | It does not. Surcharge via squelettes/styles/perso.css and squelettes/persoconfig.css.html. |
| "fluide/mixte change the column order" | Column order is the letters (PMP, MPP, PPM, MP, PM). fixe/fluide/mixte are width modes, orthogonal to order. |
"blocnav = colonne de gauche" |
Not always. navigation/extra are slots, not sides — which side they land on depends on the layout, and RTL swaps them. See the placement table below. |
"Drop inc-monbloc.html in squelettes/, then type monbloc in a slot" |
No. Every bloc* slot is a <select> with a hard-coded option list (formulaires/configurer_escal_choix_blocs.php). The webmestre picks a block; nothing scans the filesystem. To ship your own content use perso / article_libre1…5, or override a listed noisette's file. |
| Tab value = noisette filename | The onglet‹n› values are their own vocabulary: derniersarticles → inc-une_derniers.html, rubrique2 → inc-rubrique_accueil2.html. See references/noisettes.md. |
When unsure of an exact key or filename, read the plugin source rather than guessing. The authoritative list of block values is the 'data' arrays in formulaires/configurer_escal_choix_blocs.php.
Install with spip-cli
Verified against spip-cli 2.0.1 on SPIP 4.4.14. Run spip list if a command name looks off.
⚠ plugins:svp:telecharger is broken in stock spip-cli
Upstream 2.0.1 cannot download a plugin at all, and hides it:
- the depot lookup compares
UPPER(pl.prefixe)toLOWER("PREFIXE")— never equal, so every plugin reports « Le plugin xxx n'est pas référencé »; - past that, the
autoriser_exceptionis registered under the wrong key andaction/teleporter.phprefuses the download; - and
one_action()returns the action description before running it, so failures were printed as successes.
Fixed by MR !91. Check the local copy before trusting a download:
grep -q 'UPPER(pl.prefixe) = UPPER' src/Command/PluginsSvpTelecharger.php \
&& echo "patché (MR!91)" || echo "NON patché — les téléchargements échoueront en silence"
On an unpatched CLI, install the plugins by hand into plugins/ (or via the private area) and use
spip-cli only for plugins:activer.
Recette
Run from the SPIP root (the directory holding config/connect.php).
# 0. SVP unpacks into plugins/auto/ — core:preparer does NOT create it.
# Without it every download fails with
# « Le répertoire de paquets plugins/auto/ n'est pas accessible en écriture ».
mkdir -p plugins/auto && chmod u+rwx plugins/auto
# 1. register the official depot (once)
spip plugins:svp:depoter https://plugins.spip.net/depots/principal.xml
# 2. download Escal and its hard dependencies
spip plugins:svp:telecharger saisies verifier yaml agenda fullcalendarcompat \
svpstats nospam facteur tri_par_rubrique aide escal -y
# 3. activate, dependencies first
spip plugins:activer saisies verifier yaml agenda fullcalendarcompat \
svpstats nospam facteur tri_par_rubrique aide escal -y
# 4. run the plugin upgrades, then check
spip plugins:maj:bdd # prints « Installation du plugin Escal … MAJ 1.0.17 »
spip plugins:lister --short # every prefix above must appear
spip cache:vider
SVP unpacks one directory per version: plugins/auto/escal/v5.6.2/, not plugins/auto/escal/.
Step 2 also pulls calendriermini as a transitive dependency — expect 11 packages, not 10.
⚠ After a CLI install, the starter contents stay unpublished
escal_upgrade() creates a hidden rubrique plus three articles — Édito, Accès direct,
Mentions légales — already tagged, and asks for statut = publie. From the private area that
works. From spip-cli it does not: there is no authenticated author, autoriser('publierdans', …)
returns false, and the three articles are left in prepa. The Édito and Accès direct blocks then
render empty and the site looks broken. Publish them by giving the CLI a session:
spip php:eval 'include_spip("action/editer_objet"); include_spip("base/abstract_sql");
$GLOBALS["visiteur_session"] = sql_fetsel("*", "spip_auteurs", "statut=" . sql_quote("0minirezo"));
$GLOBALS["visiteur_session"]["webmestre"] = "oui";
foreach (sql_allfetsel("id_article", "spip_articles", "statut=" . sql_quote("prepa")) as $x) {
objet_instituer("article", $x["id_article"], ["statut" => "publie"]);
}'
Note the sql_quote(): the whole PHP program travels inside shell single quotes, so any nested
\" is mangled before PHP sees it. Build SQL string literals with sql_quote() rather than
escaping quotes — that is the form that actually runs.
Same limitation for anything scripted through php:eval that crosses an autoriser() check — set
the session first. config:ecrire is unaffected: it writes spip_meta directly.
Notes
plugins:activer and plugins:svp:telecharger take prefixes as positional arguments, or
--from-list=a,b,c; -y skips the prompts. Activating escal runs escal_upgrade(), which creates
the two mots-clés groups and the starter contents — never hand-write the escal meta to "install" it.
Optional plugins Escal detects if present: palette, spip_400, orr (typographic shortcuts),
Shoutbox, reservation_evenement, reservations_multiples. Enable the matching noisettes in the
Plugins config screen.
Reading and writing settings from the CLI
config:lire and config:ecrire operate on spip_meta, so they reach Escal's casier directly —
handy for scripted setup and for inspecting a site you cannot log into.
spip config:lire escal --json # dump the whole configuration
spip config:lire escal/config/blocnav2 # one setting
spip config:lire escal_base_version --json # {"escal_base_version":null} ⇒ not installed
spip config:ecrire escal/config/blocnav2:derniers_articles # full path as the option
spip config:ecrire -p escal config/blocnav2:derniers_articles # same thing, -p prepends "escal/"
spip config:ecrire escal/config/nomsite --valeur 'Mon site' # value containing ":" or spaces
The argument is split on the first colon only. A value that itself contains a colon (a URL, a
time) must go through --valeur.
Use --json whenever you test for absence. Plain config:lire always prints the key label, so its
output is never an empty string and [ -z "$(spip config:lire escal_base_version)" ] silently
passes even when the plugin has never been installed. Compare against :null} instead.
Several settings at once — note the nesting, -p escal plus a config sub-object rebuilds
escal/config/…:
spip config:ecrire -p escal --json '{"config":{"blocnav1":"edito","blocnav2":"derniers_articles","layout":"PMPfluide"}}'
spip cache:vider
Always cache:vider afterwards: the generated stylesheets are #CACHE{0} but the pages that include
them are not.
Placing a noisette
Configuration UI: espace privé → Squelettes → Escal (webmestre only). Page ?exec=configurer_escal&cfg=choix_blocs.
The slot key encodes column + page family + rank:
| Page family | Colonne navigation | Colonne extra | Centre |
|---|---|---|---|
| Sommaire | blocnav1…10 |
blocextra1…10 |
blocune1…3 |
| Article | blocnavart1…6 |
blocextraart1…6 |
— |
| Rubrique | blocnavrub1…6 |
blocextrarub1…6 |
— |
| Autres pages | blocnavpages1…10 |
blocextrapages1…10 |
— |
| Forum de site | blocnavforumsite1…10 |
— | — |
The value is a noisette short name (no inc- prefix, no .html), or rien for an empty slot.
Example — Derniers articles in second position of the navigation column on the home page:
ecrire_config('escal/config/blocnav2', 'derniers_articles');
// → renders inclusions/inc-derniers_articles.html
Each slot is a closed dropdown. The screen renders a Saisies selection per slot; its options are
hard-coded in the 'data' arrays of formulaires/configurer_escal_choix_blocs.php. The webmestre
chooses among them — there is no free-text field, and adding a file to squelettes/inclusions/ adds
nothing to the list. Option counts per dropdown (rien included) differ by family and by column:
sommaire 29 nav / 27 extra, article 22 / 20, rubrique 20 / 18, autres pages 21 / 19, forum de site 16.
The only values that differ between the two columns are choixmenuV1 / choixmenuV2, offered in
blocnav* only.
ecrire_config() bypasses the form, so a value outside the list is stored as-is: it renders
inclusions/inc-‹valeur›.html if that file exists, an empty slot otherwise — and the next save of the
config screen drops it back to a listed value. Do not sell that as the way to add a block.
Full catalogue, per-family availability, and the central « À la une » tabs: references/noisettes.md.
Layouts
Order is spelled by the letters, read left to right: P = narrow column, M = main#contenu.
- Orders:
PMP,MPP,PPM,MP,PM - Width modes: fixe (suffix none), fluide, mixte (side columns fixed, centre elastic)
- Mixte exists only for the 3-column orders → 13
layout*.css.htmlfiles
The 13 valid values of escal/config/layout, verbatim:
PMP MPP PPM MP PM · PMPfluide MPPfluide PPMfluide MPfluide PMfluide · PMPmixte MPPmixte PPMmixte
Widths: largeurlayoutbase + colonnelayoutbase (fixe), largeurmaxlayoutfluide (fluide), colonnelayoutmixte (mixte).
Where each column actually lands — navigation is not a synonym for "left":
| Ordre | Gauche → droite | Colonne la plus à gauche | Colonne la plus à droite |
|---|---|---|---|
PMP |
nav · contenu · extra | blocnav* |
blocextra* |
PPM |
nav · extra · contenu | blocnav* |
blocextra* (2ᵉ en partant de la gauche) |
PM |
nav · extra · contenu | blocnav* |
blocextra* (2ᵉ en partant de la gauche) |
MPP |
contenu · nav · extra | blocnav* (mais à droite du contenu) |
blocextra* |
MP |
contenu · nav · extra | blocnav* (mais à droite du contenu) |
blocextra* |
blocnav* always precedes blocextra* in reading order; what changes between layouts is where the
pair sits relative to the content. In MPP and MP there is no column left of the content — a
request for "the left column" there needs a layout change (PMP, PPM or PM) first.
In RTL (#LANG_DIR = rtl) the two <section> elements are emitted in the reverse order, mirroring the whole thing. So "put this block in the left column" is only answerable once you know the site's layout — check escal/config/layout first.
The layout stylesheet is loaded with media="screen and (min-width: 641px)" — below that, styles/mobile.css stacks everything. #ENV{layout} overrides #CONFIG, so ?layout=MPfluide previews without saving.
Mots-clés techniques
Installation creates two groups: affichage (44 mots) and Agenda_couleur (5 mots, hex code or CSS colour in the descriptif). Squelettes test them with {titre_mot=…}.
Three roles — full list in references/configuration.md:
- Reroute the squelette:
pleinepageon an article →article_pleinepage.html;forumon a secteur →forumSite-*.html;trombino,popup. - Designate content:
edito,acces-direct,actus,annonce,citations,mentions-legales— the noisette fetches the article carrying the mot. - Exclude from a listing:
invisible,pas-au-menu,pas-a-la-une,pas-au-plan,site-exclu.
Two further groups, type_article and type_rubrique, are not created by the installer — the webmestre creates them. Attach such a mot to a rubrique and Escal includes inclusions/inc-article_‹mot›.html / inc-rubrique_‹mot›.html when that file exists, falling back to inc-article.html / inc-rubrique_normal.html. This is the supported way to give a section its own gabarit.
To create one: espace privé → Mots-clés → Créer un nouveau groupe de mots, name it exactly
type_rubrique (or type_article), tick Rubriques (resp. Articles) in its tables_liees,
add a mot, then attach that mot to the rubrique.
How the filename is derived: the squelette does #SET{type, #TITRE} and includes
inclusions/inc-article_#GET{type}.html — no filter is applied, so the mot's titre is used
verbatim, with no slugifying, no lowercasing and no accent stripping. A mot titled Recettes looks
for inc-article_Recettes.html. The one file Escal ships for this mechanism is
inc-rubrique_trombino.html, so it needs a mot titled exactly trombino, lowercase. Give your own
mots short ASCII titles with no spaces, and name the file with the identical string. If the file is
missing, Escal silently falls back to inc-article.html / inc-rubrique_normal.html — a wrong case
looks exactly like "the feature doesn't work".
Do not read inc-article_forumSite.html / inc-rubrique_forumSite.html as examples of this
mechanism: they are included directly by forumSite-article.html and forumSite-rubrique.html,
which are reached through the forum mot on the secteur.
Restyling without forking
SPIP resolves squelettes/ before plugins, and both hooks load after every generated stylesheet.
| Goal | File to create |
|---|---|
| Plain CSS overrides | squelettes/styles/perso.css |
CSS that reads #CONFIG |
squelettes/persoconfig.css.html (ship-empty file exists for this) |
| Colour one section | squelettes/styles/secteur‹ID_SECTEUR›.css |
| Replace a block | squelettes/inclusions/inc-‹nom›.html |
| Add your own content to a lateral slot | Nothing new to name: use the listed perso (articles tagged special) or article_libre1…5, and override squelettes/inclusions/inc-perso.html if the markup must change |
| Give a rubrique its own central gabarit | squelettes/inclusions/inc-rubrique_‹mot›.html + a mot of group type_rubrique |
Never edit files inside the plugin directory — upgrades overwrite them.
Reference files
references/noisettes.md— the 107 noisettes: catalogue, per-family availability, « À la une » tabs, per-noisette settingsreferences/pages-speciales.md— agenda, mini-calendrier, auteur, forum d'article, recherche, 404, contact, annuaire, mots-clés, trombinoscope, forum de site: URL, activation, dependenciesreferences/configuration.md— the 24 config screens, key-naming conventions, the 44 technical mots-clés, PHP surface (filtres, balises, pipelines)references/documentation-amont.md— map of the author's tutorial site to noisettes, pages and settings. Beginner-oriented human docs, secondary to the code: never quote an identifier or a default from it without confirming it in the source.