build resilience: sitemap/home tolerate an unavailable database; deploy hooks migrate before build
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
4 changed files +5 −4
modified
docs/deployment.md
+1 −1
@@ -23,7 +23,7 @@ mkdir -p ~/apps/tools-spboucher-ai ~/backups/tools-spboucher-ai && chmod 700 ~/b | ||
| 23 | 23 | Secrets in `.env`: `DATABASE_URL` (tools_app), `DATABASE_URL_PUBLIC` (tools_reader), `DATABASE_URL_ADMIN` (owner role for migrations/backups), `SESSION_SECRET`, `WEBAUTHN_RP_ID=tools.spboucher.ai`, `WEBAUTHN_ORIGIN=https://tools.spboucher.ai`, `APP_URL`, `OWNER_EMAIL`, `OWNER_INITIAL_PASSWORD` (changed at first login), `BACKUP_PASSPHRASE`, `BACKUP_DIR`, `BACKUP_REMOTE`, `AI_PROVIDER=none`. |
| 24 | 24 | |
| 25 | 25 | ## Manifest (`infra/mld/tools-spboucher-ai.json` → `M1M32:~/dispatch/apps/`) |
| 26 | −PM2 `tools-web` = `node node_modules/next/dist/bin/next start -p 8540 -H 0.0.0.0`; `tools-worker` = `node node_modules/tsx/dist/cli.mjs src/worker/index.ts`. Hooks (post_sync): require `.env` → `pnpm install --frozen-lockfile` → `pnpm build` → `pnpm db:migrate` → `pnpm content:import --publish`. Placement pinned to M2M32c. `tunnel.domain = tools.spboucher.ai`. | |
| 26 | +PM2 `tools-web` = `node node_modules/next/dist/bin/next start -p 8540 -H 0.0.0.0`; `tools-worker` = `node node_modules/tsx/dist/cli.mjs src/worker/index.ts`. Hooks (post_sync): require `.env` → `pnpm install --frozen-lockfile` → `pnpm db:migrate` + `pnpm content:import --publish` + `pnpm bootstrap` → `pnpm build` (static generation reads the database, so migrations run first). Placement pinned to M2M32c. `tunnel.domain = tools.spboucher.ai`. | |
| 27 | 27 | |
| 28 | 28 | ## Release procedure (remote-first after first deploy) |
| 29 | 29 | First deploy (creates the app): |
modified
infra/mld/tools-spboucher-ai.json
+1 −1
@@ -94,8 +94,8 @@ | ||
| 94 | 94 | "post_sync": [ |
| 95 | 95 | "set -o pipefail; export PATH=\"/opt/homebrew/bin:$PATH\"; test -f .env || { echo 'ERREUR: .env absent (secrets) — copier ~/Desktop/Cluster/secrets/tools-spboucher-ai/.env'; exit 1; }; chmod 600 .env", |
| 96 | 96 | "set -o pipefail; export PATH=\"/opt/homebrew/bin:$PATH\"; pnpm install --frozen-lockfile --silent && test -d node_modules/next && echo ' deps ok'", |
| 97 | − "set -o pipefail; export PATH=\"/opt/homebrew/bin:$PATH\"; set -a; . ./.env; set +a; NEXT_TELEMETRY_DISABLED=1 pnpm build 2>&1 | tail -3 && test -f .next/BUILD_ID && echo ' build ok'", | |
| 98 | 97 | "set -o pipefail; export PATH=\"/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:$PATH\"; set -a; . ./.env; set +a; pnpm db:migrate && pnpm content:import --publish > /dev/null && pnpm bootstrap && echo ' db ok'", |
| 98 | + "set -o pipefail; export PATH=\"/opt/homebrew/bin:$PATH\"; set -a; . ./.env; set +a; NEXT_TELEMETRY_DISABLED=1 pnpm build 2>&1 | tail -3 && test -f .next/BUILD_ID && echo ' build ok'", | |
| 99 | 99 | "mkdir -p ~/backups/tools-spboucher-ai && chmod 700 ~/backups/tools-spboucher-ai && echo ' backup dir ok'" |
| 100 | 100 | ], |
| 101 | 101 | "post_start": [] |
modified
src/app/(public)/page.tsx
+1 −1
@@ -15,7 +15,7 @@ const PATHS = [ | ||
| 15 | 15 | ]; |
| 16 | 16 | |
| 17 | 17 | export default async function Home() { |
| 18 | − const { stats, featured, recent, tree, collections } = await homeData(); | |
| 18 | + const { stats, featured, recent, tree, collections } = await homeData().catch(() => ({ stats: { tools: 0, deep: 0, categories: 0, vendors: 0, collections: 0, lastVerified: null }, featured: [], recent: [], tree: [], collections: [] })); | |
| 19 | 19 | return ( |
| 20 | 20 | <> |
| 21 | 21 | <section className="relative overflow-hidden border-b border-line"> |
modified
src/app/sitemap.ts
+2 −1
@@ -3,7 +3,8 @@ import { allPublicSlugs, categoryTree, listCollections } from "@/lib/public/quer | ||
| 3 | 3 | import { APP_URL } from "@/lib/env"; |
| 4 | 4 | export const revalidate = 3600; |
| 5 | 5 | export default async function sitemap(): Promise<MetadataRoute.Sitemap> { |
| 6 | − const [tools, tree, cols] = await Promise.all([allPublicSlugs(), categoryTree(), listCollections()]); | |
| 6 | + // Never fail the build if the database is unavailable at generation time (revalidated hourly at runtime). | |
| 7 | + const [tools, tree, cols] = await Promise.all([allPublicSlugs(), categoryTree(), listCollections()]).catch(() => [[], [], []] as const); | |
| 7 | 8 | return [ |
| 8 | 9 | { url: `${APP_URL}/`, changeFrequency: "daily", priority: 1 }, { url: `${APP_URL}/tools`, changeFrequency: "daily", priority: 0.9 }, { url: `${APP_URL}/categories`, changeFrequency: "weekly", priority: 0.7 }, { url: `${APP_URL}/collections`, changeFrequency: "weekly", priority: 0.7 }, { url: `${APP_URL}/about`, changeFrequency: "monthly", priority: 0.4 }, |
| 9 | 10 | ...tree.flatMap((c) => [{ url: `${APP_URL}/categories/${c.slug}`, changeFrequency: "weekly" as const, priority: 0.6 }, ...c.children.map((x) => ({ url: `${APP_URL}/categories/${x.slug}`, changeFrequency: "weekly" as const, priority: 0.6 }))]), |
| 10 | 11 | |