JJK.engineer — Project Specification

For Claude Code: Read this entire file at the start of every session. Before making any architectural decision — adding a dependency, creating a new directory, choosing a pattern — verify it aligns with this spec. If a question isn’t answered here, ask the developer. Do not invent architecture to fill gaps. Do not add features, abstractions, or “nice-to-haves” not described in this document. When in doubt, choose the simpler option.

Document Purpose: This is the target specification for jjk.engineer. It captures decisions, not implementation. Any development work (including by Claude Code) should reference this document to ensure alignment. If a proposed change contradicts this spec, stop and discuss before proceeding.

Companion docs: docs-site/architecture/decisions/ holds the Architecture Decision Records. This spec describes what the architecture is; the ADRs record why each significant decision was made and when. Read both.

Last Updated: April 8, 2026


1. What Is This Project?

jjk.engineer is a personal engineering blog and portfolio site authored by Justin (a licensed Professional Engineer and engineering superintendent in capital projects at a chemical facility). The site’s editorial voice is satirical — it critiques corporate dysfunction, performance theater, and consultant culture through deadpan humor. The “Ernest Sludge” persona stands as a style guardian.

The site must serve two audiences simultaneously:

  • Casual readers who find the satire entertaining
  • Working engineers who recognize the dysfunction and find solidarity

The blog content is the primary product. Everything else — tools, portfolio, interactive features — is secondary.


2. Project Identity

Voice & Tone

  • Satirical, deadpan, irreverent
  • Looks like a serious corporate publication at first glance
  • The absurdity reveals itself the longer you look
  • Never mean-spirited — punches up at systems, not at people

Visual Identity

  • Not a generic tech blog. The design IS the joke.
  • Professional layout and clean typography that subtly subverts itself
  • Background layers with faded SVG absurdities: fake flowcharts, nonsense P&IDs, Gantt charts extending to 2047, engineering stamps reading “APPROVED FOR DYSFUNCTION”, anything you might find in corporate documentation/emails…
  • Micro-interactions that reinforce the satire (loading states say “Awaiting Approval…”, 404 is a Management of Change form, etc.)
  • Icons: Font Awesome Free via kit (<script src="https://kit.fontawesome.com/9f0422756e.js">). Use <i class="fa-solid fa-icon-name" aria-hidden="true"> elements. Available families: fa-solid, fa-regular, fa-brands. No inline SVGs for icons, no other icon libraries.
  • Custom CSS design system — no CSS framework (Tailwind, Bootstrap, etc.)
  • PrimeNG used only where interactive widget behavior is needed (tools pages), never for blog content rendering. Why: see Key Architectural Decisions.
  • All UI components wrapped in jjk-* selectors regardless of whether PrimeNG is used underneath. Why: decouples templates from any third-party library. If PrimeNG is ever swapped out, only the wrapper internals change — no template updates across the app.

Design Musts (Day One)

  • Dark mode / light mode based on system preference
  • Fully mobile responsive — not an afterthought
  • CSS custom properties as the design token system
  • Optimal reading width for blog content (~42rem)

3. Architecture

Workspace Structure

  • Angular monorepo (single workspace, single application — not a multi-lib setup)
  • Frontend and backend in the same repo with shared code
  • npm workspaces for shared node_modules across front/back. Why: single dependency tree prevents version mismatches between what the build pipeline uses and what the frontend bundles.
  • Path aliases (@jjk/shared, @jjk/models, @jjk/utils, @jjk/constants) configured from day one in tsconfig.base.json. Why: retrofitting path aliases is painful. They also make imports readable and decouple consumers from physical directory structure.
  • No publishable libraries — shared code lives in a shared/ directory, not an Angular library project. Why: the only consumer is this project. Library packaging overhead has zero audience.
  • If a piece of code earns extraction into a lib later, it can be extracted then. Not before.

High-Level Directory Layout

jjk-workspace/
├── package.json              # root workspace config
├── tsconfig.base.json        # path aliases live here
├── angular.json
├── frontend/                 # Angular application
├── functions/                # Firebase Cloud Functions
├── tools/build/              # content pipeline (Markdown → JSON)
├── shared/                   # models, constants, utils, services (no platform deps)
├── content/                  # markdown source files (the blog)
└── tools/                    # dev scripts, scaffolding, config

Key Architectural Decisions

  • No over-engineered content engine. Start simple. Build custom tooling only when a real authoring limitation is hit — not before.
  • Build-time content processing. Markdown → HTML happens at build time, not runtime. The Angular app receives pre-compiled JSON and renders it. Zero runtime markdown parsing. Why: keeps the frontend fast and dumb — it just fetches JSON. All complexity lives in the build step where it’s testable and debuggable outside the browser.
  • ViewEncapsulation.None for rendered content. Any component that renders pre-compiled HTML via [innerHTML] must use ViewEncapsulation.None. Angular’s default emulated encapsulation adds _ngcontent attributes to scoped CSS selectors — but dynamically injected HTML doesn’t carry those attributes, so styles silently fail. Scope these styles with the .jjk-prose class prefix to prevent leakage into the rest of the app.
  • Blog module uses zero PrimeNG. Why: bundle size AND aesthetic independence. Blog readers should never download a UI component library. And blog styling must be fully custom to achieve the satirical visual identity — PrimeNG’s opinions would fight that. PrimeNG is lazy-loaded only in tool pages that genuinely need interactive data components (tables, dialogs, form widgets).
  • Backend-driven security model. Public-facing writes go through callable Cloud Functions (onCall with enforceAppCheck: true), not direct Firestore writes from the client. Functions enforce input validation, sanitization, rate limiting, and ID generation server-side. Firestore rules act as a secondary gate — allow create: if false on collections that have a corresponding function. Client-side validation is UX only; it is never the sole enforcement. Admin mutations on sensitive collections should have onDocumentUpdated triggers writing audit trails to subcollections. Why: the client is a display layer. Anything that needs to be trusted runs on the server. This was established during the enterprise hardening pass and applies to all new features going forward.
  • Performance monitoring via Firebase Performance, not console. Never use console.time, console.timeEnd, or console.log for performance measurement. Use PerformanceService (@app/shared/services/performance.service) which wraps Firebase Performance custom traces. Traces appear in Firebase Console under Performance > Custom traces with p50–p99 data. The service returns no-op traces during SSR and emulator mode, so callers never need null checks. Why: console timing is invisible in production, can’t be dashboarded, and pollutes the console. Firebase Performance gives real user percentile data with zero console noise.

Technology Stack

Technology Target Notes
Angular 21 Use standalone components, new control flow (@if, @for), signals. Do NOT install 19 or 20.
Test runner Vitest Not Jasmine, not Jest, not Karma. Vitest only.
Node.js LTS (20+) Build pipeline and Firebase Functions
TypeScript Strict mode strict: true in all tsconfig files
PrimeNG Latest stable Lazy-loaded, tools pages only
Firebase Current SDK Functions, Auth, Firestore, Hosting, App Check, Remote Config, Secret Manager, Performance
CSS Custom properties No preprocessor required — vanilla CSS with custom properties. SCSS only if a clear need emerges
Markdown markdown-it or remark Decide during implementation — either is fine, pick one and stay consistent
Syntax highlighting Shiki Build-time highlighting, not runtime
Diagrams Mermaid Runtime rendering via mermaid library (lazy-loaded). Re-renders on theme change. See ADR 0004 amendment.
Package manager npm npm workspaces for monorepo. No yarn, no pnpm unless a blocking issue forces it

Naming Conventions

Thing Convention Example
Files (components, services, etc.) kebab-case post-card.component.ts, tag-index.service.ts
Directories kebab-case blog-engine/, shared/models/
Component selectors jjk- prefix, kebab-case jjk-post-card, jjk-shell, jjk-nav
Services PascalCase, no prefix ContentService, TagIndexService
Interfaces/Models PascalCase, no I prefix PostMeta, TagIndex, SeriesConfig
CSS custom properties --jjk- prefix --jjk-color-ink, --jjk-font-body
Content files (posts) NN-slug-title.md 01-agile-is-a-trauma-response.md
Path aliases @jjk/ prefix @jjk/models, @jjk/utils
Git branches kebab-case with type prefix feat/blog-engine, fix/dark-mode-toggle
Git commits conventional commits feat: add tag index page, fix: dark mode flicker
Firestore collections snake_case inbox_messages, timeclock_entries, rate_limits

4. Content System

File-Based Routing

  • The file path determines the URL. No manual route registration to publish a post.
  • Numeric prefixes in filenames are for ordering only and are stripped from URLs.
  • Files prefixed with underscore (_) are never converted to pages. They serve as fragments, partials, series metadata, or reference material.
  • Draft and future-dated post visibility is environment-dependent (see Build Pipeline section for full rules).

Content Directory Structure

content/
├── posts/
│   ├── 2025/
│   │   ├── 01-post-title.md
│   │   └── 02-another-post.md
│   ├── series-name/              # series = folder inside posts
│   │   ├── _series.yaml          # series metadata, not a page
│   │   ├── 01-first-entry.md
│   │   └── 02-second-entry.md
│   └── _disclaimer.md            # fragment, not a page
├── pages/                        # standalone pages (about, contact, etc.)
└── fragments/                    # reusable content blocks

Series Handling

  • A series is just a folder inside posts/ that contains a _series.yaml file.
  • Posts inside a series folder automatically receive a series:<slug> tag. Authors do not need to declare series membership in frontmatter.
  • Series are not architecturally different from standalone posts. They’re posts with an auto-applied tag and ordering from filename prefixes.
  • Tag pages for series tags sort by series order (numeric prefix). Regular tags sort by date.

Frontmatter Schema

Every markdown content file begins with YAML frontmatter. The build pipeline validates against this schema at build time. Missing required fields = build failure.

Field Type Required Default Description
title string yes Post title, used in <h1>, <title>, and meta tags
date date (YYYY-MM-DD) yes Publish date. Controls scheduled publishing in prod builds
tags string[] yes Array of tag slugs. Used for categorization and tag pages
subtitle string no Displayed below title, used in meta description if no excerpt
updated date (YYYY-MM-DD) no If present, shows “Updated” badge on the post
draft boolean no false If true, excluded from production builds entirely
cover string (path) no Path to cover image (e.g. /assets/img/posts/foo.png). Renders as post hero + card thumbnail and drives og:image. File must exist; source must be < 2MB (< 500KB recommended; optimized target < 100KB).
coverAlt string when cover set Accessibility + SEO alt text for the cover image. Build fails if cover is set without this.
excerpt string no auto-generated Short summary for post cards and meta description
toc boolean no true Whether to render table of contents
layout "post" | "page" | "tool" no "post" Determines which layout template wraps the content
readingTime number | "auto" no "auto" Minutes. Auto-calculated from word count if not specified
corporateBsLevel number (0-10) no Satirical metadata. May drive visual indicators on post cards
topArticle number (1-5) no Position in “Top 5 Articles” ranking. Omit if not a top post
member boolean no false Marks post as paid-member-only content. Gating not yet implemented

Custom Markdown Directives (Start Simple)

  • :::callout{type="..."} — styled content blocks (warnings, asides, corporate-bs callouts)
  • :::include{src="..."} — inline content from underscore-prefixed fragment files
  • More directive types (interactive component hydration, expressions) can be added later IF a real need emerges. Do not build these speculatively.

5. Infrastructure & Hosting

Vendor Consolidation: Firebase

  • Hosting: Firebase Hosting
  • Backend: Firebase Functions
  • Auth: Firebase Auth (custom claims for admin roles)
  • Database: Firestore
  • App Check: reCAPTCHA Enterprise for abuse protection
  • Remote Config: Feature flags and dynamic values (e.g., admin_idle_timeout_ms)
  • Secret Manager: API keys and sensitive config (accessed via defineSecret() in functions)
  • Goal: Single firebase deploy hits everything. Single vendor, single billing, single CLI.

What Migrates from Azure

  • Inventory Azure resources and migrate anything stateful
  • Most Azure endpoints are expected to port to Firebase Functions mechanically
  • This is a migration task, not an architecture task

Deployment

  • npm run build produces a static frontend + compiled functions
  • Firebase preview channels for draft/staging content
  • No SSR required — static HTML with Angular hydration is sufficient for a blog
  • Cloud Build scheduled trigger handles scheduled publishing (see Build Pipeline > Post Visibility Rules)

6. Build Pipeline

Core Responsibilities

  1. Discover markdown files in content/ (respecting underscore exclusion)
  2. Parse frontmatter and body
  3. Validate frontmatter against schema
  4. Apply environment-aware filtering (see Post Visibility Rules below)
  5. Enrich metadata (auto-tag series, calculate reading time)
  6. Transform markdown to HTML (syntax highlighting, custom directives, heading anchors)
  7. Generate outputs: route manifest, tag index, series manifest, RSS, sitemap
  8. Output pre-compiled JSON files for the frontend to consume as static assets

Post Visibility Rules

The build pipeline treats posts differently based on environment:

Development (npm run dev):

  • All posts are built and visible — drafts, future-dated, everything
  • Draft posts (draft: true) render with a visible “DRAFT” banner
  • Future-dated posts (date is after today, draft: false) render with a visible “SCHEDULED” banner
  • This allows the author to preview and proof everything locally

Production (npm run build):

  • Posts with draft: true are excluded entirely — not built, not in route manifest, not in RSS
  • Posts with a date in the future and draft: false are excluded entirely — they don’t exist in the build output until their date arrives
  • Only posts with draft: false (or no draft field) AND a date on or before the build date are included

Scheduled Publishing:

  • A Cloud Build scheduled trigger (via Cloud Scheduler) runs Monday and Thursday at 07:00 UTC
  • The trigger invokes the deploy-blog Cloud Build pipeline
  • Posts whose date has now arrived are automatically included in the new build — no manual intervention required
  • This enables batching: write multiple posts, set future dates, and let the schedule handle publishing

Developer Experience

  • npm run dev watches content files and rebuilds on change, with Angular dev server running concurrently
  • npm run new:post scaffolds a new post with frontmatter template
  • Build warnings for common mistakes (typos in tag names, missing images, etc.)
  • Draft and scheduled banners in dev mode are visually distinct and impossible to miss

7. Migration Priority

In order of importance. Each item includes a “done when” checkpoint — if you can do the described action, that phase is complete.

  1. Blog engine — markdown pipeline, post rendering, tag pages, navigation, RSS
    • Done when: A new .md file in content/posts/ with valid frontmatter produces a routable, styled page after npm run build. Tags link to tag pages. RSS feed includes the post. Draft/scheduled filtering works in both dev and prod modes.
  2. Visual identity — the design system, background SVGs, satirical chrome, dark/light mode
    • Done when: The site is visually indistinguishable from a “real” corporate engineering publication at first glance, with satirical details revealing themselves on closer inspection. Dark/light mode works via system preference. All pages are fully responsive.
  3. Core pages — about, portfolio/resume, contact, investors
    • Done when: /about, /portfolio, and /contact exist, render from markdown or templates, and are linked from the site navigation.
    • Portfolio/resume deprecated — replaced by /investors (satirical fundraise page with real Stripe payment links). The about page covers professional background sufficiently.
    • Done when: /about, /contact, and /investors exist, are styled on-brand, and are linked from the site navigation. Contact form writes to Firestore. Investor tiers link to real Stripe payment URLs.
  4. Admin & authentication — login, contact inbox, ratings dashboard, security hardening
    • /login — Firebase Auth email/password sign-in (single admin user). Observable-based authGuard waits for auth state to resolve before granting or redirecting. Guard verifies admin custom claim via getIdTokenResult() — not just authentication.
    • /admin redirects to /admin/inbox. Shared AdminNavComponent provides tab navigation (Inbox, Ratings) and sign-out.
    • /admin/inbox — Real-time Firestore inbox of contact form submissions. Supports read/unread toggle, soft delete, and mailto reply. Protected by authGuard.
    • /admin/ratings — Real-time ratings dashboard showing per-post aggregates, distribution bars, confidence levels, and sort options. Protected by authGuard.
    • Post rating system — Snarky 0–5 star widget on every post. Anonymous auth (lazy, on first interaction). Votes stored in ratings/{postSlug}/votes/{uid}. Cloud Function (onRatingWritten) maintains aggregate docs via transaction. Display threshold (RATING_DISPLAY_THRESHOLD) controls when stars appear in post metadata.
    • Contact form submission — Goes through submitInboxMessage callable Cloud Function (not direct Firestore write). Function enforces App Check, input validation, sanitization, rate limiting (5/email/24h), and server-side ticket ID generation. onInboxMessageCreated trigger sends email notification via Resend. onInboxMessageUpdated trigger writes audit trail to inbox_messages/{id}/audit/ subcollection.
    • Admin session security — Idle timeout for admin users (configurable via Remote Config admin_idle_timeout_ms, default 30min). Auto sign-out on inactivity. Activity tracking runs outside Angular zone.
    • Error reportingSentry captures uncaught errors and manual captureException calls in production. Lazy-loaded via core/sentry.ts to stay out of the initial bundle — main.ts calls initSentry(dsn) when environment.sentryDsn is set, which dynamically imports @sentry/angular. Components use captureException() from @app/core/sentry.
    • Auth service exposes user$ observable (shared via shareReplay(1)) and user/isAuthenticated signals for template use.
    • Not linked from site navigation — admin routes are accessed directly.
    • Done when: An authenticated admin can manage contacts at /admin/inbox and view rating analytics at /admin/ratings. Readers can rate posts anonymously. Unauthenticated or non-admin users are redirected to /login. Contact submissions are server-validated. Admin sessions auto-expire on idle.
  5. Firebase consolidation — move hosting from Azure, unify deployment
    • Done when: firebase deploy deploys the full site (hosting + functions + firestore rules). Azure resources are decommissioned. DNS points to Firebase Hosting.
  6. Migrate select tools — only tools that get traffic or are still actively used
    • Done when: Selected tools are accessible as lazy-loaded routes under /tools/*. They use PrimeNG where needed, wrapped in jjk-* components. Blog bundle size is unaffected.
  7. Everything else — sunset or archive. If nobody misses it, it’s gone.
    • Done when: Old repo is archived. No dangling Azure resources. Redirects in place for any URLs that had external links.

8. What This Project Is NOT

  • Not a static site generator framework. We’re building a blog, not a product for other people to use.
  • Not a multi-library Angular workspace. No @jjk/core, @jjk/markdown, @jjk/theme packages. Shared code lives in a directory.
  • Not a Docusaurus clone. We’re borrowing the authoring experience (file-based, markdown-first) but not reimplementing the framework.
  • Not a playground for speculative features. Every custom build tool must solve a problem the author has actually hit. If an off-the-shelf solution works, use it.
  • Not a tool-first site. The blog is the product. Tools are optional extras.
  • Not an excuse for sloppy code. “It’s just a blog” is not a reason to skip validation, hardening, or proper architecture. The feature set is blog-scale. The code quality is not.

9. Guiding Principles

  1. Enterprise-grade code, blog-scale product. The product is a satirical blog. The engineering is not casual. Code quality, security posture, and operational discipline meet production SaaS standards: strict types, server-side enforcement, proper error handling, audit trails, clean frontend/backend boundaries. The satire is in the content, never in the codebase.
  2. Content over infrastructure. If you’re spending more time on the engine than writing posts, something is wrong.
  3. Earn features, not quality. Start with the simplest feature set that works — but implement it to the highest standard. “Earn complexity” means don’t build speculative features. It does not mean skip the hardening pass, cut corners on validation, or leave security for later.
  4. The visual design is the differentiator. Invest time in the satirical aesthetic, background details, and micro-interactions. That’s what makes this site memorable.
  5. Dark mode and mobile are not features — they’re fundamentals. Never build a component without both.
  6. One vendor, one deploy. Firebase for everything. No multi-cloud complexity for a blog.
  7. Wrap everything in jjk-*. Every UI component gets a project-specific selector. No raw PrimeNG selectors in templates. No framework classes in markup.
  8. The blog that satirizes over-engineering must not itself be over-engineered. Over-engineering means speculative features, premature abstractions, and infrastructure nobody asked for. It does not mean strict types, input validation, server-side enforcement, or audit trails. Those are just engineering. Know the difference.

Member Content (Planned)

member: true frontmatter field added to schema. Marks posts as paid-member-only content. Gating implementation pending — auth strategy (Firebase custom claims + Stripe webhook) to be designed separately. Do not implement until instructed.


Note

This document is the source of truth for architectural and product decisions. Implementation details, code patterns, and technical how-tos belong elsewhere.

For Claude Code — decision protocol when this spec is silent on a topic:

  1. Can it be solved with what’s already in the spec? → Do that.
  2. Is there a simple, standard Angular convention? → Follow that.
  3. Is it a cosmetic/minor choice? → Pick the simpler option and move on.
  4. Is it an architectural choice that could be hard to undo? → Stop and ask the developer.