docs: capture Grav plugin-config-must-be-tracked convention (ce-compound)
Knowledge-track: functional Grav plugin config must live in the tracked user/config/plugins/<name>.yaml override, not the gitignored (and gpm-update-wiped) user/plugins/<name>/<name>.yaml. Secrets stay in gitignored *-private.php. Root of the api plugin being unconfigured on the fresh prod install. Cross-linked with the api/rc.10 install-failure doc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "Grav plugin config must live in the tracked user/config/plugins/ override, not the plugin folder"
|
||||
date: 2026-07-04
|
||||
category: docs/solutions/conventions
|
||||
module: "grav / plugin configuration"
|
||||
problem_type: convention
|
||||
component: tooling
|
||||
severity: high
|
||||
applies_when:
|
||||
- "Editing functional config for any GPM-managed Grav plugin"
|
||||
- "user/plugins/ is gitignored and only pages/config/accounts/themes are tracked"
|
||||
- "Preparing a fresh install or production cutover"
|
||||
- "A plugin behaves correctly locally but ships with only default config on deploy"
|
||||
related_components:
|
||||
- "grav"
|
||||
- "gpm"
|
||||
- "api plugin"
|
||||
- "content repo"
|
||||
- "deployment"
|
||||
tags:
|
||||
- grav
|
||||
- plugin-config
|
||||
- gpm
|
||||
- config-override
|
||||
- gitignore
|
||||
- deployment
|
||||
- api-plugin
|
||||
---
|
||||
|
||||
# Grav plugin config must live in the tracked user/config/plugins/ override, not the plugin folder
|
||||
|
||||
## Context
|
||||
|
||||
Grav resolves a plugin's config by deep-merging two layers: the plugin's own shipped file `user/plugins/<name>/<name>.yaml` (installed by GPM, part of the package) and the tracked override `user/config/plugins/<name>.yaml` (which wins). In this project the content repo tracks only `pages/`, `config/`, `accounts/`, `themes/`; `user/plugins/` and `user/data/` are gitignored (GPM manages plugin *code*). So any functional config a developer edits into a plugin's own `user/plugins/<name>/<name>.yaml` is invisible to version control.
|
||||
|
||||
It was this gap that left the `api` plugin unconfigured on the fresh prod install. Its `enabled`/`route`/`session_enabled`/cors/rate_limit config existed only in the untracked plugin folder locally, while the committed `user/config/plugins/api.yaml` held only a runtime `popularity.salt`. The local machine worked because the plugin folder had been hand-edited; every fresh environment got only the plugin's shipped defaults.
|
||||
|
||||
## Guidance
|
||||
|
||||
Put **functional** plugin configuration in the TRACKED override `user/config/plugins/<name>.yaml`. Grav deep-merges it over the plugin's shipped defaults, so it need only carry the keys that must differ (or the full config, for clarity). Keep **secrets and per-install generated values** OUT of the tracked file — JWT secrets, salts, encrypted tokens belong in gitignored `*-private.php` companion files (e.g. `api-private.php`, `security-private.php`) or should be regenerated per-install.
|
||||
|
||||
Never rely on edits to the plugin's own `user/plugins/<name>/<name>.yaml`: it is gitignored (won't deploy) and is overwritten on the next `php bin/gpm update`.
|
||||
|
||||
Concrete before/after, using the `api` plugin:
|
||||
|
||||
```yaml
|
||||
# WRONG: user/plugins/api/api.yaml (gitignored, GPM-managed, wiped on update)
|
||||
enabled: true
|
||||
route: /api
|
||||
auth:
|
||||
session_enabled: true
|
||||
```
|
||||
|
||||
```yaml
|
||||
# RIGHT: user/config/plugins/api.yaml (tracked, deploys, survives gpm update)
|
||||
enabled: true
|
||||
route: /api
|
||||
version_prefix: v1
|
||||
auth:
|
||||
session_enabled: true
|
||||
# JWT secret intentionally NOT here — it lives in the gitignored api-private.php
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Reproducible deploys: a fresh clone or `make remote-install-<env>` must produce a working site from the repo alone. Config stranded in the gitignored plugin folder silently yields a plugin with only its shipped defaults on every new environment — which, for a plugin whose behavior depends on non-default config, means it's misconfigured or effectively off. On prod the `api` plugin's route/auth simply didn't work.
|
||||
|
||||
The failure is silent and per-environment: it works on the developer's machine (where the plugin folder was hand-edited) and breaks everywhere else. `gpm update` compounds it by wiping the folder edit even locally, so the "working" state is not just unshared — it is also unstable on the one machine that had it.
|
||||
|
||||
## When to Apply
|
||||
|
||||
- Any time you configure a Grav plugin whose non-default settings must work on a server (prod/test) or survive a plugin update.
|
||||
- Especially for plugins whose function depends on config: `api` (route/auth/cors), `admin2`, `flex-objects`, form/media settings, etc.
|
||||
- When auditing a fresh-install failure: check whether the "working" local config actually lives in a tracked path (`git ls-files user/config/plugins/<name>.yaml`) or was stranded in `user/plugins/<name>/`.
|
||||
|
||||
## Examples
|
||||
|
||||
- **api plugin (this project):** the functional config was moved from the untracked `user/plugins/api/api.yaml` into the tracked `user/config/plugins/api.yaml`, then `make content-push` + `make remote-fetch-content-<env>` deployed it. The JWT secret stayed in the gitignored `api-private.php`.
|
||||
- **Quick audit command:** `git -C user ls-files config/plugins/` shows exactly which plugin configs are tracked/deployable; anything you rely on that isn't listed is a latent fresh-install failure.
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md` — the config gap documented here was the *other* latent problem surfaced in that same investigation: the `api` plugin also had to be *installed* first before any config could take effect. The install gap (GPM version floor) and this config-tracking gap compounded each other on the fresh prod environment.
|
||||
- `docs/working/git-sync-notes.md` — the related third config location: on prod, Grav Admin saves config into the per-environment tree `user/env/<host>/config/`, which is *also* untracked. Same "config that doesn't reach the repo" family.
|
||||
- **CLAUDE.md §0 (plugin-management model):** only `pages/`, `config/`, `accounts/`, `themes/` are tracked in the `user/` repo; `plugins/` and `data/` are gitignored and GPM-managed. That tracking boundary is exactly why functional config must live under `config/plugins/`, not in the plugin's own folder.
|
||||
@@ -91,4 +91,4 @@ This is one of three independent gotchas from the same **2026-07-04 Grav 2.0.4 p
|
||||
- `docs/solutions/integration-issues/grav-double-content-encoding-garbage-page.md` — sibling: garbage-rendered pages from a double `Content-Encoding` header on a non-FastCGI host. Different root cause (HTTP compression), same deploy.
|
||||
- `docs/solutions/test-failures/new-user-grants-api-not-admin-on-admin2.md` — sibling: an authenticated account is denied an admin-gated page because `login new-user` auto-detect granted `api.*` but not `admin.*`. Different root cause (permission provisioning), same admin2/api area.
|
||||
- `docs/working/plans/2026-07-04-grav-2.0.4-upgrade.md` — the upgrade plan whose Global Constraints spell out the GPM version floors (`grav >=2.0.4`, `api >=1.0.6`) that cause the "package not found" on an rc core.
|
||||
- **api plugin config** in the tracked `user/config/plugins/api.yaml` (`enabled` / `route` / `session_enabled`) — must ship on a clean clone; necessary but not sufficient (the plugin must be installed first). See CLAUDE.md §0 "Plugin management".
|
||||
- `docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md` — the *other* latent problem from this same investigation: the api plugin's functional config (`enabled` / `route` / `session_enabled`) must live in the tracked `user/config/plugins/api.yaml` to deploy at all. Necessary but not sufficient here (the plugin must be installed first), but a durable convention in its own right.
|
||||
|
||||
Reference in New Issue
Block a user