Skip to content

Commit 5c933b9

Browse files
feat(content): consume /docs sections from content repo (slice C) (#12)
The 180-line inline SECTIONS array in DocsPage.tsx is gone. Sections now load from .content/docs/<id>.md (one markdown file per section) via Vite import.meta.glob raw imports. Frontmatter 'order' controls section sequence; filename = anchor id. Editing the /docs page now means editing a .md file in the public content repo — no React, no CSS, no build config. Verified: 8 sections in dist/docs/index.html in the same order as before (quickstart → services → deploy → stacks → claim → authentication → limits → machine-readable). All TOC anchors intact. Dark-theme code chip rule preserved. This completes PR-B (3-slice migration to InstaNode-dev/content): slice A: blog posts (PR #10, merged) slice B: use-cases catalogue (PR #11, merged) slice C: /docs page sections (this PR) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e8f3c43 commit 5c933b9

1 file changed

Lines changed: 46 additions & 185 deletions

File tree

src/pages/DocsPage.tsx

Lines changed: 46 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
/* DocsPage — public /docs.
22
*
3-
* Single-page docs with a sidebar TOC and section anchors. Content lives in
4-
* the SECTIONS array below — adding a new section = one entry. No CMS.
3+
* Single-page docs with a sidebar TOC and section anchors. Content lives
4+
* in InstaNode-dev/content/docs/<id>.md, cloned into .content/ at build
5+
* time. Adding/removing a section = one .md file in the content repo;
6+
* no dashboard PR.
57
*
6-
* Security note: every code snippet here is reachable by anyone curling the
7-
* public domain — they are all anonymous-tier paths. Do not paste production
8-
* JWTs, internal cluster hostnames (*.svc.cluster.local), team_ids, or
9-
* AES/JWT secrets into snippets. The docs ship to the public domain. */
8+
* Section ordering comes from `order:` frontmatter (numeric).
9+
* Section anchor id = filename minus .md.
10+
*
11+
* Security note: every code snippet here is reachable by anyone curling
12+
* the public domain — they are all anonymous-tier paths. Do not paste
13+
* production JWTs, internal cluster hostnames (*.svc.cluster.local),
14+
* team_ids, or AES/JWT secrets into snippets. The docs ship to the
15+
* public domain. */
1016

1117
import { PublicShell } from '../layout/PublicShell'
1218

@@ -16,187 +22,42 @@ type Section = {
1622
body: string // same minimal markdown subset as blog posts
1723
}
1824

19-
const SECTIONS: Section[] = [
20-
{
21-
id: 'quickstart',
22-
title: 'Quickstart',
23-
body: `
24-
The whole platform fits in one curl. No signup, no API key, no Docker.
25-
26-
\`\`\`
27-
curl -X POST https://api.instanode.dev/db/new
28-
\`\`\`
29-
30-
The response includes a \`connection_url\` you can paste into any Postgres
31-
client. The database is real, dedicated, and yours for 24 hours.
32-
33-
When you're ready to keep it, see the **Claim flow** section below.
34-
`
35-
},
36-
{
37-
id: 'services',
38-
title: 'The six services',
39-
body: `
40-
Every endpoint returns a \`connection_url\` (or \`endpoint\` / \`receive_url\`)
41-
plus an \`upgrade_jwt\` you can hand to /claim.
42-
43-
- \`POST /db/new\` — Postgres (pgvector pre-installed)
44-
- \`POST /cache/new\` — Redis (ACL'd, per-token key prefix)
45-
- \`POST /nosql/new\` — MongoDB
46-
- \`POST /queue/new\` — NATS JetStream
47-
- \`POST /storage/new\` — S3-compatible (MinIO)
48-
- \`POST /webhook/new\` — public URL that receives any HTTP method
49-
50-
Every response has the same shape: \`{ ok, token, connection_url, internal_url,
51-
tier, limits, note, upgrade_jwt }\`. \`internal_url\` is the address to use
52-
when the caller itself runs inside our cluster (i.e. via /deploy/new) —
53-
public hostnames don't hairpin reliably from inside.
54-
`
55-
},
56-
{
57-
id: 'deploy',
58-
title: 'Deploying an app',
59-
body: `
60-
\`POST /deploy/new\` takes a multipart form with a gzipped tar archive
61-
containing your Dockerfile + source.
62-
63-
\`\`\`
64-
curl -X POST https://api.instanode.dev/deploy/new \\
65-
-H "Authorization: Bearer <JWT>" \\
66-
-F "tarball=@app.tar.gz" \\
67-
-F "name=my-app" \\
68-
-F "port=8080" \\
69-
-F 'env_vars={"DATABASE_URL":"postgres://..."}'
70-
\`\`\`
71-
72-
The build runs in-cluster on kaniko (~30–90s for typical Node/Python apps)
73-
and the app rolls out behind a public HTTPS URL on
74-
\`*.deployment.instanode.dev\` with a valid Let's Encrypt cert.
75-
76-
\`env_vars\` is optional — pass a JSON object and every key/value lands in
77-
the app's environment on the first build. Saves you a follow-up PATCH+redeploy.
78-
79-
For multi-service apps see **Stacks** below.
80-
`
81-
},
82-
{
83-
id: 'stacks',
84-
title: 'Stacks (multi-service deploy)',
85-
body: `
86-
\`POST /stacks/new\` takes an \`instant.yaml\` manifest plus one tarball per
87-
service. Services can reference each other with \`service://<name>\` env
88-
values — those resolve to cluster-internal \`http://<name>:<port>\` URLs at
89-
deploy time.
90-
91-
\`\`\`
92-
services:
93-
api:
94-
build: ./api
95-
port: 3000
96-
web:
97-
build: ./web
98-
port: 8080
99-
expose: true
100-
env:
101-
API_URL: service://api
102-
\`\`\`
103-
104-
Only services with \`expose: true\` get a public URL — the rest are
105-
in-cluster only. The whole stack rolls out together; partial failure is
106-
reported per-service in \`GET /stacks/{slug}\`.
107-
`
108-
},
109-
{
110-
id: 'claim',
111-
title: 'Claim flow (anonymous → paid)',
112-
body: `
113-
Anonymous resources expire in 24 hours. To keep them, claim them.
114-
115-
\`\`\`
116-
RESP=$(curl -X POST https://api.instanode.dev/db/new -d '{}')
117-
JWT=$(echo $RESP | jq -r .upgrade_jwt)
118-
119-
# Optional preview — shows what would attach, no side effects
120-
curl "https://api.instanode.dev/claim/preview?t=$JWT"
121-
122-
# Trigger the claim — sends a magic link to your email
123-
curl -X POST https://api.instanode.dev/claim \\
124-
-d "{\\"jwt\\":\\"$JWT\\", \\"email\\":\\"you@example.com\\"}"
125-
\`\`\`
126-
127-
Click the magic link to set a session cookie. Every resource attached to your
128-
fingerprint transfers to your team atomically; the connection URLs don't
129-
change so any already-running code keeps working.
130-
131-
Claimed resources move to your team's tier (hobby by default — $9/mo). There
132-
is no separate trial period on paid tiers — **the 24-hour anonymous slice
133-
is the trial**.
134-
`
135-
},
136-
{
137-
id: 'authentication',
138-
title: 'Authentication',
139-
body: `
140-
Resource provisioning is anonymous. Everything else (deploy, vault, billing,
141-
team management) requires a session JWT.
142-
143-
How to get one:
144-
145-
1. Provision any resource anonymously. The response includes a JWT in the
146-
\`upgrade_jwt\` field.
147-
2. POST that JWT to /claim with an email. We send a magic link.
148-
3. Click the link in the email; the page sets a session cookie.
149-
150-
For unattended use (CI, agents), exchange the session cookie for a long-lived
151-
API key at \`POST /api/v1/api-keys\`. Pass it as \`Authorization: Bearer
152-
<key>\` on every request.
153-
154-
To verify a token works at any time, hit \`GET /api/v1/whoami\` — returns
155-
200 with your team_id + plan_tier on success, 401 on failure.
156-
`
157-
},
158-
{
159-
id: 'limits',
160-
title: 'Tiers and limits',
161-
body: `
162-
| Tier | Postgres | Redis | MongoDB | TTL | Price |
163-
| --------- | ---------- | --------- | ---------- | ---- | ----------- |
164-
| Anonymous | 10MB / 2c | 5MB | 5MB / 2c | 24h | free |
165-
| Hobby | 1GB / 8c | 50MB | 1GB / 8c | none | $9 / mo |
166-
| Pro | 5GB / 20c | 256MB | 5GB / 20c | none | $49 / mo |
167-
| Team | coming soon | — | — | — | $199 / mo |
168-
169-
"c" = simultaneous connections. The full table is at \`/pricing\`.
170-
171-
Limits are enforced at the Postgres user level (\`CONNECTION LIMIT\` on the
172-
role) and via per-bucket storage quotas. Exceeding a limit returns a 402 with
173-
an upgrade URL — your app keeps running, the next provision just fails.
174-
`
175-
},
176-
{
177-
id: 'machine-readable',
178-
title: 'Machine-readable API',
179-
body: `
180-
The full API surface is described in OpenAPI 3.1 at:
181-
182-
\`\`\`
183-
https://api.instanode.dev/openapi.json
184-
\`\`\`
185-
186-
It is the source of truth for paths, schemas, security schemes, and which
187-
endpoints accept anonymous traffic. Agents reading this spec alone can
188-
discover the claim flow (described under \`securitySchemes.bearerAuth\`),
189-
the \`/api/v1/whoami\` identity probe, and which fields like \`upgrade_jwt\`
190-
to pass forward.
191-
192-
If you're an AI agent reading this, the recommended bootstrap is:
25+
const RAW_DOCS = import.meta.glob('../../.content/docs/*.md', {
26+
eager: true,
27+
query: '?raw',
28+
import: 'default',
29+
}) as Record<string, string>
30+
31+
const SECTIONS: Section[] = buildSections(RAW_DOCS)
32+
33+
function buildSections(raw: Record<string, string>): Section[] {
34+
return Object.entries(raw)
35+
.map(([path, src]) => {
36+
const id = path.split('/').pop()!.replace(/\.md$/, '')
37+
const { meta, body } = parseFrontmatter(src)
38+
if (!meta.title) return null
39+
return { id, title: meta.title, body: body.trim(), order: Number(meta.order) || 0 }
40+
})
41+
.filter((s): s is Section & { order: number } => s !== null)
42+
.sort((a, b) => a.order - b.order)
43+
.map(({ order: _, ...section }) => section)
44+
}
19345

194-
1. \`GET /openapi.json\`
195-
2. Provision anonymous resources
196-
3. \`GET /api/v1/whoami\` to confirm token validity once you have one
197-
`
46+
function parseFrontmatter(src: string): { meta: Record<string, string>; body: string } {
47+
const m = src.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
48+
if (!m) return { meta: {}, body: src }
49+
const meta: Record<string, string> = {}
50+
for (const line of m[1].split(/\r?\n/)) {
51+
const sep = line.indexOf(':')
52+
if (sep < 0) continue
53+
const key = line.slice(0, sep).trim()
54+
const value = line.slice(sep + 1).trim()
55+
if (key) meta[key] = value
19856
}
199-
]
57+
return { meta, body: m[2] }
58+
}
59+
60+
// Unused legacy inline content removed — sections now load from .content/docs/.
20061

20162
export function DocsPage() {
20263
return (

0 commit comments

Comments
 (0)