Building Pages.
Every page on internal.perkup.com is built the same way so the whole wiki feels like one product. This page is the contract — read it before you ship anything.
At a glance
internal.perkup.com is a Cloudflare Pages project (internal-docs) gated by Cloudflare Access to @perkupapp.com Workspace identity. Humans and their agents authenticate the same way — Google Workspace SSO. Pages must follow the shared design system + registration contract below.
How agents fetch this wiki
Every page is behind Cloudflare Access. A plain WebFetch or curl returns a 302 to the SSO login page. To read the wiki, agents use the official cloudflared CLI which performs the same SSO flow you'd do in a browser — but stores a short-lived per-user token on your machine that automatically gets attached to subsequent requests.
One-time install
brew install cloudflared
Sign in (once per ~24h)
cloudflared access login https://internal.perkup.com/
Opens your browser, walks through the same Google Workspace SSO flow that a human goes through. After signing in, cloudflared stores a token under ~/.cloudflared/ tied to your identity. Token lasts ~24h before you need to repeat.
Fetch a page
cloudflared access curl https://internal.perkup.com/agents/
Drop-in replacement for curl. Same flags, same output, same behavior — but it injects the stored auth token so the request gets through Access.
Use the token directly
TOKEN=$(cloudflared access token --app=https://internal.perkup.com/) && \ curl -sS -H "cf-access-token: $TOKEN" https://internal.perkup.com/agents/
If your tooling needs a raw HTTP header instead of the curl wrapper, this returns the JWT. Pass it as the cf-access-token header on any request.
- Every fetch is attributed to your Workspace identity in Cloudflare's audit logs — there's no shared bearer secret floating around.
- If your laptop is lost, revoking your Workspace account immediately revokes wiki access.
- If the token expires mid-task, the next fetch will surface a 302 — just re-run
cloudflared access loginin another terminal and retry.
Shared design system
Every page imports two stylesheets — never inline colors, fonts, or spacing values. These files are the single source of truth.
/_assets/wiki.css— design tokens (color, type, spacing, radius) + all wiki-* components (.wiki-header,.wiki-footer,.wiki-hero,.wiki-card-grid,.wiki-callout,.wiki-table,.wiki-code,.wiki-pill)- Color tokens are CSS custom properties on
:rootwith a:root[data-theme="light"]override. Reference them asvar(--color-accent), never raw hex. - Font stack:
-apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif. System fonts only — never web fonts. - Icons: Lucide outline set, inline SVG with
stroke="currentColor",stroke-width="2",viewBox="0 0 24 24". Never use icon webfonts. - If you need a new component, propose it back to the team — don't invent one. Section-agents do not modify
wiki.css.
The page template
Every page extends this skeleton. The four wiki-* meta tags are mandatory — that is how other agents crawl and index the wiki efficiently.
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{Page Title} · PerkUp Internal</title>
<link rel="icon" type="image/png" href="/_assets/favicon-dark.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" href="/_assets/favicon-light.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" href="/_assets/favicon-dark.png">
<!-- REQUIRED: 4 machine-readable wiki tags -->
<meta name="wiki-section" content="{section-slug}">
<meta name="wiki-page" content="{page-slug}">
<meta name="wiki-last-updated" content="YYYY-MM-DD">
<meta name="wiki-summary" content="{one-line summary, ≤ 200 chars}">
<meta property="og:title" content="{Page Title}">
<meta property="og:description" content="{summary}">
<!-- Pre-paint theme bootstrap to avoid flash -->
<script>(function(){var s=null;try{s=localStorage.getItem('perkup-theme');}catch(e){}var m=window.matchMedia&&window.matchMedia('(prefers-color-scheme: light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',s||m);})();</script>
<link rel="stylesheet" href="/_assets/wiki.css">
</head>
<body>
<!-- shared topnav (copy verbatim from this page or any other) -->
<nav class="topnav">...</nav>
<div class="layout">
<main class="main">
<h1>{Page Title}</h1>
<p class="lede">{One-paragraph lede.}</p>
<!-- content -->
</main>
<aside class="toc">...</aside>
</div>
<footer class="site-footer">...</footer>
</body>
</html>
Register your page
When you ship a new page, append entries to four registries. This is what makes the homepage's "Published Pages" list, the sitemap, and the agent-crawl indexes stay coherent.
/_meta/nav.json— append to the matching section'spagesarray:{slug, title, url, summary}./_meta/pages.json— append full per-page metadata:{slug, title, section, url, summary, last_updated, source}./sitemap.xml— append a<url>entry with<loc>and<lastmod>./llms.txt— append a line under ## Recent:- [Title](/section/page/) — summary.
Naming & URL conventions
- URLs are kebab-case (e.g.
/customers/case-studies/, not/customers/case_studies/). - Section names are plural (
/customers/, not/customer/). - Each page is a directory with
index.html— never bare.htmlfiles at section root. - File
titletag pattern:{Page Title} · PerkUp Internal - Every page opens with
<h1>matching the title, then a one-paragraph lede, then content, then a "Source" footnote.
Content age rules
Stale content erodes trust. Before pulling source content onto a page, check when it was last modified.
- Under 6 months — include verbatim (reformat to wiki style).
- 6 to 18 months — include with a
Last verified: YYYY-MM-DDnote at the top of the page. - Over 18 months AND not marked "Use as-is" or "load-bearing" in the planning sheet's Source Inventory tab → skip. Log every skip in your return report.
Where source content lives
- Wiki Build Plan — the master inventory. Tab 1 (IA), Tab 4 (Source Inventory) tell you what goes where.
- Outline → Notion Migration — 52 docs already migrated; URLs in column N point at the Notion target.
- Notion — primary source for Company, Operations, People, Brand. Use the
mcp__claude_ai_Notion__*tools. - Google Drive — primary source for sales decks, case studies, RFP responses. Use the
gwsCLI, not the google-drive MCP. - Figma — brand assets and mockups. URLs come from the planning sheet.
- perkup-app monorepo — primary source for Engineering pages (
AGENTS.mdfiles across every directory).
Deploy
A single wrangler command publishes any folder of HTML to internal.perkup.com behind the Access gate.
cd /tmp/internal_wiki && \
source ~/.claude/.api-keys/cloudflare && \
CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_ACCOUNT_ID \
CLOUDFLARE_API_TOKEN=$CLOUDFLARE_API_TOKEN \
npx --yes wrangler@latest pages deploy . \
--project-name=internal-docs \
--branch=main \
--commit-dirty=true
Or in Claude Code, invoke the /publish-internal skill with a path. It wraps the same command, validates the page structure, and updates the registries for you.
Pre-deploy checklist
- Imports
/_assets/wiki.css - 4
wiki-*meta tags filled in (section, page, last-updated, summary) - OG title + description set
- Page opens with
<h1>matching<title>, then a one-paragraph lede - Uses only
wiki-*components (no custom CSS in the page) - Ends with a "Source" footnote listing every URL the page was built from
- Updated all four registries (nav.json, pages.json, sitemap.xml, llms.txt)
- Verified vs the hiring page — does it look like a sibling?
Source: this page is the contract version of the conventions in /_meta/build-rules.md (markdown form for direct agent consumption) and the planning sheet's tabs 1-6.