agentleFS
hyperlink-academyhyperlink-academy/leaflet/.claude/skills/publication-settings/SKILL.md

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.

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
  1. Distinctive:

    Every arrow except the first is a hand-maintained explicit field list.

    line 21
  2. Distinctive:

    Not for the Monetization / Contributors / Newsletter tabs — those render their own panes and don't go through the shared form or the …

    line 31
  3. Very specific:

    Run npm run lexgen. This regenerates lexicons/pub/leaflet/*.json and the types in lexicons/api/.

    line 58
  4. Very specific:

    Add the field in both buildLeafletRecord and buildStandardRecord.

    line 74
  5. Very specific:

    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 …

    line 94
  6. Very specific:

    Leave the sync-from-server useEffect alone unless you have a reason.

    line 98
  7. Very specific:

    Per-post overridable?: Add it to src/utils/mergePreferences.ts — document prefs win over publication prefs there.

    line 119
  8. Very specific:

    Guard the submit: only include the field when it isn't null (...(recommendations !== null && { recommendations })).

    line 137
  9. Very specific:

    Run npx tsc. Then, in the app, for the setting you touched:

    line 150

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 page

Every 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 → the preferences object. Give it a default.
  • lexicons/site/standard/publication.json → the mirrored preferences object. This file is hand-maintained, not generated: lexicons/build.ts only writes lexicons/pub/leaflet/. site.standard is a shared external standard, so editing it is a deliberate act — but the two record shapes are converted into each other by normalizePublication, so a field present in only one is lost for pubs of the other type.
  • Run npm run lexgen. This regenerates lexicons/pub/leaflet/*.json and the types in lexicons/api/. Skipping it means PubLeafletPublication.Preferences — the type on updatePublication's preferences param — 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

  1. useState initializer, using the same default as the lexicon: record?.preferences?.showX === undefined ? true : record.preferences.showX. The explicit undefined check is deliberate — ??/|| on a boolean would flip a saved false back to the default.
  2. hasUnsavedChanges: compare against the saved value and add the state to the useMemo dependency array. Miss either and the footer never appears, so the user can't save at all.
  3. The submit payload's preferences object (see the wholesale-replace note above).
  4. 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 (like showPrevNext) read straight from the publication side.
  • Published page: thread it through the preferences prop types in PostPages.tsx / LinearDocumentPage.tsx / CanvasPage.tsx down to the component.
  • Server-side filtering (a showInDiscover-shaped flag): the feed queries in feeds/index.ts, actions/reader/getNewFeed.ts, actions/reader/getHotFeed.ts, and app/api/rpc/[command]/get_profile_data.ts each 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 and theme-settings/PostPreview.tsx read 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 as T | null where null means "not loaded yet". Sync null → loaded in a useEffect.
  • 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.
  • hasUnsavedChanges compares local state against the separately loaded saved value, not against the publication record.
  • On success, mutate that SWR key with the new value and { revalidate: false } — the publication record's mutate() 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:

  1. Change the control → the "You have unsaved updates!" footer appears.
  2. 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).
  3. Reload the settings page → the control still shows the new value.
  4. Load a published post / the relevant surface → the behavior actually changed.
  5. Change it back and repeat, especially for booleans — the === undefined default handling is where a saved false gets lost.

Step 3 catches the normalize/buildRecord misses; step 4 catches "saved but never read."

Failure symptom → cause

SymptomLook at
Footer never appears when the control changeshasUnsavedChanges — the comparison or its dep array
Saves, but reverts on reloadnormalizePublication, then buildPublicationRecord
Footer stays open after a successful savesame — read-back doesn't match local state
Works for one pub, not anotheronly one of buildLeafletRecord / buildStandardRecord, or only one lexicon
Saves and persists, but nothing changes on the pageconsumer not wired / mergePreferences
Another setting got reset when this one savedsubmit payload isn't sending a complete preferences object
New pubs don't have itcreatePublication.ts
Wiped when saved quickly after page loadtype-B null guard in the submit payload

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.