publication-settings
Skill527 starsChanged 4 months agoMITView on GitHub
Wire a setting in the General tab of publication settings end to end, so the change marks the form dirty, survives the Save button, and actually applies. Use whenever you add a new setting or change an existing one under dashboard/settings — the value passes through several hand-maintained field lists and silently drops out of any one you forget.
In this repository12 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- blueprint-generateSkill
- blueprintSkill
- domain-settingsSkill
- leaflet-ai-apiSkill
- lexiconsSkill
- mirror-editor-block-stylesSkill
- notificationsSkill
- specSkill
- tests-postsSkill
- tests-subscribeSkill
- update-default-themeSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 2 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 21
Every arrow except the first is a hand-maintained explicit field list.
- Distinctive: line 31
Not for the Monetization / Contributors / Newsletter tabs — those render their own panes and don't go through the shared form or the …
- Very specific: line 58
Run npm run lexgen. This regenerates lexicons/pub/leaflet/*.json and the types in lexicons/api/.
- Very specific: line 74
Add the field in both buildLeafletRecord and buildStandardRecord.
- Very specific: line 94
If the setting carries transient state that isn't part of the record (a File, a "removed" flag), reset it in the success branch of the …
- Very specific: line 98
Leave the sync-from-server useEffect alone unless you have a reason.
- Very specific: line 119
Per-post overridable?: Add it to src/utils/mergePreferences.ts — document prefs win over publication prefs there.
- Very specific: line 137
Guard the submit: only include the field when it isn't null (...(recommendations !== null && { recommendations })).
- Very specific: line 150
Run npx tsc. Then, in the app, for the setting you touched:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Publication General Settings
A setting in the General tab is not done when the control renders. The value has to survive a round trip:
local state in SettingsContent
→ hasUnsavedChanges (gates the footer)
→ form submit → updatePublication()
→ buildRecord() → putRecord to the PDS + publications.record in supabase
→ normalizePublication() on read
→ back into SettingsContent's initial state, and into the published pageEvery arrow except the first is a hand-maintained explicit field list. None of them spread. A field missing from any one is dropped with no type error and no runtime error — the toggle just doesn't stick, or sticks but does nothing.
When to use
Adding a setting to, or changing a setting in, the General tab
(app/(app)/(identity)/lish/[did]/[publication]/dashboard/settings/) — GeneralSettings,
ShareSettings, ThemeAndLayoutSettings, RecommendedPubsSetting, or a new section.
Not for the Monetization / Contributors / Newsletter tabs — those render their own panes and don't go through the shared form or the footer's save button.
Two kinds of setting
A. Backed by the publication record (every preferences.* flag, name, description,
icon). Saved by updatePublication, written into the pub's AT Proto record.
B. Backed by its own record or table (recommendations). Loads on its own SWR key, saves through its own write inside the same submit.
Most new settings are A. If the setting needs its own lexicon record, it's B — read
RecommendedPubsSetting.tsx and writeRecommendations in updatePublication.ts as the
worked example, then follow section B's extra steps on top of the SettingsContent wiring.
A. Checklist for a record-backed preference
Work top to bottom. Each step names what breaks if you skip it.
1. Lexicon — both of them
lexicons/src/publication.ts→ thepreferencesobject. Give it adefault.lexicons/site/standard/publication.json→ the mirroredpreferencesobject. This file is hand-maintained, not generated:lexicons/build.tsonly writeslexicons/pub/leaflet/.site.standardis a shared external standard, so editing it is a deliberate act — but the two record shapes are converted into each other bynormalizePublication, so a field present in only one is lost for pubs of the other type.- Run
npm run lexgen. This regenerateslexicons/pub/leaflet/*.jsonand the types inlexicons/api/. Skipping it meansPubLeafletPublication.Preferences— the type onupdatePublication'spreferencesparam — won't have your field, and the call site won't compile.
2. lexicons/src/normalize.ts → normalizePublication
The preferences object is rebuilt field by field when converting a
pub.leaflet.publication record into the normalized shape. This is the most common
miss. A field absent here writes to the PDS fine and then vanishes on read: the
setting appears to save (toast fires) and then snaps back to the default on reload, and
the unsaved-changes footer may stay stuck open because the read-back never matches local
state.
3. src/utils/buildPublicationRecord.ts
Add the field in both buildLeafletRecord and buildStandardRecord. Missing from
one, and the setting works for one publication type and silently no-ops for the other.
Note how preferences merge: overrides.preferences ?? normalizedPub?.preferences. The
override replaces the stored object wholesale — it is not a per-field merge. So the
submit handler must always send a complete preferences object; omitting one field from
the payload erases its saved value rather than leaving it alone.
4. SettingsContent.tsx — four separate places
useStateinitializer, using the same default as the lexicon:record?.preferences?.showX === undefined ? true : record.preferences.showX. The explicitundefinedcheck is deliberate —??/||on a boolean would flip a savedfalseback to the default.hasUnsavedChanges: compare against the saved value and add the state to theuseMemodependency array. Miss either and the footer never appears, so the user can't save at all.- The submit payload's
preferencesobject (see the wholesale-replace note above). - Pass state + setter down to the section component.
If the setting carries transient state that isn't part of the record (a File, a
"removed" flag), reset it in the success branch of the submit handler the way
setIconFile(null) / setIconRemoved(false) do.
Leave the sync-from-server useEffect alone unless you have a reason. It deliberately
re-syncs only name/description/icon; a background SWR revalidation that re-synced
preference state would clobber edits in progress. (That's exactly why icon removal is
tracked by a separate iconRemoved flag instead of by clearing the preview.)
5. The section component
GeneralSettings / ShareSettings / ThemeAndLayoutSettings are controlled and
stateless — value in, setter in, no local state, no saving of their own. Use
ToggleSetting / InputSetting from components/SettingsLayout. Keep it that way:
state lives in SettingsContent because that's what the footer and the submit read.
6. New publications — app/(app)/(identity)/lish/createPub/createPublication.ts
Its preferences object is also an explicit list. Skip it and existing pubs get the
setting while newly created ones silently don't.
7. Actually apply it
The point of the setting. Find the consumer and wire it:
- Per-post overridable? Add it to
src/utils/mergePreferences.ts— document prefs win over publication prefs there. Publication-only settings (likeshowPrevNext) read straight from the publication side. - Published page: thread it through the
preferencesprop types inPostPages.tsx/LinearDocumentPage.tsx/CanvasPage.tsxdown to the component. - Server-side filtering (a
showInDiscover-shaped flag): the feed queries infeeds/index.ts,actions/reader/getNewFeed.ts,actions/reader/getHotFeed.ts, andapp/api/rpc/[command]/get_profile_data.tseach filter on the raw JSON path and each treat "absent" as the default. Add the same null-tolerant condition to all of them. - Settings previews:
ThemeAndLayoutSettings's inline preview andtheme-settings/PostPreview.tsxread preferences too.
B. Extra steps for a setting with its own record
On top of the SettingsContent wiring above:
- Load it on its own SWR key with
revalidateOnFocus: false, and hold local state asT | nullwherenullmeans "not loaded yet". Syncnull → loadedin auseEffect. - Guard the submit: only include the field when it isn't
null(...(recommendations !== null && { recommendations })). Without this, hitting save before the load finishes wipes the stored value. hasUnsavedChangescompares local state against the separately loaded saved value, not against the publication record.- On success,
mutatethat SWR key with the new value and{ revalidate: false }— the publication record'smutate()won't refresh it. - Server side: write the record and mirror it into supabase the way the appview would index it from the firehose, so the row shape matches what the firehose consumer produces. Empty list = delete the record.
Verify before calling it done
Run npx tsc. Then, in the app, for the setting you touched:
- Change the control → the "You have unsaved updates!" footer appears.
- Hit Update Pub → success toast, footer disappears (it disappears because the read-back now matches local state — if it lingers, step 2 or 3 above is wrong).
- Reload the settings page → the control still shows the new value.
- Load a published post / the relevant surface → the behavior actually changed.
- Change it back and repeat, especially for booleans — the
=== undefineddefault handling is where a savedfalsegets lost.
Step 3 catches the normalize/buildRecord misses; step 4 catches "saved but never read."
Failure symptom → cause
| Symptom | Look at |
| Footer never appears when the control changes | hasUnsavedChanges — the comparison or its dep array |
| Saves, but reverts on reload | normalizePublication, then buildPublicationRecord |
| Footer stays open after a successful save | same — read-back doesn't match local state |
| Works for one pub, not another | only one of buildLeafletRecord / buildStandardRecord, or only one lexicon |
| Saves and persists, but nothing changes on the page | consumer not wired / mergePreferences |
| Another setting got reset when this one saved | submit payload isn't sending a complete preferences object |
| New pubs don't have it | createPublication.ts |
| Wiped when saved quickly after page load | type-B null guard in the submit payload |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

