ranzlappen.com

Personal blog + PolyVote community voting platform + Blog Admin dashboard + Inventory Manager, hosted on GitHub Pages.

Architecture

Hybrid project with four independent builds plus a build-time tooling module:

Build & Development

Blog (Jekyll)

bundle exec jekyll serve          # Local dev server at :4000

PolyVote (React/Vite)

cd polyvote
npm install                       # Install dependencies
npm run dev                       # Local dev server at :5173
npm run build                     # Production build (tsc + vite build)
npm run lint                      # ESLint (flat config)
npm test                          # Vitest (unit tests)
npm run format                    # Prettier formatting

Blog Admin (React/Vite)

cd blog-admin
npm install                       # Install dependencies
npm run dev                       # Local dev server
npm run build                     # Production build (tsc + vite build)
npm run lint                      # ESLint (flat config)
npm run format                    # Prettier formatting

Inventory Manager (React/Vite)

cd inventory-manager
npm install                       # Install dependencies
npm run dev                       # Local dev server
npm run build                     # Production build (tsc + vite build)
npm run lint                      # ESLint (flat config)
npm run format                    # Prettier formatting

Search Crawler (Node)

cd search-crawler
npm run crawl                     # Build ../search-external.json (web works; repos/gists need GITHUB_TOKEN)
GITHUB_TOKEN=<pat> npm run crawl  # Include the repos/gists groups (raises API limit 60β†’5000/hr)
npm run lint                      # node --check syntax pass (what CI runs)

Cloud Functions

cd polyvote/functions
npm install
npm run build                     # Compile TypeScript
npm run lint                      # tsc --noEmit
npm test                          # Vitest (unit tests)

Production deploys of castBlogVote, the Blog Admin callables (blogSaveDraft, blogListDrafts, blogGetDraft, blogDeleteDraft, blogListExistingPosts, blogFetchExistingPost, blogImportPostForEdit, blogPublishToGitHub, blogUploadImage, blogListSeriesUsage), the admin user-management callables shared with the Blog Admin Users panel (setUserRole, adminListUsers, adminBanUser, adminUnbanUser), and the Inventory Manager callables (inventoryListFolders, inventoryCreateFolder, inventoryUpdateFolder, inventoryDeleteFolder, inventoryDuplicateFolder, inventoryListItems, inventoryGetItem, inventoryCreateItem, inventoryUpdateItem, inventoryDeleteItem, inventoryToggleEbaySync, inventoryUploadPhoto, inventoryDeletePhoto, inventoryReorderPhotos, inventoryImport, inventoryExport, inventoryExportPlatforms) are automated (see CI/CD below). Manual deploys of anything else use firebase deploy --only functions:<name> from polyvote/.

Key Conventions

Deployment & CI/CD

Seven GitHub Actions workflows live in .github/workflows/. The four auto-trigger workflows are each scoped with paths / paths-ignore filters so they only fire when their inputs change; the other three are manual-trigger-capable.

Workflow Trigger Scope Deploys
ci.yml PR β†’ main Per-app jobs gated by dorny/paths-filter β€” only changed apps run lint/test/build. Nothing (validation only).
jekyll-gh-pages.yml Push β†’ main Skips docs, Firebase configs, Cloud Functions, and Firestore/RTDB/Storage rules. Full site to GitHub Pages (Jekyll + PolyVote + Blog Admin + Inventory Manager).
feature-preview.yml Push β†’ test + manual workflow_dispatch (with optional ref input, defaults to test) Same paths-ignore as jekyll-gh-pages.yml. Combined GitHub Pages artifact: main rebuilt at root (Jekyll + PolyVote + Blog Admin + Inventory Manager, identical to jekyll-gh-pages.yml’s output) plus the test branch (or dispatch ref) rebuilt Jekyll-only under /test/ via bundle exec jekyll build --baseurl /test. Preview URL: https://www.ranzlappen.com/test/ (custom domain serves the artifact root, no /<repo>/ prefix). Shares the pages concurrency group with jekyll-gh-pages.yml so the two queue, never overlap.
firebase-deploy.yml Push β†’ main (Firebase/Functions paths) + manual Builds Cloud Functions, then deploys. Firestore rules + indexes, RTDB rules, Storage rules, castBlogVote, all Blog Admin callables, admin user-management callables (setUserRole, adminListUsers, adminBanUser, adminUnbanUser), and all Inventory Manager callables (inventory*).
firebase-deploy-manual.yml Manual only (workflow_dispatch) Accepts a target input passed straight to firebase deploy --only. Default functions redeploys every function in polyvote/functions/src/index.ts β€” future-proof for newly added functions. Shares the firebase-deploy concurrency group with the auto-deploy. Whatever the target input specifies (default: all Cloud Functions).
search-crawl.yml Manual only (workflow_dispatch) Runs the search-crawler module (Node 22, no install) with the auto-provided GITHUB_TOKEN, then opens a PR with the refreshed search-external.json via the pre-installed gh CLI (pushes a side branch + gh pr create; main is protected, so it never pushes there directly β€” and no third-party action to download). Merging the PR triggers jekyll-gh-pages.yml and redeploys with the new index. contents: write + pull-requests: write; search-crawl concurrency group. One-time setup: enable Settings β†’ Actions β†’ β€œAllow GitHub Actions to create and approve pull requests”. The bot PR is GITHUB_TOKEN-authored, so CI does not auto-run on it (a data-only change β€” just merge it). Opens a PR bumping search-external.json (no deploy itself).

Preview limitations: feature-preview.yml ships Jekyll-only under /test/. PolyVote, Blog Admin, and Inventory Manager are not rebuilt at the preview subpath because their Vite base and React Router basename are hardcoded to /polyvote/, /blog-admin/, and /inventory/. Navbar links to those apps will 404 inside the preview tree. To enable SPA previews later, make base env-driven in polyvote/vite.config.ts, blog-admin/vite.config.ts, inventory-manager/vite.config.ts, the three main.tsx files, PolyVote’s ShareButton.tsx, and PolyVote’s PWA manifest (defaults preserve current paths exactly).

What fires on a given change:

Change CI (on PR to main) Pages prod (push β†’ main) Pages preview (push β†’ test) Firebase (push β†’ main)
Blog post / Jekyll page β€” βœ“ βœ“ β€”
polyvote/src/** polyvote βœ“ βœ“ (rebuilt from main only) β€”
blog-admin/src/** blog-admin βœ“ βœ“ (rebuilt from main only) β€”
inventory-manager/src/** inventory-manager βœ“ βœ“ (rebuilt from main only) β€”
polyvote/functions/** functions β€” β€” βœ“
firestore.rules / firestore.indexes.json β€” β€” β€” βœ“
database.rules.json / storage.rules β€” β€” β€” βœ“
search-crawler/** (crawler code) search-crawler β€” β€” β€”
search-external.json (crawl output) β€” βœ“ βœ“ β€”
CLAUDE.md / README.md / LICENSE β€” β€” β€” β€”

Concurrency: CI cancels superseded runs per PR branch. Pages deploys (both jekyll-gh-pages.yml and feature-preview.yml) and Firebase deploys queue (no cancel) to avoid half-applied state.

Node version: All JS jobs (CI + Pages build) run on Node 22.

Action pinning: Third-party Actions (anything outside the GitHub-maintained actions/* org β€” currently dorny/paths-filter in ci.yml and dependabot/fetch-metadata in dependabot-auto-merge.yml) are pinned to a full commit SHA with a trailing # vX.Y.Z comment, not a mutable tag. Dependabot’s github-actions ecosystem bumps the SHA and the comment, so pins stay current. (SHA-pinning is a supply-chain safeguard against a moved/compromised tag; it does not prevent transient codeload.github.com download outages β€” those are cleared by re-running the job. actions/* and the ruby/setup-ruby@v1 moving branch stay on tags.)

Required secrets:

Dependabot (.github/dependabot.yml): weekly updates for all four npm packages (polyvote, polyvote/functions, blog-admin, inventory-manager), bundler, and GitHub Actions. Minor+patch are grouped. Each PR runs CI β€” ci.yml triggers on every PR to main (no paths: filter) so the ci-required aggregator always appears as a status check.

Auto-merge (.github/workflows/dependabot-auto-merge.yml): every Dependabot PR is queued for GitHub native auto-merge (gh pr merge --auto --merge) and lands once required status checks pass. Requires branch protection on main to mark ci-required as a required status check β€” without that, --auto merges immediately without waiting and a failing CI won’t block the merge (this happened with PR #236, a firebase-admin v12β†’v13 major bump whose functions job failed but landed anyway because no required check was configured). ci-required is a single aggregator job in ci.yml that succeeds only when every conditional app job (polyvote, functions, blog-admin, inventory-manager) either passed or was skipped via path filter; mark it required and the conditional-job deadlock problem disappears.

Manual fallbacks:

Tech Stack

Layer Blog PolyVote Blog Admin Inventory Manager
Framework Jekyll (Ruby) React 19 + TypeScript React 19 + TypeScript React 19 + TypeScript
Styling Custom CSS β€” main style.css (~3,200 lines), per-page stylesheets (spectrum.css, electronics-fundamentals.css, cmd-cheat-sheet.css), the shared abbreviations.css, and the shared reference-table.css (sticky-tab + live-search big-table scaffolding used by Spectrum and the CLI cheat sheet) Tailwind CSS v3 + Framer Motion Tailwind CSS v4 (via @tailwindcss/vite) Tailwind CSS v4 (via @tailwindcss/vite)
Router β€” react-router-dom v6 react-router-dom v7 react-router-dom v7
State Vanilla JS Zustand Zustand Zustand
Backend GitHub Pages (static) Firebase (Firestore, Auth, Functions) Firebase (Firestore, Auth) Firebase (Firestore, Auth, Storage)
Comments Giscus (GitHub Discussions) Firebase subcollections β€” β€”
Editor β€” β€” CodeMirror 6 β€”
Deployment jekyll-gh-pages.yml β†’ GitHub Pages Built by jekyll-gh-pages.yml into _site/polyvote/ Built by jekyll-gh-pages.yml into _site/blog-admin/ Built by jekyll-gh-pages.yml into _site/inventory/

Project Structure

β”œβ”€β”€ _config.yml                 # Jekyll configuration
β”œβ”€β”€ _data/
β”‚   β”œβ”€β”€ pages.yml               # Navigation registry (nav + footer)
β”‚   β”œβ”€β”€ projects.yml            # External app + reference-page favicons (footer strip)
β”‚   β”œβ”€β”€ abbreviations/          # Per-page glossary datasets (shared utility)
β”‚   β”‚   β”œβ”€β”€ cmd-cheat-sheet.yml
β”‚   β”‚   β”œβ”€β”€ electronics.yml
β”‚   β”‚   └── spectrum.yml
β”‚   β”œβ”€β”€ cmd-cheat-sheet/        # CLI cheat sheet command data + maintenance README
β”‚   β”œβ”€β”€ spectrum/               # EM spectrum band data + maintenance README
β”‚   └── references/electronics/ # Architecture / maintenance README for the EF page
β”œβ”€β”€ _includes/                  # Jekyll partials (head, header, footer…)
β”‚   └── abbreviations-section.html  # Shared glossary partial β€” see Key Conventions
β”œβ”€β”€ _layouts/                   # Page templates (default, home, post, page)
β”œβ”€β”€ _posts/                     # Blog content (Markdown)
β”œβ”€β”€ assets/
β”‚   β”œβ”€β”€ css/
β”‚   β”‚   β”œβ”€β”€ style.css                    # Main blog stylesheet
β”‚   β”‚   β”œβ”€β”€ abbreviations.css            # Shared glossary styling
β”‚   β”‚   β”œβ”€β”€ reference-table.css          # Shared big-table scaffolding (Spectrum + cmd cheat sheet)
β”‚   β”‚   β”œβ”€β”€ references-index.css         # /references/ landing-page cards
β”‚   β”‚   β”œβ”€β”€ spectrum.css                 # Spectrum reference page (overrides)
β”‚   β”‚   β”œβ”€β”€ electronics-fundamentals.css # Electronics reference page
β”‚   β”‚   β”œβ”€β”€ cmd-cheat-sheet.css          # CLI cheat sheet reference page (overrides)
β”‚   β”‚   β”œβ”€β”€ cmd-widgets.css              # CLI cheat sheet Interactive Tools widgets
β”‚   β”‚   └── cookie-consent.css
β”‚   β”œβ”€β”€ js/
β”‚   β”‚   β”œβ”€β”€ abbreviations.js             # Shared glossary modal + decoration
β”‚   β”‚   β”œβ”€β”€ reference-table.js           # Shared tab/search/empty/blurb wiring for big tables
β”‚   β”‚   β”œβ”€β”€ spectrum.js                  # Spectrum: thin wrapper over reference-table.js
β”‚   β”‚   β”œβ”€β”€ cmd-cheat-sheet.js           # CLI cheat sheet: reference-table wrapper + worked-example modal wiring
β”‚   β”‚   β”œβ”€β”€ cmd-widget-*.js              # CLI cheat sheet Interactive Tools (chmod/find/regex/curl + core + bundle)
β”‚   β”‚   β”œβ”€β”€ resize-handle.js             # Shared custom textarea resize grabber (reference pages)
β”‚   β”‚   └── electronics-*.js             # 9-file EF widget bundle
β”‚   └── images/favicons/                 # Local copies of external app + reference page favicons
β”œβ”€β”€ pages/                      # Static pages (about, contact, privacy, references/*…)
β”œβ”€β”€ feed.xml                    # Atom feed (custom, status-filtered)
β”œβ”€β”€ sitemap.xml                 # Sitemap (custom, status-filtered)
β”œβ”€β”€ search.json                 # Local search index (posts + pages + references), Liquid-generated, group-tagged
β”œβ”€β”€ search-external.json        # External search index (subdomains/gh-pages/repos/gists), crawler-generated snapshot
β”œβ”€β”€ .github/
β”‚   β”œβ”€β”€ dependabot.yml          # Weekly dependency updates
β”‚   └── workflows/
β”‚       β”œβ”€β”€ ci.yml                      # PR validation (per-app jobs)
β”‚       β”œβ”€β”€ dependabot-auto-merge.yml   # Queue Dependabot PRs for GitHub native auto-merge
β”‚       β”œβ”€β”€ feature-preview.yml         # Build + deploy main + `test` preview to Pages
β”‚       β”œβ”€β”€ firebase-deploy.yml         # Deploy Firestore/RTDB rules + castBlogVote + Blog Admin callables
β”‚       β”œβ”€β”€ firebase-deploy-manual.yml  # Manual Cloud Functions deploys (workflow_dispatch)
β”‚       β”œβ”€β”€ jekyll-gh-pages.yml         # Build + deploy prod site to Pages
β”‚       └── search-crawl.yml            # Manual re-crawl of search-external.json (workflow_dispatch)
β”œβ”€β”€ search-crawler/             # Dependency-free Node crawler β†’ search-external.json (see README)
β”‚   β”œβ”€β”€ crawl.mjs               # Orchestrator entry point
β”‚   β”œβ”€β”€ sources.config.js       # Declarative seeds (web hosts + groups, github user, caps)
β”‚   └── src/                    # util / extract / web / github fetchers
β”œβ”€β”€ blog-admin/
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ components/         # Editor UI, auth, dialogs
β”‚   β”‚   β”œβ”€β”€ pages/              # Dashboard, Editor, Login
β”‚   β”‚   β”œβ”€β”€ firebase.ts         # Firebase client config
β”‚   β”‚   β”œβ”€β”€ store.ts            # Zustand store
β”‚   β”‚   └── types.ts            # TypeScript interfaces
β”‚   └── eslint.config.js        # ESLint flat config
β”œβ”€β”€ inventory-manager/
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ components/         # AdminGuard, Toast, Header, FieldInput, PhotoGrid, ImportDialog, ConfirmDialog
β”‚   β”‚   β”œβ”€β”€ pages/              # Dashboard, FolderTable, SchemaEditor, ItemEditor, EbayExport, Login
β”‚   β”‚   β”œβ”€β”€ firebase.ts         # Firebase client config + httpsCallable wrappers
β”‚   β”‚   β”œβ”€β”€ store.ts            # Zustand store (auth, folders, items, selection, toasts)
β”‚   β”‚   β”œβ”€β”€ types.ts            # TypeScript interfaces (mirrors functions/src/inventory/shared.ts)
β”‚   β”‚   β”œβ”€β”€ ebay.ts             # eBay condition IDs, durations, formats
β”‚   β”‚   └── index.css           # Tailwind import + theme variables
β”‚   β”œβ”€β”€ eslint.config.js        # ESLint flat config
β”‚   └── vite.config.ts          # base: '/inventory/'
└── polyvote/
    β”œβ”€β”€ src/
    β”‚   β”œβ”€β”€ components/         # React components
    β”‚   β”œβ”€β”€ pages/              # Route-level pages + admin/
    β”‚   β”œβ”€β”€ hooks/              # Zustand store, Firestore hooks
    β”‚   β”œβ”€β”€ types/              # TypeScript interfaces
    β”‚   └── __tests__/          # Vitest tests
    β”œβ”€β”€ functions/src/          # Firebase Cloud Functions (TypeScript)
    β”‚   └── inventory/          # Inventory Manager callables (folders, items, photos, import/export, eBay CSV)
    β”œβ”€β”€ firestore.rules         # Firestore security rules
    β”œβ”€β”€ firestore.indexes.json  # Firestore composite indexes
    β”œβ”€β”€ database.rules.json     # Realtime Database rules (vote aggregates)
    β”œβ”€β”€ storage.rules           # Firebase Storage rules (inventory photos, public-read)
    β”œβ”€β”€ firebase.json           # Firebase project config
    β”œβ”€β”€ .firebaserc             # Firebase project ID
    └── eslint.config.js        # ESLint flat config

Post-task self-check

After every turn that produces a branch, PR, feature, or bug fix, do a quick self-check before replying: does the change introduce anything worth codifying in docs or automation? Scan for new env vars, npm scripts, path filters, deploy targets, secrets, setup steps, dependencies, or conventions that should be reflected in README.md, CLAUDE.md, .github/workflows/*.yml, or .github/dependabot.yml.

Decide per case:

If nothing is warranted, say β€œno doc/workflow updates needed” in one line. Skip this self-check entirely for pure Q&A turns that don’t change code.