## Why
The build derived `<title>` from the **sidebar label** and the
description from the **first paragraph clipped to 220 characters**. Both
are the wrong source. A sidebar label is written for a 240px-wide nav
that supplies its own context; a search result has none.
On production that produced:
- **7 pages titled "Overview"**, and 32 pages sharing a title with
another page
- **109 of 193 descriptions cut off mid-word**, and 174 over 160
characters (where Google truncates)
```
/guides/welcome/key-concepts/
BEFORE (220): ...and if you misunderstand them early, the product wi
```
Near-identical titles are the weak-differentiation signal behind
"crawled — currently not indexed", which is the exact symptom on this
property.
## What changed
`seo_title` and `seo_description` frontmatter on **every one of the 192
pages**, written by hand against each page's real content and optimised
for the click — lead with what the reader can do, name the concrete
things covered, no keyword stuffing, no repeating the title.
| | Before | After |
|---|---|---|
| Duplicate titles | 32 pages | **0** |
| Duplicate descriptions | — | **0** |
| Descriptions cut mid-word | 109 | **0** |
| Descriptions over 160 chars | 174 | **0** |
| Title length (incl. suffix) | up to "Overview" | **26–60** |
| Description length | 31–240 | **122–158** |
**No prose was rewritten.** I measured content overlap between the
colliding pairs first — only 17–23%, so these were always distinct pages
wearing the same label, not duplicate content. The `docs/` diff is **564
insertions, zero deletions**.
### Examples
```
BEFORE — all seven identical: "Overview | Paperclip Docs"
AFTER: "Adapters Overview" "API Overview" "CLI Overview"
"Deployment Overview" "Bundled Skills" "Optional Skills"
"Experimental Features"
BEFORE — both identical: "Issues | Paperclip Docs"
AFTER: "Issues: Creating and Tracking Work" /guides/day-to-day/issues/
"Issues API" /reference/api/issues/
```
```
/guides/getting-started/five-minute-path/
BEFORE (220): The shortest route from a running Paperclip instance to an agent that has
just completed work for you. After Paperclip is installed, the path takes
about 5 minutes: create a company, hire a CEO agent, approve its first s
AFTER (131): Install Paperclip, create a company, hire a CEO agent, and approve its
first strategy — a working AI company in about five minutes.
```
## Plumbing
- **`site/build-release.mjs`** prefers the authored fields; the derived
path stays only as a fallback the CI gate prevents ever reaching.
`<title>`, `<meta name="description">`, and the `TechArticle` JSON-LD
all read from the same source.
- **`site/app.js`** uses the same fields, so a client-side transition
cannot leave a different title on screen than the crawler was served.
Verified in-browser: navigating `/guides/day-to-day/issues/` →
`/reference/api/issues/` swaps to exactly the server-rendered values, no
JS errors.
- **`scripts/verify-seo-metadata.mjs`** (new) enforces presence,
uniqueness, length, complete-sentence endings, and the
frontmatter-safety rules. Wired into `docs:test` and CI as its own step.
I negative-tested the gate rather than assuming it works — it correctly
fails on a duplicate title, a mid-word description, and a missing field,
and passes again once restored.
## Automating it going forward
`skills/sync-docs/SKILL.md` now:
- gains an **"Authored SEO metadata"** section: why the fields exist,
the enforced rules as a table, and how to actually write them (title
names the subject not the nav position; description writes for the
click; never generate mechanically from the first paragraph — *that is
the fallback the gate exists to prevent*).
- requires both fields in the **page-authoring instruction** given to
every subagent, with guidance on when to update an existing description
versus leave it.
- runs `docs:test:seo-metadata` and `docs:test:crawlable-links` as
**fail-loud gates in Phase 7** before commit.
- documents all six build gates in the files/gates section.
So once this lands, a new page cannot ship without hand-written
metadata, and a title collision cannot reach production.
## Verified
All six verifiers pass:
```
Static route verification passed
Asset fingerprint verification passed
Hierarchical Skills nav verification passed
Skill source block verification passed (11 skill pages, 1 control page)
Crawlable-link contract passed: 40886 internal links across 194 pages resolve in one hop
Authored-SEO contract passed: 192 pages carry a unique, hand-written seo_title and seo_description
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
7.8 KiB
paperclip_version: v2026.525.0 seo_title: Workspace Diff Viewer Plugin seo_description: See exactly what agents changed inside a workspace: files moved and added, the staged-versus-unstaged split, and the line-level patch.
Workspace diff viewer
If you want to see what your agents actually changed inside a workspace — which files moved, what got added, the staged-versus-unstaged split, the line-level patch — without leaving the Paperclip dashboard, the @paperclipai/plugin-workspace-diff plugin is the surface that does it. It adds a Changes tab to execution and project workspaces, computes the diff locally with Git on the host running Paperclip, and renders it with @pierre/diffs.
This page is for operators and developers who want to enable, configure, or understand the limits of that Changes tab.
The Workspace diff plugin is in alpha alongside the plugin runtime itself. Expect breaking changes between Paperclip releases and pin your version when you depend on it.
Quick install
The plugin ships as a published npm package, so any Paperclip install (Docker, systemd, bare-metal — all the same) can pull it in with one command from a host that has your Paperclip CLI configured:
paperclipai plugin install @paperclipai/plugin-workspace-diff
Or from the dashboard: Settings → Plugins → Install Plugin, paste @paperclipai/plugin-workspace-diff, submit.
The plugin won't appear in paperclipai plugin examples — that command only lists the four built-in reference example plugins, not the full catalogue of first-party plugins. Install Workspace diff viewer by full package name.
If you're running from a monorepo checkout, see Develop a plugin locally and point the installer at packages/plugins/plugin-workspace-diff instead.
On first enable the plugin asks for a small, focused capability set: ui.detailTab.register, execution.workspaces.read, and project.workspaces.read. It never writes to the workspace — diffs are read-only — and it doesn't own its own database namespace, agents, or routines. Review the Permissions card on the plugin detail page before approving.
What you get
Once the plugin is installed and enabled, one thing shows up in the Paperclip UI:
- A Changes tab on every execution workspace and project workspace detail page (slot id
workspace-changes-tab, slot typedetailTab, order25, entity typesexecution_workspaceandproject_workspace). Open any workspace and the tab is right there in the row of detail tabs.
The tab name is Changes (the plugin's displayName is "Workspace Changes"). Inside, you get the familiar file-tree-plus-patch layout: a list of changed files with their Git status (added, modified, deleted, renamed, copied, type-changed, untracked, unknown), and a per-file patch view that splits staged, unstaged, head, and untracked content.
Behind the scenes the plugin runs Git directly against the workspace path. There's no background worker, no managed agent, no routines, no migrations — just a small data endpoint the Changes tab calls when you open it.
How the diff is computed
The plugin's worker exposes a single data endpoint (workspace-diff) that the Changes tab calls with the workspace id and a query. The worker:
- Looks up the workspace through the SDK's
executionWorkspaces.getorprojects.listWorkspaces, depending on entity type. - Resolves a base ref — either the workspace's own
baseRef, or, for execution workspaces, the parent project workspace'sdefaultRef/repoRef. You can also pass an explicitbaseRefin the query. - Confirms the workspace
cwdis an absolute, real directory inside a Git repository (withO_NOFOLLOWchecks andgit rev-parse --show-toplevel). - Runs
gitwith a 10 second timeout and a 2 MiB list buffer to enumerate changed files and produce patches.
Two views are supported via the view query parameter:
working-tree(the default) — staged + unstaged + untracked changes relative toHEAD.head— whatHEADlooks like compared to the resolved base ref.
If you ask for the head view without supplying a base ref, the Changes tab falls back to working-tree rather than failing, so the diff always renders even when the base hasn't been picked yet.
You can also pass paths / path to limit the diff to specific files, and includeUntracked (default true) to toggle untracked-file inclusion.
Controls in the Changes tab
The tab's toolbar gives you two toggles and a base-ref input:
- Split / Unified — switch between side-by-side and inline patch rendering. Split is the default.
- Working tree / Against ref — pick
working-tree(default) to inspect uncommitted changes, or Against ref to diffHEADagainst the resolved base ref. - Base ref — the input next to the view toggle (shown when Against ref is selected). If you leave it blank, the tab prefills the workspace's
defaultBaseRef(resolved by the worker) and flips the view to Against ref for you, so common cases just work without any toggling. Once you edit either control by hand, your choice sticks for the session.
The toolbar stays sticky at the top of the tab while you scroll, so the toggles and the base-ref input remain in reach on large diffs.
Limits worth knowing
The plugin enforces hard caps so a runaway diff can't lock up your dashboard. The defaults (from WORKSPACE_DIFF_CAPS) are:
| Cap | Default |
|---|---|
maxFiles |
200 changed files |
maxFileBytes |
512 KiB per file (binary / oversized files are skipped) |
maxPatchBytes |
256 KiB per file patch |
maxTotalPatchBytes |
1 MiB total patch payload |
When a cap is hit you don't get a failure — you get a structured warning alongside whatever the plugin could compute. The warning codes you may see:
base_ref_missing,base_ref_invalid— the resolved base ref couldn't be used.binary_file,file_oversized— file skipped from inline patch rendering.file_count_truncated,patch_truncated— total budget exceeded.git_command_failed— Git itself returned non-zero (the stderr is included).missing_cwd,non_git_workspace,workspace_path_invalid— the workspace path isn't usable.path_filter_invalid,symlink_target_outside_workspace— path-safety guard tripped.
These warnings render in the Changes tab so operators can see exactly why something is missing instead of staring at an empty list.
Tips and common use cases
- Use it for agent code review. When an agent finishes work in an execution workspace, open the Changes tab to see the patch before you merge or hand off — same flow you'd use in a code-review UI, without leaving Paperclip.
- Set a
baseRefon your project workspace. The plugin falls back to the project workspace'sdefaultRefthenrepoRef, so configuring those once means every execution workspace under that project gets a sensible diff baseline for free. - The workspace must be a real Git repo. Non-Git workspaces are not supported — you'll get a
non_git_workspacewarning. If you keep an execution workspace as a plain folder, the Changes tab will tell you to either initialise Git or skip the plugin for that workspace. - The plugin only reads. It never stages, commits, resets, or otherwise touches your repository. If you need to act on a diff, do it from the host shell or from another plugin.
Related
- Administration → Plugins — the operator-facing Plugin Manager.
- How-to → Develop a plugin locally — point Paperclip at a local checkout of the plugin.
- Reference → Plugin SDK — the authoring surface, if you want to extend the diff viewer or write your own detail-tab plugin.