feat(core): scaffold Astro platform — snippet parser, validation gate, server, pm2, deploy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
11 changed files +7,226 −0
added
.gitignore
+15 −0
@@ -0,0 +1,15 @@ | |||||
| 1 | +# ============================================================ | ||||
| 2 | +# SVGarden — https://www.svgarden.dev | ||||
| 3 | +# Author : Simon-Pierre Boucher | ||||
| 4 | +# Contact: contact@spboucher.ai | ||||
| 5 | +# File : .gitignore | ||||
| 6 | +# Desc : Never commit secrets, build output, or dependencies | ||||
| 7 | +# ============================================================ | ||||
| 8 | +node_modules/ | ||||
| 9 | +dist/ | ||||
| 10 | +.astro/ | ||||
| 11 | +.env | ||||
| 12 | +.env.* | ||||
| 13 | +ngrok.yml | ||||
| 14 | +*.log | ||||
| 15 | +.DS_Store | ||||
added
README.md
+96 −0
@@ -0,0 +1,96 @@ | |||||
| 1 | +# SVGarden 🌱 | ||||
| 2 | + | ||||
| 3 | +> A massive, searchable bank of SVG + CSS animations with copy-ready code snippets. | ||||
| 4 | +> **Production:** https://www.svgarden.dev — **Author:** Simon-Pierre Boucher — contact@spboucher.ai | ||||
| 5 | + | ||||
| 6 | +Every animation is one self-contained `.html` file: paste it into a blank page and it works. | ||||
| 7 | +No frameworks, no CDNs, no build step required by the snippets themselves — and each one | ||||
| 8 | +ships with a short "How it works" lesson. | ||||
| 9 | + | ||||
| 10 | +## Quick start | ||||
| 11 | + | ||||
| 12 | +```bash | ||||
| 13 | +npm install | ||||
| 14 | +npm run dev # local dev server on :4321 | ||||
| 15 | +npm run build # validate snippets + build static site into dist/ | ||||
| 16 | +npm run serve # serve dist/ with the production Express server (:4321, /healthz) | ||||
| 17 | +``` | ||||
| 18 | + | ||||
| 19 | +## Adding a snippet | ||||
| 20 | + | ||||
| 21 | +```bash | ||||
| 22 | +npm run new # interactive scaffolder → snippets/<category>/<slug>.html | ||||
| 23 | +npm run validate # the same gate the build runs | ||||
| 24 | +``` | ||||
| 25 | + | ||||
| 26 | +One file = one animation. The gallery, detail page, category pages, search index and | ||||
| 27 | +tag filters all regenerate at build time — never edit generated pages. | ||||
| 28 | + | ||||
| 29 | +Snippet hard rules (enforced by `scripts/validate-snippets.mjs`, which **fails the build**): | ||||
| 30 | + | ||||
| 31 | +- SVGarden author header at the top (attribution travels with every copy/download); | ||||
| 32 | +- `<!--svgarden ... -->` YAML frontmatter (title, slug, category, tags, difficulty, | ||||
| 33 | + techniques, how_it_works, customizable, created); | ||||
| 34 | +- self-contained (no external assets), all classes prefixed `sg-<slug>`, | ||||
| 35 | +- customizable values as `--sg-*` custom properties declared on the root element, | ||||
| 36 | +- ≤ 120 lines, `js` tag iff the snippet contains a `<script>`, | ||||
| 37 | +- every `<svg>` is `aria-hidden="true"` or `role="img"` + `<title>`. | ||||
| 38 | + | ||||
| 39 | +## Architecture | ||||
| 40 | + | ||||
| 41 | +| Piece | Where | | ||||
| 42 | +|---|---| | ||||
| 43 | +| Snippet bank | `snippets/<category>/<slug>.html` | | ||||
| 44 | +| Parser / single source of truth | `src/lib/snippets.mjs` | | ||||
| 45 | +| Site (Astro, static output) | `src/pages`, `src/components`, `src/layouts` | | ||||
| 46 | +| Search index endpoint | `src/pages/search-index.json.js` (Fuse.js, lazy-loaded client-side) | | ||||
| 47 | +| Build info for `/healthz` | `src/pages/build-info.json.js` | | ||||
| 48 | +| Production server | `server.mjs` (Express, gzip/brotli, immutable caching for hashed assets) | | ||||
| 49 | +| Process manager | `ecosystem.config.cjs` (pm2: `svgarden-web` + `svgarden-tunnel`) | | ||||
| 50 | +| Deploy | `scripts/deploy.sh` | | ||||
| 51 | + | ||||
| 52 | +## Deployment (node M3U96b + ngrok) | ||||
| 53 | + | ||||
| 54 | +The site runs on cluster node **M3U96b**, served at `127.0.0.1:4321` and exposed publicly | ||||
| 55 | +through an ngrok tunnel at **https://www.svgarden.dev**. | ||||
| 56 | + | ||||
| 57 | +```bash | ||||
| 58 | +./scripts/deploy.sh | ||||
| 59 | +``` | ||||
| 60 | + | ||||
| 61 | +The script syncs the source to the node (via `git pull` once a git remote exists on the | ||||
| 62 | +node; via `rsync` until then), runs `npm ci` + `npm run build`, restarts both pm2 apps, | ||||
| 63 | +then health-checks `http://127.0.0.1:4321/healthz` and `https://www.svgarden.dev/healthz`. | ||||
| 64 | + | ||||
| 65 | +### One-time ngrok requirements (operator steps) | ||||
| 66 | + | ||||
| 67 | +1. ngrok installed and authenticated on the node (`ngrok config add-authtoken <token>`). | ||||
| 68 | + **Never** commit or share the token. | ||||
| 69 | +2. `www.svgarden.dev` reserved as a **custom domain** in the ngrok dashboard | ||||
| 70 | + (Universal Gateway → Domains). | ||||
| 71 | +3. DNS: a `CNAME` for `www.svgarden.dev` pointing at the target ngrok gives you when | ||||
| 72 | + reserving the domain. This step happens at your DNS provider; the deploy script | ||||
| 73 | + degrades gracefully (local server stays up) until it propagates. | ||||
| 74 | +4. Tunnel definition in the node's ngrok config (`~/Library/Application Support/ngrok/ngrok.yml` | ||||
| 75 | + on macOS — add the block, don't overwrite the file): | ||||
| 76 | + | ||||
| 77 | +```yaml | ||||
| 78 | +tunnels: | ||||
| 79 | + svgarden: | ||||
| 80 | + proto: http | ||||
| 81 | + addr: 4321 | ||||
| 82 | + domain: www.svgarden.dev | ||||
| 83 | +``` | ||||
| 84 | + | ||||
| 85 | +pm2 then keeps both alive across reboots (`pm2 save` + `pm2 startup` already configured | ||||
| 86 | +on the node): `svgarden-web` runs `server.mjs`, `svgarden-tunnel` runs | ||||
| 87 | +`ngrok start svgarden --log=stdout`. | ||||
| 88 | + | ||||
| 89 | +## Quality gates | ||||
| 90 | + | ||||
| 91 | +1. `npm run validate` passes; | ||||
| 92 | +2. `npm run build` completes with zero warnings; | ||||
| 93 | +3. new snippets visually verified on light **and** dark preview backgrounds; | ||||
| 94 | +4. copy-button output pastes into a blank `.html` file and renders correctly. | ||||
| 95 | + | ||||
| 96 | +© Simon-Pierre Boucher — contact@spboucher.ai — svgarden.dev | ||||
added
astro.config.mjs
+22 −0
@@ -0,0 +1,22 @@ | |||||
| 1 | +/** | ||||
| 2 | + * ============================================================ | ||||
| 3 | + * SVGarden — https://www.svgarden.dev | ||||
| 4 | + * Author : Simon-Pierre Boucher | ||||
| 5 | + * Contact: contact@spboucher.ai | ||||
| 6 | + * File : astro.config.mjs | ||||
| 7 | + * Desc : Astro configuration — static output for svgarden.dev | ||||
| 8 | + * ============================================================ | ||||
| 9 | + */ | ||||
| 10 | +import { defineConfig } from 'astro/config'; | ||||
| 11 | + | ||||
| 12 | +export default defineConfig({ | ||||
| 13 | + site: 'https://www.svgarden.dev', | ||||
| 14 | + output: 'static', | ||||
| 15 | + trailingSlash: 'never', | ||||
| 16 | + build: { | ||||
| 17 | + format: 'file', | ||||
| 18 | + }, | ||||
| 19 | + server: { | ||||
| 20 | + port: 4321, | ||||
| 21 | + }, | ||||
| 22 | +}); | ||||
added
ecosystem.config.cjs
+29 −0
@@ -0,0 +1,29 @@ | |||||
| 1 | +/** | ||||
| 2 | + * ============================================================ | ||||
| 3 | + * SVGarden — https://www.svgarden.dev | ||||
| 4 | + * Author : Simon-Pierre Boucher | ||||
| 5 | + * Contact: contact@spboucher.ai | ||||
| 6 | + * File : ecosystem.config.cjs | ||||
| 7 | + * Desc : pm2 config — static server (svgarden-web) + ngrok tunnel (svgarden-tunnel) | ||||
| 8 | + * ============================================================ | ||||
| 9 | + */ | ||||
| 10 | +module.exports = { | ||||
| 11 | + apps: [ | ||||
| 12 | + { | ||||
| 13 | + name: 'svgarden-web', | ||||
| 14 | + script: 'server.mjs', | ||||
| 15 | + interpreter: 'node', | ||||
| 16 | + env: { PORT: 4321, HOST: '127.0.0.1', NODE_ENV: 'production' }, | ||||
| 17 | + max_restarts: 10, | ||||
| 18 | + restart_delay: 2000, | ||||
| 19 | + }, | ||||
| 20 | + { | ||||
| 21 | + name: 'svgarden-tunnel', | ||||
| 22 | + script: '/opt/homebrew/bin/ngrok', | ||||
| 23 | + interpreter: 'none', | ||||
| 24 | + args: ['start', 'svgarden', '--log=stdout'], | ||||
| 25 | + max_restarts: 10, | ||||
| 26 | + restart_delay: 5000, | ||||
| 27 | + }, | ||||
| 28 | + ], | ||||
| 29 | +}; | ||||
added
package-lock.json
+6533 −0
Large diff (6,533 lines) is not loaded by default to keep the page fast.Load diff
added
package.json
+27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/deploy.sh
+66 −0
Large diff (66 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/new-snippet.mjs
+82 −0
Large diff (82 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/validate-snippets.mjs
+122 −0
Large diff (122 lines) is not loaded by default to keep the page fast.Load diff
added
server.mjs
+65 −0
Large diff (65 lines) is not loaded by default to keep the page fast.Load diff
added
src/lib/snippets.mjs
+169 −0
Large diff (170 lines) is not loaded by default to keep the page fast.Load diff