## 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]>
9.4 KiB
seo_title, seo_description
| seo_title | seo_description |
|---|---|
| CLI Auth and Board Claim | Two identity flows for the command line, both built so token material never leaves your browser or CLI. Claim a board, log in by device code, rotate tokens. |
CLI Auth & Board Claim
Paperclip has two separate identity flows for humans on the command line and browser:
- Board claim — a one-time bootstrap step that promotes your browser-authenticated user to the owner of a freshly started authenticated instance. It migrates the trusted "local board" placeholder into a real, named human account.
- CLI auth — the everyday flow that pairs a local
paperclipaiCLI process with a signed-in board user, so the CLI can call/apiendpoints on your behalf without you typing credentials into your shell.
Both flows use a short-lived, server-minted challenge and a browser approval page. Both are designed so the secret material (the board API token, the ownership migration) never leaves the browser or the CLI process that asked for it.
This guide walks through both, shows the approval pages, and explains how to use the resulting tokens with the CLI.
When you need each flow
- You just installed Paperclip in authenticated deployment mode on a server, and the loopback trust is still active → run Board claim once.
- You want to run
paperclipaicommands against that instance from your laptop → run CLI auth (paperclipai auth login). - You're using trusted deployment mode (loopback-only local dev) → neither flow is required; the CLI and browser already trust the loopback.
Claiming a board from the CLI
Board claim only exists when all of these are true:
- Deployment mode is
authenticated. - The only instance admin is the built-in
local-boardplaceholder (i.e., no human has claimed the instance yet). - The claim challenge hasn't expired (the server keeps the active challenge for 24 hours and refreshes it automatically after that).
When the server starts and those conditions match, it prints a Board claim URL to the server log. It looks like:
http://localhost:3000/board-claim/<token>?code=<code>
The token is 48 hex characters; the code is 24 hex characters. Together they identify exactly one pending claim.
Open the URL in your browser. If you aren't signed in yet, the page will ask you to sign in or create an account first and bounce you back to the same URL after authentication.
Once you're signed in, the page shows a panel titled Claim Board ownership with a short explanation ("This will promote your user to instance admin and migrate company ownership access from local trusted mode.") and a single Claim ownership button.
Click it and the server does three things in a single transaction:
- Inserts an
instance_adminrole for your user (if you weren't one already). - Deletes the
local-boardplaceholder admin. - Walks every company in the database and, for each one, either creates an
active,owner-role membership for you or reactivates your existing membership and sets the role toowner.
The button swaps to Claiming… during the request. When it returns successfully the page switches to Board ownership claimed with a link to open the board. The in-memory challenge is marked claimed and cannot be reused.
If the challenge expired, or the URL is malformed, the page shows Claim challenge unavailable instead. Restart the server (or wait for the next refresh) to mint a new challenge.
Device-code flow: paperclipai auth login
For day-to-day CLI use, the flow is a device-code-style pairing. The CLI creates a challenge, opens your browser, you approve it, and the CLI finishes with a stored token.
Starting the login
From the machine you want to use the CLI on:
paperclipai auth login
The CLI:
- POSTs to
/api/cli-auth/challengeswith a description of itself (command,clientName,requestedAccess, and an optionalrequestedCompanyId). - Gets back a challenge containing
token,boardApiToken,approvalUrl,pollPath,expiresAt, and a polling interval. - Prints the approval URL to your terminal and tries to open it in your default browser.
- Starts polling the server at
pollPathuntil the challenge is approved, cancelled, or expires.
Common flags:
# Request instance-admin approval (only instance admins can approve this)
paperclipai auth login --instance-admin
# Scope to a specific company
paperclipai auth login --company-id <company-id>
# Target a non-default server
paperclipai auth login --api-base https://paperclip.example.com
Approving the challenge in the browser
Your browser lands on a page titled Approve Paperclip CLI access:
The panel shows what the CLI is asking for:
- Command — the CLI command that initiated the login (e.g.
paperclipai auth login). - Client — the
clientNamethe CLI sent, defaultpaperclipai cli. - Requested access — either
BoardorInstance admin. - Requested company — only shown if the CLI asked to be scoped to a company.
Two buttons at the bottom:
- Approve CLI access — only enabled if your signed-in user is allowed to approve this level of access. An instance-admin challenge requires an instance-admin approver.
- Cancel — rejects the challenge. The CLI sees the
cancelledstatus on its next poll and exits with an error.
If you aren't signed in, the page first shows Sign in required with a button that returns you here after authentication. If the challenge has already been approved, cancelled, or expired by the time you visit the URL, the page reflects that state instead of offering the buttons.
Finishing on the CLI
Once you click Approve, the CLI's next poll sees status approved, calls /api/cli-auth/me with the board API token to confirm identity, and writes the credential to the local credential store. It prints a success summary:
{
"ok": true,
"apiBase": "https://paperclip.example.com",
"userId": "usr_...",
"approvalUrl": "https://paperclip.example.com/cli-auth/..."
}
If you cancel or let the challenge expire, the CLI errors out with CLI auth challenge was cancelled. or CLI auth challenge expired before approval. — re-run paperclipai auth login to try again.
After claiming: using and rotating the token
Where the token lives
The CLI stores the board API token (and the user id it resolves to) in the platform-appropriate config directory, keyed by apiBase. You don't need to copy the token anywhere — every CLI command that accepts --api-base will look up the matching stored credential automatically.
Using it
Once logged in, the CLI sends Authorization: Bearer <stored-token> on every request. You can verify by running:
paperclipai auth whoami
which calls /api/cli-auth/me and prints:
{
"user": { "id": "...", "name": "...", "email": "..." },
"userId": "...",
"isInstanceAdmin": true,
"companyIds": ["..."],
"source": "board-cli",
"keyId": "..."
}
All other CLI commands — the ones documented in Setup Commands and Control-Plane Commands — reuse this credential unless you pass an explicit --token or override PAPERCLIP_API_KEY.
Rotating and revoking
When you want to rotate the credential, log out and log back in:
paperclipai auth logout
paperclipai auth login
paperclipai auth logout:
- POSTs to
/api/cli-auth/revoke-currentwith the stored token to revoke it server-side. - Deletes the local credential entry for this
apiBase.
If the server-side revoke fails (network error, token already invalid) the local credential is still removed — you won't be stuck with a stale entry. A new paperclipai auth login mints a fresh challenge and a fresh token with no relationship to the old one.
Multiple servers and users
The credential store is keyed by normalised apiBase, so one machine can hold separate credentials for a local dev instance, a staging server, and a production server at the same time. Switching between them is a matter of passing --api-base (or setting it in config). Logging in as a different user for the same apiBase replaces the existing credential for that server.
Troubleshooting
- "Invalid CLI auth URL." — You opened the approval page with a missing
idortokenquery param. Re-runpaperclipai auth loginto get a fresh link. - "CLI auth challenge unavailable" — The token is valid but the server no longer has a matching challenge (it expired, or the server restarted). Start the login again.
- "This challenge requires instance-admin access." — You're approving an
--instance-adminchallenge but your signed-in user isn't an instance admin. Sign in with an admin account and retry. - "CLI auth challenge expired before approval." on the CLI — You let the browser tab sit too long. Just rerun
paperclipai auth login. - Board claim page shows "Claim challenge unavailable." — The one-time ownership challenge has already been consumed, or it expired. If the instance has no real admins yet, restart the server — it mints a new challenge and reprints the URL in the logs.
Where to go next
- Setup Commands — installing, onboarding, and configuring a Paperclip instance from the CLI.
- Authentication (API) — the underlying authentication model for board users, agents, and runs.