Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ pnpm build
Versions come from changesets: add one with `pnpm changeset` for every
change to a published package, written for someone upgrading.

A package that has not been released yet stays `"private": true`. The release
workflow publishes every public package whose version is missing from npm,
changeset or not, so a public package would go out with its first merge to
`main`. The release PR removes the flag and adds the package's first
changeset. That first publish is manual (`pnpm publish` in the package):
npm's trusted-publisher setting can only be added to a package that exists.

Never name a package script `publish`, `version` or `prepare`: npm and pnpm
run scripts with those names on their own during a publish or a version
bump. `emdash-plugin init` generates `"publish": "emdash-plugin publish"`,
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Plugins for [EmDash](https://github.com/emdash-cms/emdash) by [Eisbachcode](http
| Package | What it does |
|---|---|
| [`@eisbachcode/emdash-plugin-analytics`](packages/analytics) | Cloudflare Web Analytics on the EmDash dashboard and next to your content: traffic per entry, a setup check, German and English |
| [`@eisbachcode/emdash-plugin-content-freshness`](packages/content-freshness) | A scheduled audit of your content: stale entries, missing SEO descriptions, forgotten drafts, schedules that never fired. Read-only, no network |

## Development

Expand Down
1 change: 1 addition & 0 deletions packages/content-freshness/.agents/skills
1 change: 1 addition & 0 deletions packages/content-freshness/.claude/CLAUDE.md
1 change: 1 addition & 0 deletions packages/content-freshness/.claude/skills
2 changes: 2 additions & 0 deletions packages/content-freshness/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
14 changes: 14 additions & 0 deletions packages/content-freshness/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Agent instructions

Before editing this plugin, read `skills/creating-plugins/SKILL.md` completely. Codex discovers the same directory through `.agents/skills`; Claude discovers it through `.claude/skills` and reads these instructions through `.claude/CLAUDE.md`.
Keep `emdash-plugin.jsonc` aligned with the runtime implementation, declare every capability and host the plugin uses, and run the generated validation, typecheck, test, and build scripts after changes.

## Toolchain

`emdash` is a peer with a floor and no ceiling (`>=0.39.0`, the same as
`env:emdash` in the manifest); the dev dependency stays below the next
major until the toolchain is moved on purpose. Built with
`@emdash-cms/plugin-cli@0.12.0` and `@emdash-cms/plugin-test@0.2.1`;
`scripts/compat-matrix.sh` at the repository root runs the suite against
later EmDash releases. A plain `pnpm install` at the workspace root is
enough.
100 changes: 100 additions & 0 deletions packages/content-freshness/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# content-freshness

Scheduled freshness audit for [EmDash](https://emdashcms.com) content. Finds the
entries that quietly went stale and puts them in one list in the admin.

What it looks for:

| Finding | Priority |
| --- | --- |
| Scheduled to publish, the date passed, still not published | urgent |
| Published without an SEO description | should fix; nice to fix when the entry has an excerpt-like field |
| Published and untouched for longer than your threshold | should fix |
| SEO description too short or too long | nice to fix |
| Draft abandoned for longer than your threshold | nice to fix |

Many sites render a description of their own when the SEO panel is empty,
usually from an excerpt or intro field. An entry with a non-empty `excerpt`,
`description`, `summary`, `intro`, `teaser`, `lead`, `subtitle`, or a field
ending in `_excerpt`, `_description`, `_summary` or `_subheadline` therefore
gets the lower priority, and the finding names the field. A site whose
templates always render a description can switch the check off.

A fixed entry drops out of the report on the next sweep — the audit stores the
current state, not a history.

Each run audits one page of one collection, and an audit carries on in
follow-up runs until it is done, one per firing of the site's cron trigger,
instead of waiting a day per collection.

The report and the dashboard widget also warn about routable collections
without a URL pattern. EmDash then links their entries as
`/{collection}/{slug}` in the sitemap and the admin, which is a 404 on any
site whose routes differ, and nothing else says so.

## Trust contract

`content:read` and `schema:read`, nothing else. Every rule is computed from
fields the content and schema APIs already return, so there is **no network
access** and no `content:write` — the plugin reports, it never edits an entry.

## Install

Config-based, on any EmDash site and any plan:

```js
// astro.config.mjs
import contentFreshness from "@eisbachcode/emdash-plugin-content-freshness";

emdash({ plugins: [contentFreshness] });
```

Or under `sandboxed: []` if the site has a sandbox runner configured.

Then activate it once under **Admin → Extensions**. Every collection on the
site is audited.

**The site needs a cron trigger.** On Cloudflare that is `"crons"` in
`wrangler.jsonc` plus `scheduled: createScheduledHandler()` on the Worker
entry. Without one, the audit is scheduled and never runs.

## Settings

| Setting | Default | |
| --- | --- | --- |
| Stale after | 12 months | For published entries. |
| Draft forgotten after | 6 months | For drafts. |
| SEO description | 50–160 characters | Outside this range is a low-priority finding. |
| Report entries without an SEO description | on | Off for a site whose templates always render a description. The length check still runs. |
| Entries per run | 50 | One collection list, one `content.list` and two batched storage calls per run, whatever the page size. |
| Schedule | `0 4 * * *` | Any cron expression. |

## Develop

```sh
pnpm install
pnpm test
pnpm typecheck
pnpm build
```

## Compatibility

| EmDash | Plugin test suite |
| ------ | ----------------- |
| 0.39.1 | 38 passed |
| 0.39.0 | 38 passed |

Declared range: `emdash >=0.39.0 <1.0.0`. The table covers **every released
version inside it** — 0.39.0 and 0.39.1 are the only ones published, so this is
the whole range rather than a sample. 0.39.0 is the floor because the plugin
lists collections through `schema:read`, which earlier versions do not have.

Reproduce with `scripts/compat-matrix.sh` from the repo root. Last run
2026-09-24, when 0.39.1 was the latest EmDash release. A newer EmDash than the
table's top row is untested rather than unsupported: nothing blocks the install,
and this table is how you tell the difference.

## Licence

MIT © Eisbachcode
45 changes: 45 additions & 0 deletions packages/content-freshness/emdash-plugin.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",

"slug": "content-freshness",
"publisher": "did:plc:iucwrgdd464ddkranaa6ux3l",

"license": "MIT",
"author": { "name": "Eisbachcode", "url": "https://eisbachcode.de" },
"security": { "email": "daniel@eisbachcode.de" },
"repo": "https://github.com/eisbachcode/emdash-plugins",

// The only version check a user meets before installing. EmDash refuses
// an install whose host falls outside this range, so it carries a floor
// and deliberately no ceiling: EmDash ships a minor every 6-8 days, and
// a ceiling would block correct installs every week rather than catch a
// break. The manifest has no advisory "tested up to" field to express
// caution without blocking, so the tested range belongs in the README.
"release": {
"requires": { "env:emdash": ">=0.39.0" }
},
"description": "Audits your content for staleness: entries untouched for too long, missing or badly sized SEO descriptions, forgotten drafts, schedules that never fired, and collections without a URL pattern.",
"keywords": ["content", "audit", "seo", "maintenance", "editorial"],

// Trust contract: read content and the collection list, nothing else.
//
// No network at all — every rule is computed from fields the content
// and schema APIs already return. And no `content:write`: this plugin
// reports, it never edits an entry.
"capabilities": ["content:read", "schema:read"],
"allowedHosts": [],

"storage": {
// One row per (entry, rule). Re-running overwrites, so a fixed
// entry drops out of the report on the next sweep.
"findings": { "indexes": ["collection", "severity", "rule", "entryId"] },
},

"admin": {
"pages": [
{ "path": "/report", "label": "Content freshness", "icon": "clock" },
{ "path": "/settings", "label": "Freshness settings", "icon": "settings" },
],
"widgets": [{ "id": "summary", "title": "Needs attention", "size": "third" }],
},
}
66 changes: 66 additions & 0 deletions packages/content-freshness/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "@eisbachcode/emdash-plugin-content-freshness",
"version": "0.1.0",
"private": true,
"description": "EmDash plugin: scheduled freshness audit for your content.",
"keywords": [
"emdash",
"emdash-plugin",
"cms",
"plugin",
"content",
"audit",
"seo"
],
"license": "MIT",
"author": "Eisbachcode <daniel@eisbachcode.de>",
"homepage": "https://github.com/eisbachcode/emdash-plugins/tree/main/packages/content-freshness#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/eisbachcode/emdash-plugins.git",
"directory": "packages/content-freshness"
},
"bugs": "https://github.com/eisbachcode/emdash-plugins/issues",
"type": "module",
"main": "dist/index.mjs",
"exports": {
".": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.mts"
},
"./sandbox": "./dist/plugin.mjs"
},
"files": [
"dist",
"emdash-plugin.jsonc"
],
"scripts": {
"validate": "emdash-plugin validate",
"build": "emdash-plugin build",
"dev": "emdash-plugin dev",
"typecheck": "tsc --noEmit",
"test": "emdash-plugin validate && vitest run",
"bundle": "emdash-plugin bundle",
"registry:login": "emdash-plugin login",
"registry:publish": "emdash-plugin publish",
"release:setup": "emdash-plugin release setup",
"prepublishOnly": "pnpm run build"
},
"engines": {
"node": ">=22.16"
},
"peerDependencies": {
"emdash": ">=0.39.0"
},
"devDependencies": {
"@eisbachcode/emdash-plugin-shared": "workspace:*",
"@emdash-cms/plugin-cli": "0.12.0",
"@emdash-cms/plugin-test": "0.2.1",
"emdash": ">=0.39.0 <1.0.0",
"typescript": "^5.9.0",
"vitest": "^4.1.0"
},
"publishConfig": {
"access": "public"
}
}
29 changes: 29 additions & 0 deletions packages/content-freshness/skills/creating-plugins/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
name: creating-plugins
description: Build, test, and publish this sandboxed EmDash plugin. Use for changes to emdash-plugin.jsonc, src/plugin.ts, hooks, routes, capabilities, storage, Block Kit admin UI, bundling, or releases.
---

# Creating EmDash plugins

Read `emdash-plugin.jsonc` and `src/plugin.ts` before editing. The manifest is the identity and trust contract; the source contains runtime hooks and routes.

## Runtime rules

- Assign the runtime definition to a `SandboxedPlugin`-typed constant and export it as default from `src/plugin.ts`.
- Use Web APIs. Do not import Node.js built-ins into plugin runtime code.
- Declare every runtime API in `capabilities` and every network destination in `allowedHosts`.
- Use `ctx.storage` for queryable records and `ctx.kv` for key-value state.
- Use Block Kit for sandboxed admin UI. Do not ship browser React components.
- Treat public routes as internet-facing and validate their inputs.

## Validation

Use the package scripts in this repository. The test script builds the plugin and runs it inside workerd through EmDash's production sandbox wrapper and host bridge. Use `createPluginTestHost()` to invoke hooks and routes, create content fixtures, and inspect plugin KV or declared storage. Dispose the host after each test so its bindings reset.

Before handing off a change, run validation, typecheck, tests, and build. A release also requires a version bump in `package.json` when runtime behavior or the trust contract changes.

## Publishing

Use the local publish script for a release started from this computer. Use the release-setup script for GitHub Actions. Setup detects a root Changesets configuration and offers to follow packages released by Changesets; otherwise it uses package tags. Connect the generated reusable workflow to the existing Changesets publish job by passing its published-package output. Changesets Action v1 names the step output `publishedPackages`; v2 names it `published-packages`. Expose it as a `published-packages` job output and pass it to the generated workflow from a dependent job when Changesets reports `published == 'true'`. The first automated release connects the repository workflow; later packages reuse it only when their signed profiles name the same repository.

For complete EmDash patterns and API details, use https://docs.emdashcms.com/plugins/creating-plugins/.
Loading
Loading