Skip to content

feat(content-freshness): a report that links to the editor, a Freshness panel, settings per collection, German - #13

Merged
danielmlr merged 9 commits into
mainfrom
feat/content-freshness-phase2
Sep 25, 2026
Merged

danielmlr merged 9 commits into
mainfrom
feat/content-freshness-phase2

Conversation

@danielmlr

Copy link
Copy Markdown
Member

Phase 2 of the content-freshness plan: the tools editors work with. The package stays "private": true, so there's no changeset and nothing gets published.

What changes for editors

  • The report as sections.
    • Each entry is named by its title, collection and, on multilingual sites, language.
    • Each entry has an Open link straight into its editor. Block Kit table cells can only hold text, which is why the table is gone.
    • Priority and findings follow on a line below.
    • Filters for collection and priority, and paging at 25 entries.
    • An action only carries its own value, so every control carries the whole view: a filter keeps the other filter, and paging keeps both.
  • A Freshness panel in the entry editor.
    • It shows the entry's findings and re-checks the entry each time it opens. That way a finding fixed with a plain save, which no content hook reports, disappears as soon as someone looks at the entry again.
    • Its route requires content:edit_own, so authors see their own entries.
  • Setting findings aside, from the panel.
    • Mark as reviewed holds back a stale entry or a forgotten draft until its threshold runs out again. A still-correct imprint doesn't need an edit just to quiet the report.
    • Ignore holds back a description finding for good.
    • A missed schedule can't be set aside.
    • Undo brings the finding back.
  • Settings per collection. A collection can set its own months for stale entries and forgotten drafts: empty means the site default, 0 means never. It can also be left out of the audit entirely; its findings then disappear with the next sweep.
  • Widget: "N of them are yours".
  • German. The admin follows the administrator's language, in EmDash's own German terms. The catalogue is typed against the English keys, so a missing translation fails the typecheck.

Bug fixed along the way (1fa6e01)

In-process on D1, a run with 99 or more entries or cleanup rows failed. Storage's getMany/deleteMany bind the plugin ID and collection plus every ID in a single IN (...), and D1 allows 100 parameters per statement. Phase 1 deleted in batches of 100. Page and batch size are now capped at 98, which keeps the call budget unchanged. The test host runs plugins sandboxed, where deleteMany deletes row by row; that's why no test caught it.

Also: a sweep with no collection left to audit now still runs its cleanup. Before, a site whose only collection was left out kept that collection's findings.

Code review

/code-review high ran on the finished branch before this PR. It found 10 issues.

Fixed in fix(content-freshness): findings from the review …, each with a test that I confirmed fails when the fix is removed:

  1. A per-collection threshold couldn't be cleared. The admin drops an emptied number field from the submission entirely. For every collection the form showed, a missing field now means "emptied".
  2. An override that isn't a number became 0 and so switched the rule off. It's now dropped.
  3. A filter on a deleted collection produced a select without a matching option, and the host rejected the report. It now falls back to all collections.
  4. Every panel reload wrote a row. EmDash reloads an open panel after every save, autosaves included. A check now reads the row first and writes only on change.
  5. Set-aside findings were never removed. Permanent deletion now removes them; trashing keeps them, so a restore brings them back. An expired review is dropped at the next check.
  6. Without schema:read, a sweep would have removed every row. In that case no sweep starts now.
  7. The widget counted "yours" even when there was nothing to show. It no longer does.
  8. "Scheduled for X but still scheduled." said nothing. It's replaced with "Scheduled to go live on X, but it never did."
  9. The sweep's budget comment was out of date. It's updated.

Not changed: the race between a sweep's read of the set-aside findings and a set-aside click in that same moment. The window is milliseconds, and the next panel open or sweep corrects it.

Verified

  • 99 tests, emdash-plugin validate, typecheck and build. The bundle is 25.7 KB, most of the increase being the German catalogue.

  • scripts/compat-matrix.sh 0.39.0 0.39.1 0.40.0 on top of the 0.40.1 baseline: content-freshness 99 and analytics 312 on every version.

  • Bridge calls per invocation (limit 10, the budget test checks each):

    Invocation Calls
    Run that starts a sweep 9
    Follow-up 8
    Cleanup 6
    Content hook 5
    Panel 5
    First dashboard view 8
    First report view 9
    Paging, Audit now, saving settings at most 7
  • Not done: screenshots and a live run. Both belong to the release phase (plan Phase 4), together with the README screenshots.

In the test host an admin locale of de-AT arrives as English, because the host resolves locales against the admin's own list. langOf itself handles de-AT.

Block Kit table cells hold text only, so an editor reading "about" in the
old table had to find the entry by hand. Each entry is now a section named
after its title, collection and, on a multilingual site, language, with an
"Open" link into the entry editor, followed by its priority and findings.

Filters for collection and priority, and paging at 25 entries. A block
action carries only its own value, so every control carries the whole view:
a filter keeps the other one, and paging keeps both. The widget links to
the report.

All text comes from a message catalogue, English for now, and rows store
the entry's title and author for the report and the widget.
In-process, storage's `getMany` and `deleteMany` put every id into one
`IN (...)` next to the plugin id and the collection name, and D1 binds at
most 100 parameters per statement; the sandbox bridge does the same for
`getMany`. The cleanup deleted rows in batches of 100, and "Entries per
run" allowed 100, so on a D1 site running the plugin in-process a cleanup
with 99 or more stale rows, or a page of 99 or more clean entries, failed.

Both stop at 98 now. Splitting the calls instead would spend bridge calls
the sandbox budget does not have. The test host runs plugins sandboxed,
where `deleteMany` deletes row by row, which is why no test caught this.
…etting findings aside

The panel lists the entry's findings and checks the entry each time it
opens, so a finding fixed with a plain save, which no content hook
reports, leaves the report as soon as anyone looks at the entry. Its route
needs `content:edit_own`: authors see their own entries' findings, the
report stays at editor and above.

From the panel a finding can be set aside. "Mark as reviewed" holds a
stale entry or a forgotten draft back until its threshold runs out again,
so an imprint that is still correct needs no edit to quiet the report.
"Ignore" holds a description finding back for good. A missed schedule
cannot be set aside. "Undo" brings a finding back. Set-aside findings live
in a `dismissals` collection keyed like the finding rows, so a sweep reads
a page's worth in one call; the content hooks and the panel share one
check of an entry.
…eft out

News goes stale sooner than a legal page, and testimonials never do. Each
collection can set its own months for stale entries and forgotten drafts
at the end of the settings form; an empty field keeps the site's value and
0 switches the rule off. A collection can also be left out of the audit:
its rows go with the next sweep's cleanup, and the Freshness panel of its
entries says it is left out. A review's "checked again from" date follows
the collection's own threshold.

A sweep now also runs when no collection is left to audit, so its cleanup
still removes the rows of collections that were left out; before, a site
whose only collection was left out kept its findings forever.
Next to the counts by priority, the dashboard card says how many of the entries that need attention the viewer wrote, read from the row's author against the signed-in user; no users:read needed.
Every Block Kit string reads German when the administrator's locale is German, using EmDash's own German terms (Kollektion, Eintrag, Entwurf, URL-Muster). The catalogue is typed against the English keys, so a missing translation fails the typecheck. In-process installs get the locale on the editor panel today and on pages and widgets once EmDash passes it there too.
… report

- A per-collection threshold could not be cleared: the admin drops an
  emptied number field from the submission, so the key never arrived.
  For every collection the form showed, a missing month field now means
  the field was emptied.
- An override that is not a number became 0, which switches the rule
  off. It is dropped instead.
- A filter on a collection deleted since the page was shown produced a
  select without a matching option, and the host rejected the report.
  It falls back to all collections.
- EmDash reloads an open editor panel after every save, autosaves
  included, and each reload wrote the entry's row. A check now reads the
  row and writes only when it changed.
- Set-aside findings were never removed. Deleting an entry for good
  removes them (trashing keeps them for a restore), and a review that
  ran out is dropped the next time the entry is checked.
- Without `schema:read` the collection list is empty, and a sweep over
  no collections would have cleaned up every row. No sweep starts then.
- The widget counted the viewer's entries even when there was nothing
  to show.
- "Scheduled for X but still scheduled." now reads "Scheduled to go live
  on X, but it never did.", in both languages.
- The sweep's budget comment names every call again.
@changeset-bot

changeset-bot Bot commented Sep 25, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6d4f25d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@danielmlr
danielmlr merged commit 8aa05f6 into main Sep 25, 2026
2 checks passed
@danielmlr
danielmlr deleted the feat/content-freshness-phase2 branch September 25, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant