[codex] Improve docs Lighthouse scores (#41)

## Summary

Improves the docs page Lighthouse profile for the report attached to
[PAP-12441](/PAP/issues/PAP-12441).

- Removed render-blocking third-party docs shell requests for Google Tag
Manager, Google Fonts, and Lucide.
- Vendored the existing `[email protected]` parser locally and copied
`site/vendor` into release builds.
- Replaced Lucide runtime icon rendering with inline SVG paths for the
existing docs section icons.
- Fixed reported accessibility issues: mobile zoom disabling, hidden
drawer focusability, TOC ARIA semantics, footer heading order,
muted-text contrast, TOC button accessible name, and article link
affordance.
- Added smaller responsive WebP variants for the dashboard overview
screenshot and explicit dimensions/fetch hints for article images.
- Minified CSS in release builds.

## Canary

- Commit preview: https://2f89e7fc.paperclip-docs-74t.pages.dev
- Branch preview:
https://codex-lighthouse-docs-scores.paperclip-docs-74t.pages.dev
- Target route from the report:
https://2f89e7fc.paperclip-docs-74t.pages.dev/guides/getting-started/five-minute-path/

## Root Cause

The docs shell was paying for several third-party render-blocking
requests before first contentful render, and the target guide loaded a
large full-size dashboard PNG for a small mobile viewport. Several
Lighthouse accessibility audits were caused by UI semantics rather than
content: a hidden but focusable drawer, menu roles around plain links, a
non-zoomable viewport, footer heading level skips, and color-only
article links.

## Validation

- `npm run docs:build` passes.
- `npm run docs:test:static-routes` passes.
- `npm run docs:test:hierarchical-skills-nav` passes.
- `node --check site/app.js`, `node --check .site/app.js`, and `node
--check site/build-release.mjs` pass.
- Static release checks pass for preserved SEO metadata, removed
third-party assets, zoomable viewport, inert drawer, no footer headings,
underlined article links, responsive screenshot wiring, TOC nav
semantics, and copied optimized assets.
- Cloudflare Pages check passed for commit `129baaa`.

Local Lighthouse could not complete in this container because the
available cached Chromium binaries fail to start without
`libatk-1.0.so.0`. PageSpeed Online is also quota-limited from this
environment. The canary URL above is ready for a browser-capable
Lighthouse run.

---------

Co-authored-by: DevRel Gemini <[email protected]>
Co-authored-by: Paperclip <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-07-02 13:39:51 -05:00
1 parent dd7fa3e1c6
commit b214cdebda
10 files changed
+880 -83

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+486 -1
View File
@@ -9,7 +9,450 @@
"marked": "^12.0.2"
},
"devDependencies": {
"@playwright/test": "^1.58.2"
"@playwright/test": "^1.58.2",
"esbuild": "^0.25.12"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@playwright/test": {
@@ -28,6 +471,48 @@
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+2 -1
View File
@@ -26,7 +26,8 @@
"screenshots:refresh:all": "node scripts/screenshots/run.mjs --all"
},
"devDependencies": {
"@playwright/test": "^1.58.2"
"@playwright/test": "^1.58.2",
"esbuild": "^0.25.12"
},
"dependencies": {
"marked": "^12.0.2"
+49 -1
View File
@@ -46,8 +46,38 @@ try {
assert(skillsHtml.includes("<h1>Skills Reference</h1>"), "skills route is missing crawler-visible page content");
assert(skillsHtml.includes("the file shape on disk"), "skills route body is missing expected docs copy");
assert(skillsHtml.includes('<base data-seo-base href="/" />'), "nested route is missing the release base path");
assert(skillsHtml.includes('href="styles.css"'), "nested route does not load stylesheet from the release base path");
assert(skillsHtml.includes("<style data-inline-release-css>"), "nested route does not inline release CSS");
assert(!skillsHtml.includes('rel="stylesheet" href="styles.css"'), "nested route still render-blocks on styles.css");
assert(skillsHtml.includes('src="app.js"'), "nested route does not load app JS from the release base path");
assert(skillsHtml.includes("html:not(.motion-ready) *"), "nested route is missing the first-paint motion gate");
const rootHtml = read("index.html");
assert(
rootHtml.includes('href="/guides/getting-started/five-minute-path/" data-nav="link">Quickstart</a>'),
"footer Quickstart link should point to the docs quickstart route",
);
assert(
rootHtml.includes('href="/guides/org/adapters/" data-nav="link">Integrations</a>'),
"footer Integrations link should point to the adapters docs route",
);
assert(
rootHtml.includes("paperclip/blob/main/CONTRIBUTING.md"),
"footer Contributing link should point to the repo contributing file",
);
const quickstartHtml = read("guides/getting-started/five-minute-path/index.html");
assert(
quickstartHtml.includes('data-screenshot="../../user-guides/screenshots/light/dashboard/dashboard-overview.png"'),
"quickstart route is missing the theme-aware dashboard screenshot marker",
);
assert(
quickstartHtml.includes('width="2880" height="1800"'),
"quickstart dashboard screenshot is missing intrinsic dimensions",
);
assert(
quickstartHtml.includes("dashboard-overview-900.webp 900w"),
"quickstart dashboard screenshot is missing the responsive WebP srcset",
);
const appJs = read("app.js");
assert(!appJs.includes("/#/"), "generated app JS still contains primary hash route URLs");
@@ -85,6 +115,24 @@ try {
"canonical route redirects must appear before the SPA fallback rewrite",
);
const headers = read("_headers");
assert(
headers.includes("Strict-Transport-Security: max-age=31536000; includeSubDomains; preload"),
"Cloudflare headers are missing HSTS",
);
assert(
headers.includes("Cross-Origin-Opener-Policy: same-origin"),
"Cloudflare headers are missing COOP",
);
assert(
headers.includes("Content-Security-Policy: default-src 'self';"),
"Cloudflare headers are missing CSP",
);
assert(
headers.includes("connect-src 'self' https://api.github.com"),
"Cloudflare CSP must allow the GitHub stars API",
);
const deployGuide = read("DEPLOY.md");
assert(deployGuide.includes("Cloudflare Pages"), "deploy guide is missing Cloudflare Pages guidance");
assert(deployGuide.includes("Pushing `main` triggers the production deployment"), "deploy guide is missing production deploy guidance");
+150 -37
View File
@@ -1,13 +1,26 @@
/* ─── Section icons ─────────────────────────────────────────────────────── */
// Rendered via Lucide (https://lucide.dev). Section names reference any Lucide
// icon by its kebab-case id; `lucide.createIcons()` replaces <i data-lucide>
// placeholders with inline SVG after each nav render.
const SECTION_ICON_PATHS = {
'book-open': '<path d="M12 7v14"/><path d="M3 18a2 2 0 0 1 2-2h7V5H5a2 2 0 0 0-2 2z"/><path d="M21 18a2 2 0 0 0-2-2h-7V5h7a2 2 0 0 1 2 2z"/>',
braces: '<path d="M8 3H7a2 2 0 0 0-2 2v3a2 2 0 0 1-2 2 2 2 0 0 1 2 2v3a2 2 0 0 0 2 2h1"/><path d="M16 21h1a2 2 0 0 0 2-2v-3a2 2 0 0 1 2-2 2 2 0 0 1-2-2V9a2 2 0 0 0-2-2h-1"/>',
cloud: '<path d="M17.5 19H8a5 5 0 1 1 1.1-9.9A7 7 0 0 1 22 12.5 4.5 4.5 0 0 1 17.5 19z"/>',
'kanban-square': '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7v10"/><path d="M12 7v6"/><path d="M16 7v3"/>',
'layout-dashboard': '<rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/>',
network: '<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M12 8v4M5 16v-2a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v2"/>',
package: '<path d="m12 3 8 4.5v9L12 21l-8-4.5v-9z"/><path d="m4 7.5 8 4.5 8-4.5"/><path d="M12 12v9"/>',
plug: '<path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M7 8h10v4a5 5 0 0 1-10 0z"/>',
puzzle: '<path d="M19 13h-2.5a1.5 1.5 0 0 0 0 3H19v3H5v-3h2.5a1.5 1.5 0 0 0 0-3H5V5h4a2 2 0 1 1 4 0h6z"/>',
rocket: '<path d="M4.5 16.5c-1.2 1-1.5 3-1.5 3s2-.3 3-1.5"/><path d="M9 15 6 12c2-5 6-8 13-9-1 7-4 11-9 13z"/><path d="M15 9h.01"/>',
'settings-2': '<path d="M20 7h-9"/><path d="M14 17H4"/><circle cx="7" cy="7" r="3"/><circle cx="17" cy="17" r="3"/>',
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
terminal: '<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>',
wrench: '<path d="M14.7 6.3a4 4 0 0 0-5 5L3 18l3 3 6.7-6.7a4 4 0 0 0 5-5l-2.4 2.4-3-3z"/>',
};
function sectionIconTag(section) {
return `<i data-lucide="${escapeAttr(section?.icon || 'book')}"></i>`;
}
function renderLucideIcons() {
if (typeof lucide !== 'undefined') lucide.createIcons();
const icon = SECTION_ICON_PATHS[section?.icon] || SECTION_ICON_PATHS['book-open'];
return `<svg class="section-icon-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icon}</svg>`;
}
function renderLucideIcons() {}
function getSectionKind(section) {
if (section && typeof section === 'object') return section.tier || 'Guides';
return 'Guides';
@@ -20,6 +33,9 @@ const TIER_ORDER = ['Learn', 'Administration', 'Reference'];
if (saved === 'dark') document.documentElement.setAttribute('data-theme', 'dark');
else document.documentElement.removeAttribute('data-theme');
})();
window.addEventListener('load', () => {
requestAnimationFrame(() => document.documentElement.classList.add('motion-ready'));
}, { once: true });
/* ─── State ─────────────────────────────────────────────────────────────── */
let navData = null;
@@ -31,6 +47,7 @@ let tocScrollHandler = null;
let tocResizeHandler = null;
let tocDocumentClickHandler = null;
let tocKeydownHandler = null;
let markdownRendererPromise = null;
const SEO_SITE_NAME = 'Paperclip Docs';
const SEO_DEFAULT_TITLE = 'Paperclip Docs';
const SEO_DEFAULT_DESCRIPTION = 'Guides, references, and walkthroughs for running Paperclip, an AI company operating system for agent teams, governance, budgets, and workflows.';
@@ -45,6 +62,38 @@ const APP_BASE_PATH = (() => {
const APP_BASE_URL = new URL(`${APP_BASE_PATH.replace(/\/$/, '')}/`, window.location.origin);
const APP_SHELL_URL = new URL('index.html', APP_BASE_URL);
function ensureMarkdownRenderer() {
if (window.marked) return Promise.resolve(window.marked);
if (!markdownRendererPromise) {
markdownRendererPromise = new Promise((resolve, reject) => {
const existing = document.querySelector('script[data-markdown-renderer]');
if (existing) {
existing.addEventListener('load', () => resolve(window.marked), { once: true });
existing.addEventListener('error', () => reject(new Error('Markdown renderer failed to load.')), { once: true });
return;
}
const script = document.createElement('script');
script.src = resolveContentUrl('vendor/marked.min.js');
script.defer = true;
script.dataset.markdownRenderer = '';
script.addEventListener('load', () => {
if (window.marked) resolve(window.marked);
else reject(new Error('Markdown renderer failed to initialize.'));
}, { once: true });
script.addEventListener('error', () => reject(new Error('Markdown renderer failed to load.')), { once: true });
document.head.appendChild(script);
});
}
return markdownRendererPromise;
}
async function fetchMarkdown(file) {
const res = await fetch(resolveContentUrl(file));
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
}
/* ─── Screenshot src resolver ───────────────────────────────────────────── */
function resolveScreenshotSrc(src) {
const theme = document.documentElement.dataset.theme || 'light';
@@ -425,6 +474,7 @@ function decorateCodeBlocks(article) {
}
} catch (_) {}
const refresh = () => {
fetch('https://api.github.com/repos/' + REPO, {
headers: { 'Accept': 'application/vnd.github.v3+json' },
})
@@ -435,6 +485,12 @@ function decorateCodeBlocks(article) {
try { localStorage.setItem(CACHE_KEY, JSON.stringify({ count: data.stargazers_count, ts: Date.now() })); } catch (_) {}
})
.catch(() => {});
};
if ('requestIdleCallback' in window) {
window.requestIdleCallback(refresh, { timeout: 3000 });
} else {
window.setTimeout(refresh, 1500);
}
})();
/* ─── Theme toggle wiring ───────────────────────────────────────────────── */
@@ -447,7 +503,7 @@ document.getElementById('theme-toggle').addEventListener('click', () => {
localStorage.setItem('pc-guides-theme', next);
// Swap all visible screenshot srcs
document.querySelectorAll('#article img[data-screenshot]').forEach(img => {
img.src = resolveScreenshotSrc(img.dataset.screenshot);
applyScreenshotSource(img, img.dataset.screenshot);
});
});
@@ -456,6 +512,7 @@ function openDrawer() {
const drawer = document.getElementById('drawer');
const backdrop = document.getElementById('drawer-backdrop');
const hamburger = document.getElementById('hamburger');
drawer.inert = false;
drawer.classList.add('is-open');
backdrop.classList.add('is-open');
drawer.setAttribute('aria-hidden', 'false');
@@ -469,6 +526,7 @@ function closeDrawer() {
drawer.classList.remove('is-open');
backdrop.classList.remove('is-open');
drawer.setAttribute('aria-hidden', 'true');
drawer.inert = true;
hamburger.setAttribute('aria-expanded', 'false');
document.body.style.overflow = '';
}
@@ -536,13 +594,12 @@ document.addEventListener('click', e => {
/* ─── Search ────────────────────────────────────────────────────────────── */
let searchIndex = [];
let searchFocusedIdx = -1;
let searchIndexPromise = null;
async function buildSearchIndex() {
const tasks = allPages.map(async page => {
try {
const res = await fetch(resolveContentUrl(page.file));
if (!res.ok) return null;
const md = await res.text();
const md = await fetchMarkdown(page.file);
// Extract headings
const headings = [];
const hRe = /^#{1,3}\s+(.+)$/gm;
@@ -561,6 +618,13 @@ async function buildSearchIndex() {
} catch { return null; }
});
searchIndex = (await Promise.all(tasks)).filter(Boolean);
return searchIndex;
}
function ensureSearchIndex() {
if (searchIndex.length) return Promise.resolve(searchIndex);
if (!searchIndexPromise) searchIndexPromise = buildSearchIndex();
return searchIndexPromise;
}
function searchGuides(query) {
@@ -645,6 +709,9 @@ function openSearchModal() {
if (!modal || !input) return;
modal.hidden = false;
document.body.style.overflow = 'hidden';
ensureSearchIndex().then(() => {
if (input.value.trim()) renderSearchResults(input.value);
});
// focus next tick so modal animates in cleanly
requestAnimationFrame(() => { input.focus(); input.select(); });
}
@@ -672,7 +739,21 @@ function initSearch() {
const box = document.getElementById('search-results');
if (!input || !box) return;
input.addEventListener('input', () => renderSearchResults(input.value));
input.addEventListener('input', () => {
if (searchIndex.length) {
renderSearchResults(input.value);
return;
}
if (input.value.trim()) {
box.innerHTML = '<div class="search-empty">Loading search…</div>';
box.classList.add('is-open');
} else {
box.classList.remove('is-open');
}
ensureSearchIndex().then(() => {
if (input.value.trim()) renderSearchResults(input.value);
});
});
input.addEventListener('keydown', e => {
const items = [...box.querySelectorAll('.search-result')];
@@ -748,16 +829,16 @@ async function init() {
buildSidebar();
buildMobileDrawer();
initSearch();
buildSearchIndex(); // background — no await
const pathRoute = getPathRoute();
const rawRoute = applyRedirect(pathRoute || getLegacyRoute());
const initialRoute = parseRoute(rawRoute);
if (initialRoute.page && pathRoute) initialRoute.headingId = getCurrentHeadingRoute();
const normalizedRaw = normalizeRouteKey(decodeURIComponent((rawRoute || '').trim()));
if (initialRoute.page) {
loadPage(initialRoute.page.file, initialRoute.headingId, 'replace');
const staticArticle = document.getElementById('article');
const useStaticArticle = Boolean(pathRoute && staticArticle?.children.length);
loadPage(initialRoute.page.file, initialRoute.headingId, 'replace', { useStaticArticle });
} else {
// Empty or unknown route -> landing
showLanding();
@@ -936,7 +1017,7 @@ function buildFlatList() {
}
/* ─── Load page ─────────────────────────────────────────────────────────── */
async function loadPage(file, targetHeading = null, historyMode = 'push') {
async function loadPage(file, targetHeading = null, historyMode = 'push', options = {}) {
const page = allPages.find(candidate => candidate.file === file);
currentFile = file;
showArticleView();
@@ -944,20 +1025,21 @@ async function loadPage(file, targetHeading = null, historyMode = 'push') {
showLoading();
let md;
const article = document.getElementById('article');
const useStaticArticle = Boolean(options.useStaticArticle && article?.children.length);
if (useStaticArticle) {
currentMarkdown = '';
} else {
try {
const res = await fetch(resolveContentUrl(file));
if (!res.ok) throw new Error(`HTTP ${res.status}`);
md = await res.text();
md = await fetchMarkdown(file);
currentMarkdown = md;
updatePageSeo(page, md);
article.innerHTML = await renderMarkdown(md);
} catch (e) {
showError(`Could not load: ${file}`, e.message);
return;
}
const html = renderMarkdown(md);
const article = document.getElementById('article');
article.innerHTML = html;
}
article.style.display = '';
// Insert sticky meta-row after the first h1 (before other post-processing so TOC can attach to it).
@@ -1091,7 +1173,9 @@ function buildPageActions(page) {
const copyMarkdown = async () => {
try {
await navigator.clipboard.writeText(currentMarkdown || '');
const markdown = currentMarkdown || await fetchMarkdown(page.file);
if (!currentMarkdown) currentMarkdown = markdown;
await navigator.clipboard.writeText(markdown);
flashCopied('Page copied');
} catch (e) {
console.error('Copy failed', e);
@@ -1180,11 +1264,12 @@ function stripFrontmatter(md) {
return rest.slice(closeMatch.index + closeMatch[0].length).replace(/^\r?\n/, '');
}
function renderMarkdown(md) {
async function renderMarkdown(md) {
const renderer = await ensureMarkdownRenderer();
renderer.setOptions({ gfm: true, breaks: false });
md = stripFrontmatter(md);
md = preprocessTabs(md);
marked.setOptions({ gfm: true, breaks: false });
return sanitizeMarkdownHtml(marked.parse(md));
return sanitizeMarkdownHtml(renderer.parse(md));
}
const ALLOWED_MARKDOWN_TAGS = new Set([
@@ -1280,7 +1365,7 @@ function renderTabsBlock(labels, body) {
let idx = 0;
while ((m = re.exec(body)) !== null) {
out += `<div class="tab-panel${idx === 0 ? ' active' : ''}" data-panel="${escapeAttr(m[1].trim())}">`;
out += marked.parse(m[2].trim());
out += window.marked.parse(m[2].trim());
out += `</div>`;
idx++;
}
@@ -1373,15 +1458,43 @@ function postProcessTabs(root) {
});
}
function postProcessImages(root) {
root.querySelectorAll('img').forEach(img => {
// Remap ../images/ paths to screenshots/{theme}/ and store original for theme swaps
const rawSrc = img.getAttribute('src') || img.src;
const RESPONSIVE_SCREENSHOT_VARIANTS = new Map([
['dashboard/dashboard-overview.png', { width: 2880, height: 1800, variantWidth: 900 }],
]);
function getScreenshotVariantConfig(src) {
const match = String(src).match(/(?:screenshots|images)\/(?:light|dark)\/(.+)$/);
if (!match) return null;
return RESPONSIVE_SCREENSHOT_VARIANTS.get(match[1]) || null;
}
function applyScreenshotSource(img, rawSrc) {
const resolved = resolveScreenshotSrc(rawSrc);
if (resolved !== rawSrc) {
img.dataset.screenshot = rawSrc; // store original path
img.src = resolved;
img.dataset.screenshot = rawSrc;
}
img.src = resolved;
const variantConfig = getScreenshotVariantConfig(resolved) || getScreenshotVariantConfig(rawSrc);
if (!variantConfig) return;
const optimizedSrc = resolved.replace(/\.png(?:\?.*)?$/i, '-900.webp');
img.width = variantConfig.width;
img.height = variantConfig.height;
img.sizes = '(max-width: 820px) calc(100vw - 48px), 820px';
img.srcset = `${optimizedSrc} ${variantConfig.variantWidth}w, ${resolved} ${variantConfig.width}w`;
img.classList.add('responsive-screenshot');
img.style.aspectRatio = `${variantConfig.width} / ${variantConfig.height}`;
}
function postProcessImages(root) {
[...root.querySelectorAll('img')].forEach((img, index) => {
// Remap ../images/ paths to screenshots/{theme}/ and store original for theme swaps
const rawSrc = img.getAttribute('src') || img.src;
applyScreenshotSource(img, rawSrc);
img.decoding = 'async';
img.loading = index === 0 ? 'eager' : 'lazy';
img.fetchPriority = index === 0 ? 'high' : 'auto';
img.addEventListener('error', () => {
const ph = document.createElement('div');
ph.className = 'img-placeholder';
@@ -1520,15 +1633,15 @@ function buildToc(article, file) {
const wrap = document.createElement('div');
wrap.className = 'toc-wrap';
wrap.innerHTML = `
<button type="button" class="toc-toggle" aria-expanded="false" aria-controls="toc-panel" aria-label="On this page">
<button type="button" class="toc-toggle" aria-expanded="false" aria-controls="toc-panel">
${TOC_SVG}
<span class="toc-label">On this page</span>
<span class="count">${headings.length}</span>
${CHEVRON_SVG}
</button>
<div class="toc-panel" id="toc-panel" role="menu">
<nav class="toc-panel" id="toc-panel" aria-label="On this page">
<div class="toc-panel-label">On this page</div>
</div>
</nav>
`;
const panel = wrap.querySelector('.toc-panel');
const toggle = wrap.querySelector('.toc-toggle');
+141 -11
View File
@@ -3,6 +3,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { transform } from "esbuild";
import { marked } from "marked";
const __filename = fileURLToPath(import.meta.url);
@@ -12,6 +13,7 @@ const sourceIndexPath = path.join(__dirname, "index.html");
const sourceStylesPath = path.join(__dirname, "styles.css");
const sourceAppJsPath = path.join(__dirname, "app.js");
const sourceNavPath = path.join(__dirname, "content.json");
const sourceVendorDir = path.join(__dirname, "vendor");
const screenshotsSourceDir = path.join(docsRoot, "user-guides", "screenshots");
const defaultSiteUrl = "https://docs.paperclip.ing";
const defaultSeoDescription = "Guides, references, and walkthroughs for running Paperclip, an AI company operating system for agent teams, governance, budgets, and workflows.";
@@ -447,6 +449,10 @@ function escapeHtml(value) {
.replace(/"/g, "&quot;");
}
function escapeAttr(value) {
return escapeHtml(value).replace(/'/g, "&#39;");
}
function escapeXml(value) {
return String(value)
.replace(/&/g, "&amp;")
@@ -456,6 +462,50 @@ function escapeXml(value) {
.replace(/'/g, "&apos;");
}
const responsiveScreenshotVariants = new Map([
["dashboard/dashboard-overview.png", { width: 2880, height: 1800, variantWidth: 900 }],
]);
function screenshotReleasePath(src, theme = "dark") {
const match = String(src).match(/(?:^|\/)user-guides\/screenshots\/(?:light|dark)\/(.+)$/);
if (match) return `user-guides/screenshots/${theme}/${match[1]}`;
return src;
}
function screenshotVariantConfig(src) {
const match = String(src).match(/(?:^|\/)user-guides\/screenshots\/(?:light|dark)\/(.+)$/);
if (!match) return null;
return responsiveScreenshotVariants.get(match[1]) || null;
}
function releaseMarkdownImage(href, title, text) {
const src = screenshotReleasePath(href);
const attrs = [
`src="${escapeAttr(src)}"`,
`alt="${escapeAttr(text || "")}"`,
];
if (title) attrs.push(`title="${escapeAttr(title)}"`);
const variantConfig = screenshotVariantConfig(src);
if (variantConfig) {
const optimizedSrc = src.replace(/\.png(?:\?.*)?$/i, "-900.webp");
attrs.push(
`class="responsive-screenshot"`,
`data-screenshot="${escapeAttr(href)}"`,
`width="${variantConfig.width}"`,
`height="${variantConfig.height}"`,
`sizes="(max-width: 820px) calc(100vw - 48px), 820px"`,
`srcset="${escapeAttr(`${optimizedSrc} ${variantConfig.variantWidth}w, ${src} ${variantConfig.width}w`)}"`,
`decoding="async"`,
`loading="eager"`,
`fetchpriority="high"`,
`style="aspect-ratio:${variantConfig.width}/${variantConfig.height}"`,
);
}
return `<img ${attrs.join(" ")}>`;
}
function markdownToPlainText(markdown) {
return markdown
.replace(/```[\s\S]*?```/g, " ")
@@ -627,6 +677,11 @@ function buildCloudflareHeaders() {
return `/*
X-Robots-Tag: index, follow
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Content-Security-Policy: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://api.github.com; upgrade-insecure-requests
/sitemap.xml
Content-Type: application/xml; charset=utf-8
@@ -750,22 +805,73 @@ location ${deploymentBasePath} {
`;
}
function renderStaticMarkdown(markdown) {
marked.setOptions({ gfm: true, breaks: false });
return marked.parse(markdown);
function renderTabsBlock(labels, body) {
const names = labels.split(",").map((label) => label.trim());
let output = '<div class="tabs-container">';
output += '<div class="tabs-bar">';
names.forEach((name, index) => {
output += `<button class="tab-btn${index === 0 ? " active" : ""}" data-tab="${escapeAttr(name)}">${escapeHtml(name)}</button>`;
});
output += "</div>";
const tabRegex = /<!-- tab: (.+?) -->([\s\S]*?)(?=<!-- tab:|$)/g;
let match;
let index = 0;
while ((match = tabRegex.exec(body)) !== null) {
output += `<div class="tab-panel${index === 0 ? " active" : ""}" data-panel="${escapeAttr(match[1].trim())}">`;
output += marked.parse(match[2].trim());
output += "</div>";
index += 1;
}
return `${output}</div>`;
}
function buildStaticPageHtml(sourceIndex, metadata, markdown, basePath) {
function preprocessTabs(markdown) {
const openMarker = "<!-- tabs:";
const closeMarker = "<!-- /tabs -->";
const maxIterations = 100;
let output = markdown;
for (let index = 0; index < maxIterations; index += 1) {
const closeIndex = output.indexOf(closeMarker);
if (closeIndex === -1) break;
const openIndex = output.lastIndexOf(openMarker, closeIndex - 1);
if (openIndex === -1) break;
const afterOpen = output.indexOf("-->", openIndex);
if (afterOpen === -1 || afterOpen > closeIndex) break;
const labels = output.slice(openIndex + openMarker.length, afterOpen).trim();
const body = output.slice(afterOpen + 3, closeIndex);
output = output.slice(0, openIndex) + renderTabsBlock(labels, body) + output.slice(closeIndex + closeMarker.length);
}
return output;
}
function renderStaticMarkdown(markdown) {
const renderer = new marked.Renderer();
renderer.image = releaseMarkdownImage;
marked.setOptions({ gfm: true, breaks: false, renderer });
return marked.parse(preprocessTabs(markdown));
}
function inlineReleaseStyles(html, css) {
return html.replace(
'<link rel="stylesheet" href="styles.css" />',
`<style data-inline-release-css>${css}</style>`,
);
}
function buildStaticPageHtml(sourceIndex, metadata, markdown, basePath, releaseStyles) {
const articleHtml = renderStaticMarkdown(markdown);
const routeBaseHref = basePath === "auto" ? "/" : basePath;
return injectSeo(sourceIndex, metadata, { baseHref: routeBaseHref })
return inlineReleaseStyles(injectSeo(sourceIndex, metadata, { baseHref: routeBaseHref }), releaseStyles)
.replace('<section id="landing">', '<section id="landing">')
.replace('<div id="article-view">', '<div id="article-view" class="is-active">')
.replace('<div id="loading">', '<div id="loading" style="display:none">')
.replace('<article id="article" style="display:none"></article>', `<article id="article">${articleHtml}</article>`);
}
async function writeStaticRoutePages({ outDir, sourceIndex, pages, markdownBodiesByFile, basePath }) {
async function writeStaticRoutePages({ outDir, sourceIndex, pages, markdownBodiesByFile, basePath, releaseStyles }) {
for (const metadata of pages) {
const { page } = metadata;
const markdown = markdownBodiesByFile.get(page.file);
@@ -775,7 +881,7 @@ async function writeStaticRoutePages({ outDir, sourceIndex, pages, markdownBodie
throw new Error(`Refusing to write route outside release directory: ${page.slug}`);
}
await ensureDir(path.dirname(routePath));
await fs.writeFile(routePath, buildStaticPageHtml(sourceIndex, metadata, markdown, basePath));
await fs.writeFile(routePath, buildStaticPageHtml(sourceIndex, metadata, markdown, basePath, releaseStyles));
}
}
@@ -820,6 +926,25 @@ The generated \`.htaccess\` and \`nginx.conf.example\` are optional examples for
`;
}
function minifyCss(source) {
return source
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\s+/g, " ")
.replace(/\s*([{}:;,>+~])\s*/g, "$1")
.replace(/;}/g, "}")
.trim();
}
async function minifyJs(source) {
const result = await transform(source, {
loader: "js",
minify: true,
target: "es2020",
legalComments: "none",
});
return result.code;
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const sourceNav = JSON.parse(await fs.readFile(sourceNavPath, "utf8"));
@@ -832,9 +957,13 @@ async function main() {
const sourceIndex = await fs.readFile(sourceIndexPath, "utf8");
const sourceStyles = await fs.readFile(sourceStylesPath, "utf8");
const sourceAppJs = await fs.readFile(sourceAppJsPath, "utf8");
const releaseStyles = minifyCss(sourceStyles);
const releaseAppJs = rewriteAppJs(sourceAppJs, options.basePath);
await fs.writeFile(path.join(options.outDir, "styles.css"), sourceStyles);
await fs.writeFile(path.join(options.outDir, "app.js"), releaseAppJs);
await fs.writeFile(path.join(options.outDir, "styles.css"), releaseStyles);
await fs.writeFile(path.join(options.outDir, "app.js"), await minifyJs(releaseAppJs));
if (await pathExists(sourceVendorDir)) {
await copyDirRecursive(sourceVendorDir, path.join(options.outDir, "vendor"));
}
await fs.writeFile(path.join(options.outDir, ".htaccess"), buildHtaccess(options.basePath));
await fs.writeFile(path.join(options.outDir, "nginx.conf.example"), buildNginxConfig(options.basePath));
await fs.writeFile(path.join(options.outDir, "DEPLOY.md"), buildDeployGuide(options.basePath));
@@ -862,7 +991,7 @@ async function main() {
const fm = frontmatterByFile.get(page.file);
if (fm) page.frontmatter = fm;
}
await fs.writeFile(path.join(options.outDir, "content.json"), `${JSON.stringify(releaseNav, null, 2)}\n`);
await fs.writeFile(path.join(options.outDir, "content.json"), `${JSON.stringify(releaseNav)}\n`);
const pageMetadata = await pageMetadataForNav(releaseNav, options.outDir, options.siteUrl, options.basePath);
await fs.writeFile(path.join(options.outDir, "_redirects"), buildCloudflareRedirects({
@@ -876,13 +1005,14 @@ async function main() {
siteUrl: options.siteUrl,
basePath: options.basePath,
};
await fs.writeFile(path.join(options.outDir, "index.html"), injectSeo(sourceIndex, rootMetadata));
await fs.writeFile(path.join(options.outDir, "index.html"), inlineReleaseStyles(injectSeo(sourceIndex, rootMetadata), releaseStyles));
await writeStaticRoutePages({
outDir: options.outDir,
sourceIndex,
pages: pageMetadata,
markdownBodiesByFile,
basePath: options.basePath,
releaseStyles,
});
await fs.writeFile(path.join(options.outDir, "sitemap.xml"), buildSitemap({
+11 -25
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Paperclip Docs</title>
<meta name="description" data-seo-managed content="Guides, references, and walkthroughs for running Paperclip, an AI company operating system for agent teams, governance, budgets, and workflows." />
<meta name="robots" data-seo-managed content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
@@ -15,15 +15,6 @@
<meta name="twitter:card" data-seo-managed content="summary" />
<meta name="twitter:title" data-seo-managed content="Paperclip Docs" />
<meta name="twitter:description" data-seo-managed content="Guides, references, and walkthroughs for running Paperclip, an AI company operating system for agent teams, governance, budgets, and workflows." />
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-0JGE5X0HMM"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-0JGE5X0HMM');
</script>
<script>
/* Apply theme before first paint (dark by default) to avoid a flash. */
(function () {
@@ -31,12 +22,7 @@
if (saved === 'dark') document.documentElement.setAttribute('data-theme', 'dark');
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,100..900;1,100..900&family=Inter:ital,wght@0,100..900;1,100..900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48'/></svg>" />
<script src="https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js" integrity="sha384-/TQbtLCAerC3jgaim+N78RZSDYV7ryeoBCVqTuzRrFec2akfBkHS7ACQ3PQhvMVi" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/lucide.min.js" integrity="sha384-hJnF5AwidE18GSWTAGHv3ByzzvfNZ1Tcx5y1UUV3WkauuMCEzBJBMSwSt/PUPXnM" crossorigin="anonymous"></script>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
@@ -119,7 +105,7 @@
</button>
<div id="drawer-backdrop"></div>
<aside id="drawer" aria-label="Mobile navigation" aria-hidden="true">
<aside id="drawer" aria-label="Mobile navigation" aria-hidden="true" inert>
<div id="drawer-head">
<span class="label">Documentation</span>
<button class="icon-btn" id="drawer-close" aria-label="Close menu">
@@ -181,28 +167,28 @@
</div>
<div class="footer-columns footer-container">
<div class="footer-col">
<h4 class="footer-col-heading">Product</h4>
<div class="footer-col-heading">Product</div>
<a href="https://paperclip.ing/#get-started" target="_blank" rel="noopener">Get started</a>
<a href="https://paperclip.ing/#get-started" target="_blank" rel="noopener">Quickstart</a>
<a href="/guides/getting-started/five-minute-path/" data-nav="link">Quickstart</a>
</div>
<div class="footer-col">
<h4 class="footer-col-heading">Platform</h4>
<a href="https://paperclip.ing/#orgchart-section" target="_blank" rel="noopener">Agents</a>
<div class="footer-col-heading">Platform</div>
<a href="/guides/org/agents/" data-nav="link">Agents</a>
<a href="https://paperclip.ing/#governance-section" target="_blank" rel="noopener">Governance</a>
<a href="https://paperclip.ing/#orgchart-section" target="_blank" rel="noopener">Integrations</a>
<a href="/guides/org/adapters/" data-nav="link">Integrations</a>
</div>
<div class="footer-col">
<h4 class="footer-col-heading">Developers</h4>
<div class="footer-col-heading">Developers</div>
<a href="#" data-nav="home">Documentation</a>
<a href="/reference/api/overview" data-nav="link">API reference</a>
<a href="https://github.com/paperclipai/paperclip" target="_blank" rel="noopener">GitHub</a>
</div>
<div class="footer-col">
<h4 class="footer-col-heading">Resources</h4>
<div class="footer-col-heading">Resources</div>
<a href="https://paperclip.ing/blog" target="_blank" rel="noopener">Blog</a>
<a href="https://paperclip.ing/changelog" target="_blank" rel="noopener">Changelog</a>
<a href="https://github.com/paperclipai/paperclip" target="_blank" rel="noopener">Contributing</a>
<a href="https://github.com/paperclipai/paperclip" target="_blank" rel="noopener">License</a>
<a href="https://github.com/paperclipai/paperclip/blob/main/CONTRIBUTING.md" target="_blank" rel="noopener">Contributing</a>
<a href="https://github.com/paperclipai/paperclip/blob/main/LICENSE" target="_blank" rel="noopener">License</a>
<a href="https://discord.gg/m4HZY7xNG3" target="_blank" rel="noopener">Discord</a>
</div>
</div>
+35 -7
View File
@@ -9,7 +9,7 @@
--stone-dark: #c8c3bc; /* border hover */
--ink: #0a0a0a; /* primary text */
--graphite: #52585d; /* secondary text */
--ash: #9a958a; /* muted text */
--ash: #6f6a61; /* muted text */
--void: #141413; /* charcoal */
--green: #22c55e; /* signal / success */
--green-deep: #188a3c; /* success deep */
@@ -30,10 +30,9 @@
--header-h: 60px;
--topbar-h: var(--header-h);
/* Inter Tight for display, Inter for body, JetBrains Mono for code */
--font-serif: 'Inter Tight', -apple-system, BlinkMacSystemFont, sans-serif;
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--font-serif: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-body: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
/* Brand radius scale — "each radius signals meaning" */
--radius-xs: 4px; /* status pills, code chips, table cells */
@@ -66,6 +65,12 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html:not(.motion-ready) *,
html:not(.motion-ready) *::before,
html:not(.motion-ready) *::after {
transition: none !important;
animation: none !important;
}
body {
background: var(--linen); color: var(--ink);
font-family: var(--font-body); line-height: 1.65;
@@ -967,11 +972,21 @@
#article p { margin-bottom: 16px; }
#article a {
color: var(--ink); text-decoration: none;
color: var(--ink);
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--ink) 45%, transparent);
text-decoration-thickness: 1px;
text-underline-offset: 0.18em;
transition: color var(--transition);
overflow-wrap: anywhere;
}
#article a:hover { color: var(--graphite); }
#article a:hover {
color: var(--graphite);
text-decoration-color: currentColor;
}
#article .heading-anchor {
text-decoration: none;
}
#article strong { color: var(--ink); font-weight: 600; }
#article em { font-style: italic; }
#article ul, #article ol { padding-left: 22px; margin-bottom: 16px; }
@@ -1055,9 +1070,14 @@
#article img {
max-width: 100%; border-radius: var(--radius-sm);
border: 1px solid var(--stone); display: block;
height: auto;
margin: 24px 0;
box-shadow: 0 2px 8px rgba(26,26,26,0.06);
}
#article img.responsive-screenshot {
width: 100%;
object-fit: contain;
}
.img-placeholder {
background: var(--parchment);
border: 1.5px dashed var(--stone-dark);
@@ -1097,6 +1117,14 @@
.callout-tip .callout-label { color: #188a3c; font-weight: 600; }
.callout-warning .callout-label { color: #b45309; font-weight: 600; }
.callout-danger .callout-label { color: #991b1b; font-weight: 600; }
[data-theme="dark"] .callout-info .callout-icon,
[data-theme="dark"] .callout-info .callout-label { color: #60a5fa; }
[data-theme="dark"] .callout-tip .callout-icon,
[data-theme="dark"] .callout-tip .callout-label { color: #22c55e; }
[data-theme="dark"] .callout-warning .callout-icon,
[data-theme="dark"] .callout-warning .callout-label { color: #f59e0b; }
[data-theme="dark"] .callout-danger .callout-icon,
[data-theme="dark"] .callout-danger .callout-label { color: #f87171; }
/* Tabs */
.tabs-container { margin-bottom: 24px; }
+6
View File
@@ -0,0 +1,6 @@
/**
* marked v12.0.2 - a markdown parser
* Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed)
* https://github.com/markedjs/marked
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length<t;)n.push("");for(;s<n.length;s++)n[s]=n[s].trim().replace(/\\\|/g,"|");return n}function x(e,t,n){const s=e.length;if(0===s)return"";let r=0;for(;r<s;){const i=e.charAt(s-r-1);if(i!==t||n){if(i===t||!n)break;r++}else r++}return e.slice(0,s-r)}function b(e,t,n,s){const r=t.href,i=t.title?c(t.title):null,l=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){s.state.inLink=!0;const e={type:"link",raw:n,href:r,title:i,text:l,tokens:s.inlineTokens(l)};return s.state.inLink=!1,e}return{type:"image",raw:n,href:r,title:i,text:c(l)}}class w{options;rules;lexer;constructor(t){this.options=t||e.defaults}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=t[0].replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1");e=x(e.replace(/^ *>[ \t]?/gm,""),"\n");const n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))Line truncated