Skip to content

Commit 7e30103

Browse files
lwwmanningclaude
andauthored
Overhaul site infrastructure (#47)
Massive overhaul of vortex.dev site infrastructure to improve developer experience and fix various shortcomings found along the way. Phase 1 — Tooling: - Bun-only: drop package-lock.json, pin packageManager + engines, add .nvmrc - Biome replaces ESLint+Prettier (drop eslint.config.mjs, .prettierrc) - Tighten tsconfig (noUnusedLocals/Parameters, alwaysStrict, etc.) - Switch from Renovate to Dependabot with grouping + 14-day cooldown - Add vercel.json Phase 2 — SEO/robustness: - Replace static sitemap.xml with dynamic sitemap.ts (was missing all 7 blog post URLs — search crawlers couldn't see them) - Add error.tsx global boundary - Dynamic OG image generation via next/og at /, /blog, /blog/[slug] - RSS feed (xml/json/atom) at /rss/[type] - JSON-LD Article schema + reading-time on blog posts - Move MDXRenderer to a server component so the velite-emitted code is evaluated on the server, not in the browser. This lets CSP drop 'unsafe-eval' entirely and removes that XSS surface area. - Harden security headers: HSTS, Referrer-Policy, Permissions-Policy, frame-ancestors, base-uri, form-action Phase 3 — Core CI: - ci.yml (SHA-pinned), claude.yml, dependabot-auto-merge.yml - scripts/verify.ts smoke suite (sitemap/robots/RSS/OG/JSON-LD/links) - Lighthouse + Playwright deferred to a follow-up Real bug fixed along the way: api/subscribe/route.ts instantiated the Resend client at module load, which crashed the build whenever RESEND_API_KEY was unset. Lazy-initialize inside the handler. Local verify suite: 15/15 checks passing. Signed-off-by: Will Manning <will@willmanning.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 40adb07 commit 7e30103

43 files changed

Lines changed: 1245 additions & 9299 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: bun
4+
directory: "/"
5+
schedule:
6+
interval: weekly
7+
day: monday
8+
time: "08:00"
9+
timezone: "America/New_York"
10+
open-pull-requests-limit: 5
11+
# Wait 14 days after a release before proposing a bump. Avoids dragging in
12+
# day-old versions that get yanked or hot-patched within their first week.
13+
cooldown:
14+
default-days: 14
15+
# Logical groups so related bumps land together. Patterns are matched
16+
# against package names; a package can only belong to one group, so order
17+
# matters (more-specific groups first).
18+
groups:
19+
react:
20+
patterns:
21+
- "react"
22+
- "react-dom"
23+
- "@types/react"
24+
- "@types/react-dom"
25+
content-pipeline:
26+
patterns:
27+
- "remark-*"
28+
- "rehype-*"
29+
- "@next/mdx"
30+
- "@mdx-js/*"
31+
- "react-markdown"
32+
- "velite"
33+
lint-format:
34+
patterns:
35+
- "@biomejs/*"
36+
minor-and-patch:
37+
update-types:
38+
- minor
39+
- patch
40+
labels:
41+
- dependencies
42+
43+
- package-ecosystem: github-actions
44+
directory: "/"
45+
schedule:
46+
interval: weekly
47+
day: monday
48+
time: "08:00"
49+
timezone: "America/New_York"
50+
cooldown:
51+
default-days: 14
52+
groups:
53+
actions:
54+
update-types:
55+
- minor
56+
- patch
57+
- major
58+
labels:
59+
- dependencies
60+
- github-actions

.github/workflows/ci.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
concurrency:
9+
group: ci-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
# Least-privilege: this workflow only reads the repo to lint/build/verify; it
13+
# never writes back. Anything that needs write scopes lives in a separate
14+
# workflow with its own scoped permissions.
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
ci:
20+
name: Lint, build, verify
21+
runs-on: ubuntu-latest
22+
timeout-minutes: 10
23+
steps:
24+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
25+
26+
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
27+
with:
28+
bun-version: 1.3.13
29+
30+
# Bun is the package manager and script runner, but Next.js (and tsc)
31+
# run on Node. ubuntu-latest's default Node version drifts; pin via
32+
# .nvmrc so a future GitHub bump can't break the build silently.
33+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
34+
with:
35+
node-version-file: .nvmrc
36+
37+
- name: Install
38+
run: bun install --frozen-lockfile
39+
40+
- name: Lint + format check
41+
run: bun run check:ci
42+
43+
# Build BEFORE typecheck: the build runs velite (populating .velite/
44+
# which the `#site/content` path alias resolves to) and produces
45+
# next-env.d.ts. tsc fails without these, so order matters here.
46+
- name: Build
47+
run: bun run build
48+
49+
- name: Type check
50+
run: bun run typecheck
51+
52+
- name: Start server
53+
run: |
54+
bun run start > /tmp/server.log 2>&1 &
55+
echo $! > /tmp/server.pid
56+
for i in $(seq 1 30); do
57+
if curl -sf http://localhost:3000 > /dev/null; then
58+
echo "server ready"
59+
exit 0
60+
fi
61+
sleep 1
62+
done
63+
echo "server failed to start in 30s"
64+
cat /tmp/server.log
65+
exit 1
66+
67+
- name: Verify endpoints
68+
run: bun run verify
69+
70+
- name: Stop server
71+
if: always()
72+
run: |
73+
if [ -f /tmp/server.pid ]; then
74+
kill $(cat /tmp/server.pid) 2>/dev/null || true
75+
fi
76+
77+
- name: Server logs (on failure)
78+
if: failure()
79+
run: cat /tmp/server.log || true

.github/workflows/claude.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Claude
2+
3+
# Responds to @claude mentions in issues, PR descriptions, comments, and reviews.
4+
# Claude reads the surrounding context, can push commits to the PR branch, and
5+
# replies inline.
6+
#
7+
# Required setup (one-time):
8+
# - Repo secret ANTHROPIC_API_KEY (or CLAUDE_CODE_OAUTH_TOKEN) — set in
9+
# Settings → Secrets and variables → Actions.
10+
# - The GitHub App "Claude" installed on the repo, OR the default
11+
# GITHUB_TOKEN with the permissions block below.
12+
13+
on:
14+
issue_comment:
15+
types: [created]
16+
pull_request_review_comment:
17+
types: [created]
18+
pull_request_review:
19+
types: [submitted]
20+
issues:
21+
types: [opened, assigned]
22+
23+
concurrency:
24+
group: claude-${{ github.event.issue.number || github.event.pull_request.number }}
25+
cancel-in-progress: false
26+
27+
jobs:
28+
claude:
29+
name: Run Claude
30+
if: |
31+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
32+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
33+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
34+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
35+
runs-on: ubuntu-latest
36+
timeout-minutes: 30
37+
permissions:
38+
contents: write
39+
pull-requests: write
40+
issues: write
41+
id-token: write
42+
actions: read
43+
steps:
44+
- uses: actions/checkout@v6
45+
with:
46+
fetch-depth: 1
47+
48+
- uses: anthropics/claude-code-action@v1
49+
with:
50+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
51+
github_token: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Dependabot auto-merge
2+
3+
# Auto-enables merge on Dependabot PRs for patch + minor bumps.
4+
# Major bumps stay open for manual review.
5+
#
6+
# Required setup (one-time, in repo settings):
7+
# 1. Settings → General → Pull Requests → "Allow auto-merge" ✓
8+
# 2. (optional but recommended) Branch protection on main with the
9+
# CI status check marked Required. Otherwise merge fires immediately
10+
# without waiting for CI.
11+
#
12+
# Uses pull_request_target rather than pull_request because Dependabot
13+
# PRs are treated as fork PRs by default, so pull_request runs with a
14+
# read-only GITHUB_TOKEN that can't enable auto-merge. pull_request_target
15+
# is safe here because we never check out or run PR-side code — we only
16+
# read metadata from the GitHub API.
17+
18+
on:
19+
pull_request_target:
20+
types: [opened, reopened, synchronize]
21+
22+
permissions:
23+
contents: write
24+
pull-requests: write
25+
26+
jobs:
27+
auto-merge:
28+
name: Enable auto-merge for patch + minor bumps
29+
runs-on: ubuntu-latest
30+
if: github.actor == 'dependabot[bot]'
31+
steps:
32+
- name: Get Dependabot metadata
33+
id: metadata
34+
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
35+
with:
36+
github-token: ${{ secrets.GITHUB_TOKEN }}
37+
38+
- name: Enable auto-merge (patch + minor)
39+
if: |
40+
steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
41+
steps.metadata.outputs.update-type == 'version-update:semver-minor'
42+
run: gh pr merge --auto --squash "$PR_URL"
43+
env:
44+
PR_URL: ${{ github.event.pull_request.html_url }}
45+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46+
47+
- name: Comment on major bumps
48+
if: steps.metadata.outputs.update-type == 'version-update:semver-major'
49+
run: |
50+
gh pr comment "$PR_URL" --body "🛑 Major version bump — auto-merge skipped, please review manually.
51+
52+
- Package: \`${{ steps.metadata.outputs.dependency-names }}\`
53+
- From: \`${{ steps.metadata.outputs.previous-version }}\`
54+
- To: \`${{ steps.metadata.outputs.new-version }}\`"
55+
env:
56+
PR_URL: ${{ github.event.pull_request.html_url }}
57+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22

.prettierrc

Lines changed: 0 additions & 8 deletions
This file was deleted.

.vscode/extensions.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
11
{
2-
"recommendations": [
3-
"tailwindcss.tailwindcss-intellisense",
4-
"esbenp.prettier-vscode"
5-
]
2+
"recommendations": ["tailwindcss.tailwindcss-intellisense", "biomejs.biome"]
63
}

.vscode/settings.json

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
{
22
"editor.formatOnSave": true,
3-
"editor.defaultFormatter": "esbenp.prettier-vscode",
3+
"editor.defaultFormatter": "biomejs.biome",
44
"editor.codeActionsOnSave": {
5-
"source.fixAll.eslint": "explicit",
6-
"source.fixAll.stylelint": "explicit",
7-
"source.removeUnusedImports": "explicit",
8-
"source.organizeImports": "explicit"
5+
"source.fixAll.biome": "explicit"
96
},
10-
"css.validate": false,
117
"[css]": {
128
"editor.formatOnSave": false
139
},
@@ -17,8 +13,5 @@
1713
".*ClassName",
1814
"ngClass",
1915
".*Styles"
20-
],
21-
"[scss]": {
22-
"editor.defaultFormatter": "esbenp.prettier-vscode"
23-
}
16+
]
2417
}

biome.json

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"useIgnoreFile": true
7+
},
8+
"files": {
9+
"ignoreUnknown": true,
10+
"includes": [
11+
"**",
12+
"!src/content",
13+
"!public",
14+
"!bun.lock",
15+
"!.next",
16+
"!.velite",
17+
"!out",
18+
"!build",
19+
"!next-env.d.ts",
20+
"!src/app/globals.css"
21+
]
22+
},
23+
"formatter": {
24+
"enabled": true,
25+
"indentStyle": "space",
26+
"indentWidth": 2,
27+
"lineWidth": 80,
28+
"lineEnding": "lf"
29+
},
30+
"javascript": {
31+
"formatter": {
32+
"quoteStyle": "double",
33+
"jsxQuoteStyle": "double",
34+
"trailingCommas": "none",
35+
"arrowParentheses": "always",
36+
"semicolons": "always"
37+
}
38+
},
39+
"css": {
40+
"parser": {
41+
"cssModules": true,
42+
"tailwindDirectives": true
43+
},
44+
"linter": {
45+
"enabled": true
46+
}
47+
},
48+
"linter": {
49+
"enabled": true,
50+
"rules": {
51+
"recommended": true,
52+
"suspicious": {
53+
"noExplicitAny": "off",
54+
"noArrayIndexKey": "off",
55+
"noAssignInExpressions": "off",
56+
"noShadowRestrictedNames": "off"
57+
},
58+
"style": {
59+
"useImportType": "warn",
60+
"noNonNullAssertion": "off",
61+
"noParameterAssign": "off",
62+
"noDescendingSpecificity": "off"
63+
},
64+
"correctness": {
65+
"noUnusedVariables": {
66+
"level": "error",
67+
"options": {
68+
"ignoreRestSiblings": true
69+
}
70+
},
71+
"noUnusedImports": "error",
72+
"useExhaustiveDependencies": "off",
73+
"useHookAtTopLevel": "error"
74+
},
75+
"a11y": {
76+
"useKeyWithClickEvents": "error",
77+
"noStaticElementInteractions": "error",
78+
"useButtonType": "error",
79+
"noSvgWithoutTitle": "error"
80+
},
81+
"complexity": {
82+
"noForEach": "off",
83+
"noUselessFragments": "off"
84+
},
85+
"performance": {
86+
"noImgElement": "off"
87+
},
88+
"security": {
89+
"noDangerouslySetInnerHtml": "off"
90+
}
91+
}
92+
},
93+
"assist": {
94+
"enabled": true,
95+
"actions": {
96+
"source": {
97+
"organizeImports": "on"
98+
}
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)