Skip to content

Commit c3405b7

Browse files
committed
feat(website): brand-themed docs with AI handoff actions
- Map Starlight CSS variables to main shipnode.dev tokens (dark bg, Geist/JetBrains Mono, accent #d6a85d, sharp edges, grid backdrop). - Override Starlight PageTitle to add per-page actions: Copy as Markdown, View raw .md, Open in Claude, Open in ChatGPT. - Serve raw markdown at /docs/<slug>.md via a dynamic endpoint so Copy/View actions and AI ingestion work for any page. - Expand Quick start to a full happy-path walkthrough (server prep, install, config, env, deploy, rollback, CI, hardening). - Point readers at the shipnode Claude Code skill on the docs index and quick start.
1 parent 4f9512a commit c3405b7

6 files changed

Lines changed: 426 additions & 21 deletions

File tree

website/astro.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export default defineConfig({
1414
{ icon: 'github', label: 'GitHub', href: 'https://github.com/devalade/shipnode' },
1515
],
1616
customCss: ['./src/styles/docs.css'],
17+
components: {
18+
PageTitle: './src/components/docs/PageTitle.astro',
19+
},
1720
sidebar: [
1821
{
1922
label: 'Getting started',
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
import Default from '@astrojs/starlight/components/PageTitle.astro';
3+
4+
const { entry } = Astro.locals.starlightRoute;
5+
const slug = entry.id.replace(/\.(md|mdx)$/, '');
6+
const mdUrl = `/${slug}.md`;
7+
const pageUrl = new URL(Astro.url.pathname, Astro.site ?? 'https://shipnode.dev').toString();
8+
9+
const aiPrompt = `I'm reading the ShipNode docs at ${pageUrl}. Help me apply this to my project.`;
10+
const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(aiPrompt)}`;
11+
const chatgptUrl = `https://chatgpt.com/?q=${encodeURIComponent(aiPrompt)}`;
12+
---
13+
14+
<Default><slot /></Default>
15+
16+
<div class="page-actions" data-md-url={mdUrl}>
17+
<button type="button" class="copy-md" aria-label="Copy this page as Markdown">
18+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
19+
<rect x="9" y="9" width="12" height="12"></rect>
20+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
21+
</svg>
22+
<span class="copy-label">Copy as Markdown</span>
23+
</button>
24+
<a href={mdUrl} target="_blank" rel="noopener">
25+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
26+
<path d="M14 3h7v7"></path><path d="M21 3l-9 9"></path><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"></path>
27+
</svg>
28+
View raw .md
29+
</a>
30+
<a href={claudeUrl} target="_blank" rel="noopener">
31+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
32+
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
33+
</svg>
34+
Open in Claude
35+
</a>
36+
<a href={chatgptUrl} target="_blank" rel="noopener">
37+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
38+
<circle cx="12" cy="12" r="9"></circle>
39+
<path d="M8 12h8M12 8v8"></path>
40+
</svg>
41+
Open in ChatGPT
42+
</a>
43+
</div>
44+
45+
<script>
46+
document.querySelectorAll<HTMLButtonElement>('.page-actions .copy-md').forEach((btn) => {
47+
btn.addEventListener('click', async () => {
48+
const wrap = btn.closest<HTMLElement>('.page-actions');
49+
const url = wrap?.dataset.mdUrl;
50+
const label = btn.querySelector<HTMLElement>('.copy-label');
51+
if (!url || !label) return;
52+
try {
53+
const res = await fetch(url);
54+
const text = await res.text();
55+
await navigator.clipboard.writeText(text);
56+
const original = label.textContent;
57+
label.textContent = 'Copied';
58+
btn.classList.add('copied');
59+
setTimeout(() => {
60+
label.textContent = original;
61+
btn.classList.remove('copied');
62+
}, 1800);
63+
} catch {
64+
label.textContent = 'Copy failed';
65+
setTimeout(() => (label.textContent = 'Copy as Markdown'), 1800);
66+
}
67+
});
68+
});
69+
</script>

website/src/content/docs/docs/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ You have a VPS (Ubuntu or Debian), a Node.js app, and you want a real deploy wor
2222
- [Install](/docs/install/)
2323
- [Quick start](/docs/quick-start/)
2424
- [Configuration](/docs/configuration/)
25+
26+
## Pair these docs with an AI
27+
28+
Every page on this site has actions in the header to **Copy as Markdown**, **View the raw .md**, or jump straight into **Claude** or **ChatGPT** with the current page pre-loaded as context.
29+
30+
Working inside [Claude Code](https://claude.com/claude-code)? The `shipnode` skill ships with the CLI and knows the full command surface — deploys, rollbacks, Caddy/PM2 templates, `.env` management, server inspection. It auto-triggers when you describe a deploy task, or run `/shipnode` to invoke it directly.
Lines changed: 153 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,178 @@
11
---
22
title: Quick start
3-
description: Three commands from empty server to deployed app.
3+
description: Everything you need to go from an empty Ubuntu VPS to a deployed Node.js app, in about ten minutes.
44
---
55

6-
After [installing shipnode](/docs/install/) in your project:
6+
This walkthrough covers the **full happy path** — provisioning a fresh server, generating a config, deploying your first release, rolling back, and wiring CI. Skip ahead if a section doesn't apply.
7+
8+
:::tip[Use the ShipNode AI skill in Claude Code]
9+
Inside [Claude Code](https://claude.com/claude-code), the `shipnode` skill knows this CLI end-to-end — deploying, rolling back, configuring Caddy/PM2, managing `.env`, and reading server state. Type `/shipnode` in Claude Code or just describe what you want; the skill is auto-triggered when relevant. Every page on this site also has **Copy as Markdown / Open in Claude / Open in ChatGPT** buttons at the top so you can hand the docs to any AI assistant.
10+
:::
11+
12+
## 0. Prerequisites
13+
14+
| You need | Why |
15+
|---|---|
16+
| **A VPS** running Ubuntu 22.04+ or Debian 12+ | ShipNode provisions Node, PM2, Caddy on this OS. |
17+
| **A non-root SSH user** with `sudo` and a public key in `~/.ssh/authorized_keys` | All `shipnode` commands run as this user. |
18+
| **A domain** pointing an A record at the VPS IP | Caddy will issue an HTTPS cert automatically. |
19+
| **Node.js 18+ locally** | The CLI is a Node.js package. |
20+
21+
If your VPS only has a root user right now, create the deploy user first:
22+
23+
```bash
24+
ssh root@your.vps.ip
25+
adduser deploy
26+
usermod -aG sudo deploy
27+
mkdir -p /home/deploy/.ssh
28+
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
29+
chown -R deploy:deploy /home/deploy/.ssh
30+
chmod 700 /home/deploy/.ssh && chmod 600 /home/deploy/.ssh/authorized_keys
31+
```
32+
33+
## 1. Install shipnode in your project
34+
35+
```bash
36+
# npm
37+
npm install -D @devalade/shipnode
38+
39+
# pnpm
40+
pnpm add -D @devalade/shipnode
41+
42+
# yarn
43+
yarn add -D @devalade/shipnode
44+
45+
# bun
46+
bun add -d @devalade/shipnode
47+
```
48+
49+
`shipnode` lives in your project as a dev dependency so every collaborator and your CI uses the same version.
50+
51+
## 2. Generate the config
752

853
```bash
9-
# 1. Generate config (interactive — detects framework, package manager, app type)
1054
npx shipnode init
55+
```
56+
57+
This prompts for framework, package manager, app type, SSH target, domain, and port — then writes `shipnode.config.ts`. You can rerun `init` or edit the file by hand at any time. See the [configuration reference](/docs/configuration/) for every option.
58+
59+
A minimal backend config:
60+
61+
```ts
62+
import { shipnode } from '@devalade/shipnode';
1163

12-
# 2. Provision the server (Node.js, PM2, Caddy, package manager)
64+
export default shipnode
65+
.backend()
66+
.ssh({ host: '203.0.113.10', user: 'deploy' })
67+
.deployTo('/var/www/api')
68+
.pm2('api', { instances: 2 })
69+
.port(3000)
70+
.domain('api.example.com')
71+
.healthCheck('/health')
72+
.nodeVersion('22')
73+
.pkgManager('pnpm')
74+
.build();
75+
```
76+
77+
## 3. Provision the server
78+
79+
```bash
1380
npx shipnode setup
81+
```
82+
83+
One-time, idempotent. Installs **mise**, **Node.js**, **PM2** (+ `pm2-logrotate`), **Caddy**, and your package manager. Re-running it is safe — it skips anything already present.
84+
85+
Verify with:
86+
87+
```bash
88+
npx shipnode doctor
89+
```
90+
91+
If anything is red, fix it before deploying.
92+
93+
## 4. Upload secrets
1494

15-
# 3. Deploy
95+
If your app reads from `.env`, push it once:
96+
97+
```bash
98+
npx shipnode env
99+
```
100+
101+
The file lands at `<deployPath>/shared/.env` and is symlinked into every release. PM2 picks up changes on the next `restart` or `deploy` (both use `--update-env`).
102+
103+
## 5. Deploy
104+
105+
```bash
16106
npx shipnode deploy
17107
```
18108

19-
That's the full path. `init` writes `shipnode.config.ts`. `setup` is a one-time server provision. `deploy` builds, syncs, releases, reloads PM2, and health-checks before declaring the release healthy.
109+
What happens:
110+
111+
```
112+
rsync ./ -> /var/www/api/releases/20260524160000
113+
install pnpm install --frozen-lockfile
114+
build pnpm run build
115+
symlink current -> releases/20260524160000
116+
pm2 reload api --update-env
117+
health GET /health 200 OK 47ms
118+
deployed https://api.example.com
119+
```
120+
121+
If the health check fails, the symlink stays on the previous release and the failed one is discarded. No partial outage.
20122

21-
## What the deploy actually does
123+
## 6. Confirm and operate
22124

125+
```bash
126+
npx shipnode status # PM2 state + current release
127+
npx shipnode logs # stream logs
128+
npx shipnode metrics # PM2 CPU/memory dashboard
23129
```
24-
rsync ./ -> /var/www/api/releases/20260524160000
25-
install pnpm install --frozen-lockfile
26-
build pnpm run build
27-
symlink current -> releases/20260524160000
28-
pm2 reload api --update-env
29-
health GET /health 200 OK 47ms
30-
deployed https://api.example.com
130+
131+
Need to run a one-off command on the server inside the current release?
132+
133+
```bash
134+
npx shipnode run "node scripts/migrate.js"
31135
```
32136

33-
If the health check fails, the symlink stays on the previous release and the new one is discarded.
137+
## 7. Roll back
34138

35-
## Roll back
139+
Something off in production?
36140

37141
```bash
38142
npx shipnode rollback --steps 1
39143
```
40144

41-
## Next
145+
The `current` symlink moves back one release and PM2 reloads. The default `keepReleases` is 5, so you have headroom.
146+
147+
## 8. Wire CI (optional)
148+
149+
Generate a ready-to-use GitHub Actions workflow:
150+
151+
```bash
152+
npx shipnode ci github
153+
npx shipnode ci env-sync --all
154+
```
155+
156+
This drops `.github/workflows/deploy.yml` and pushes your `.env` keys to GitHub repository secrets. See the [CI/CD guide](/docs/ci-cd/) for the secrets it expects.
157+
158+
## 9. Harden the server (recommended)
159+
160+
```bash
161+
npx shipnode harden
162+
```
163+
164+
Locks down SSH (key-only, no root), enables UFW, installs `fail2ban`, and turns on unattended security upgrades.
165+
166+
Audit it anytime:
167+
168+
```bash
169+
npx shipnode doctor --security
170+
```
171+
172+
## What's next
42173

43-
- [Configuration](/docs/configuration/) — the full `shipnode.config.ts` reference
44-
- [Multi-environment](/docs/environments/) — staging + production from one repo
45-
- [CI/CD](/docs/ci-cd/) — generate a GitHub Actions workflow
174+
- [shipnode.config.ts reference](/docs/configuration/) — every method, every option
175+
- [Multi-environment](/docs/environments/) — add a staging deploy
176+
- [Workers](/docs/workers/) — long-running PM2 processes alongside the web app
177+
- [Cloudflare Tunnel](/docs/cloudflare/) — close inbound ports entirely
178+
- [Backups](/docs/backups/) — scheduled `pg_dump` + file backups to S3
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { APIRoute, GetStaticPaths } from 'astro';
2+
import { getCollection } from 'astro:content';
3+
4+
export const getStaticPaths: GetStaticPaths = async () => {
5+
const entries = await getCollection('docs');
6+
return entries.map((entry) => {
7+
// entry.id is like "docs/install.md" — strip the leading "docs/" and extension.
8+
const slug = entry.id.replace(/^docs\//, '').replace(/\.(md|mdx)$/, '');
9+
return { params: { slug }, props: { entry } };
10+
});
11+
};
12+
13+
export const GET: APIRoute = ({ props }) => {
14+
const entry = props.entry as { body: string; data: { title?: string; description?: string } };
15+
const fm = [
16+
'---',
17+
entry.data.title ? `title: ${JSON.stringify(entry.data.title)}` : null,
18+
entry.data.description ? `description: ${JSON.stringify(entry.data.description)}` : null,
19+
'---',
20+
'',
21+
]
22+
.filter(Boolean)
23+
.join('\n');
24+
return new Response(`${fm}\n${entry.body ?? ''}`, {
25+
headers: { 'content-type': 'text/markdown; charset=utf-8' },
26+
});
27+
};

0 commit comments

Comments
 (0)