Skip to content

faisalkhan91/Fluxus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,005 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fluxus

Angular License Docker Deploy

A personal portfolio site built with Angular 22, styled as a code-editor workspace. The entire application is statically generated (SSG) and served from a hardened NGINX container on Kubernetes.

Live: faisalkhan.dpdns.org


Tech Stack

Layer Technology
Framework Angular 22 (standalone components, signals, zoneless, OnPush)
Rendering SSG via @angular/ssr — the entire route tree is prerendered at build time
Styling Scoped component CSS + global design tokens (src/styles.css)
Blog Markdown files rendered via marked + highlight.js
Container Multi-stage Docker — node:24.17-alpine builder, nginxinc/nginx-unprivileged:1.30-alpine runtime
Web Server NGINX with gzip, granular cache policies, and full security headers
Language TypeScript 6.0 (strict mode, no any)

Features

  • Glass Workspace UI — Glassmorphism design system with backdrop-filter, custom design tokens, dark/light mode
  • Static Site Generation — All routes prerendered at build time for instant load and SEO
  • Blog Engine — Markdown posts with anchor IDs, copy-button code blocks, reading progress bar, prev/next + breadcrumb navigation, share row, "Edit on GitHub" deep link, and a /blog/tag/:tag archive
  • GitHub-sourced Project Showcase/projects is driven by a portfolio GitHub topic filter + a thin local override manifest. Each card + /projects/:slug detail page renders live stars/forks/language/last-push/license pills, a language-distribution bar, optional release tag, a README excerpt, a 52-week commit sparkline, and related blog posts. Adding a new project is a single click on github.com
  • Auto-generated assets — Per-post Open Graph card PNGs, Atom feed (/feed.xml), sitemap (/sitemap.xml), and image dimensions for CLS-safe markdown images
  • Structured data — Site-wide Person + WebSite JSON-LD; per-post BlogPosting + BreadcrumbList
  • Adaptive Favicon — SVG favicon with dark/light mode support, multi-size ICO, apple-touch-icon, PWA manifest icons
  • Full SEO — Per-route <title>, og:*, twitter:* meta tags, canonical URLs
  • Accessibility — Skip-to-content + skip-to-nav links, WCAG AA focus styles, ARIA attributes on all interactive elements, focus trap on the mobile menu
  • Cmd+K command palette — Site-wide search over routes + blog posts, no third-party deps
  • Incremental hydration — Below-fold blocks (hero "latest posts") deferred via @defer (hydrate on viewport) to slim TBT
  • Service Worker@angular/service-worker precaches the app shell and lazy-caches blog posts + images for offline reading
  • Web Vitals telemetryweb-vitals lib, lazy-loaded; logs CLS / INP / LCP / FCP / TTFB to console in dev and beacons to a self-hosted endpoint in prod when configured
  • Modern CSS — OKLCH accent palette, container queries on the content shell, scroll-driven reading-progress animation (zero-JS where supported)
  • Print stylesheet — Resume / blog post print-friendly view (chrome hidden, black-on-white, prose expanded)
  • Security Headers — CSP (script-src hashed at build time, no 'unsafe-inline' for scripts), HSTS preload, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
  • Comprehensive Test Suite — 900+ Vitest specs (with posts.json and nav-routes contract tests), Playwright a11y + behaviour suite, opt-in visual regression, Lighthouse CI in GitHub Actions
  • Automated CI/CD — single-check CI gate, CodeQL SAST, Release Please semver, multi-arch Docker publish, GitOps PR to Homelab, Trivy gate plus weekly drift re-scan, scheduled smoke probe, weekly GHCR retention

Quick Start

git clone https://github.com/faisalkhan91/Fluxus.git
cd Fluxus
npm install
ng serve --port 4300

Open http://localhost:4300/ in your browser.

Prerequisites

  • Node.js 24 (LTS) — package.json declares engines.node: ">=24.15.0 <25"
  • npm
  • Angular CLI 22+ (npm install -g @angular/cli@latest)
  • Docker (for containerized builds)

Build

Production (SSG + asset generation)

npm run build:prod

This runs the full build pipeline in order:

  1. node scripts/sync-reading-times.mjs — recomputes readingTime on every posts.json entry from the matching markdown body so manifest and prose never disagree.
  2. node scripts/build-image-dims.mjs — sweeps src/assets/images/ with sharp and writes intrinsic width/height for every image into src/app/core/services/image-dims.generated.ts. The MarkdownService renderer reads this map so blog <img> tags ship with width / height / loading="lazy" / decoding="async" (no CLS).
  3. node scripts/build-image-variants.mjs — generates responsive WebP width-variants for the raster images (project screenshots + blog covers/figures) and writes src/app/core/image/image-variants.generated.ts, which the runtime IMAGE_LOADER consumes so NgOptimizedImage emits a real width-descriptor srcset.
  4. node scripts/build-version-stamp.mjs — writes the build's commit SHA + timestamp into app-version.generated.ts so the sidebar footer can surface it.
  5. ng build --configuration production — Angular SSG build with the service worker (ngsw-config.json) included. Prerenders every known /projects/:slug, /projects/tag/:tag, /blog/:slug, and /blog/tag/:tag.
  6. node scripts/inject-meta.mjs — rewrites per-route <title>, description, canonical, and Open Graph / Twitter tags. Blog posts get JSON-LD BlogPosting + BreadcrumbList; project-tag archives get CollectionPage + BreadcrumbList; project detail pages get per-project <head> tags.
  7. node scripts/build-sitemap.mjs — regenerates dist/fluxus/browser/sitemap.xml from posts.json + the static route list + the generated project list (no manual sitemap maintenance).
  8. node scripts/build-feed.mjs — emits dist/fluxus/browser/feed.xml (Atom 1.0).
  9. node scripts/build-og-cards.mjs — renders a 1200×630 PNG OG card per blog post that lacks an explicit cover field.
  10. node scripts/build-csp.mjs — hashes every inline <script> in the prerendered HTML and writes dist/fluxus/security-headers.conf. The Docker image consumes that file so the production CSP can stay strict (no 'unsafe-inline' for scripts).
  11. node scripts/audit-csp.mjs — walks the prerendered tree and fails the build if any inline script / event handler lacks a matching CSP hash.
  12. node scripts/audit-prerender.mjs — structural audit of the prerendered tree; fails on SSR regressions (empty <h1>s, missing/mismatched canonical + OG/Twitter meta, <svg> without aria-hidden, missing pre-paint theme script, invalid tab buttons) and sweeps every page for dead internal links / broken image assets.
  13. node scripts/audit-structured-data.mjs — structured-data + social-metadata regression check against the real dist output: JSON-LD parses and carries @context/@type, every og:image page also has og:image:alt, and feed.xml is well-formed Atom (RFC 4287).

Build output goes to dist/fluxus/browser/ — one directory per prerendered route (index.html inside each), plus feed.xml, sitemap.xml, og/<slug>.png, and the SW manifests. A representative recent run emitted 117 prerendered routes: 8 top-level + 13 blog posts + 23 blog-tag archives + 64 project-tag archives + 9 project detail pages. The counts move with the repo's topic tags and blog manifest; the pipeline enumerates them automatically.

Project metadata is fetched out of band. projects.generated.{ts,json} + scripts/cache/projects-github.json are committed artefacts consumed directly by the build. The actual GitHub fetch (scripts/fetch-projects-github.mjs, via Octokit GraphQL — one request for every repo in scope) runs on a scheduled refresh-projects.yml workflow (daily cron + workflow_dispatch) and opens a PR with any updated files. That keeps every PR build deterministic, network-free, and immune to GitHub API flake. Locally, npm run fetch:projects runs the same script — set GITHUB_TOKEN in your env for a fresh fetch; unset falls back to the committed cache.

Docker

Build the image

docker build -t fluxus .

Run locally

docker run --rm -p 8080:8080 fluxus

Open http://localhost:8080/ in your browser.

The container runs as non-root (UID 101) on port 8080, ready for Kubernetes deployment with readOnlyRootFilesystem: true and all capabilities dropped.

The image includes a HEALTHCHECK that polls /healthz every 30 seconds, so Docker reports container health without relying on an external orchestrator.

Multi-architecture builds (linux/amd64, linux/arm64) are produced automatically on every tagged release.


Testing

Unit tests use Vitest with jsdom:

npm test              # watch mode
npm test -- --watch=false  # single run (CI)

Notable implementation details

A handful of project-specific contracts that aren't obvious from the file tree:

site.config.json

Single source of truth for site identity. Three fields drive five build scripts (inject-meta, build-sitemap, build-feed, build-og-cards, audit-prerender):

  • siteUrl — production origin, used for canonicals, OG URLs, sitemap entries, atom feed self-links.
  • siteName — page title suffix, OG site name, JSON-LD publisher.
  • twitterHandle — optional, emitted as twitter:site / twitter:creator when set.

A fork updates this file once and the script suite re-targets automatically.

Adding a theme

Three files have to change in lockstep, or a fresh build will fail the CSP audit:

  1. src/app/core/services/theme.registry.ts — append to the ThemeId union and the THEME_REGISTRY array.
  2. src/styles.css — append a [data-theme='<id>'] token block mirroring [data-theme='crimson-light'].
  3. src/index.html — append the same id to the inline pre-paint script's THEME_IDS allowlist.

After all three, run npm run build:prod so audit-csp.mjs regenerates the script-src hash for the updated pre-paint script. The contract is enforced by theme.registry.spec.ts — drift fails unit tests before it reaches CSP.

TypeScript: verbatimModuleSyntax

tsconfig.json has "verbatimModuleSyntax": true. Pure-interface imports must use import type { X } (or import { type X, y } for mixed). Violating produces error TS1484 at compile time. Apply this to every Project, BlogPost, Skill, etc. import — the compiler enforces it.

Mobile / iOS

  • Top-level chrome (sidebar, body, hero) uses 100dvh with a 100vh fallback so the iOS Safari viewport tracks dynamic chrome instead of leaving an ~80 px phantom scroll zone.
  • viewport-fit=cover is set so env(safe-area-inset-*) reports non-zero on iPhone with a home indicator. The mobile nav pill, toast region, and reading-progress bar all consume the relevant inset.
  • Mobile menu modal applies inert on the rest of the shell so VoiceOver / keyboard can't escape the dialog.

Quality gates

Six layered checks guard the build. The CI workflow runs lint, typecheck, audit, unit tests, build, Playwright, and Lighthouse on every PR; the CSP, prerender, and structured-data audits run inside build:prod, so they execute in CI on every PR as well as locally.

Gate Command What it catches
Unit tests npm test -- --watch=false 900+ Vitest specs (components, services, blog content + nav-routes contract tests)
Static analysis npm run lint  ·  npm run typecheck ESLint, Angular template rules, strict TypeScript
Prerender audit (in build:prod) runs automatically in npm run build:prod (also npm run audit:prerender standalone) SSR regressions: empty <h1>s, missing OG/canonical/twitter meta, broken tab buttons, FOUC script
Live a11y / behaviour npm run e2e (after npm run build:prod) axe (WCAG AA), focus trap, theme pre-paint, View Transitions, prefers-reduced-motion
Visual regression (opt-in) npm run e2e:visual (after npm run build:prod) Per-route × theme × viewport screenshot baselines under tests/e2e/visual.spec.ts-snapshots/
Lighthouse CI lighthouserc.json runs in GitHub Actions on every PR Performance / a11y / best-practices / SEO category thresholds + LCP / CLS / TBT budgets

The e2e pass uses Playwright + @axe-core/playwright. On a fresh checkout, install the chromium browser binary once:

npm run e2e:install

Then build the site and run the pass:

npm run build:prod
npm run e2e                # headless behaviour suite (default)
npm run e2e:ui             # interactive UI mode for debugging
npm run e2e:visual         # opt-in visual regression run
npm run e2e:visual:update  # refresh visual baselines after intended changes

Tests live in tests/e2e/ and run against the prerendered output served via http-server on :4300.

Bundle analyzer

Spot-check what's in the JS chunks (mostly used to catch regressions like a heavy lib leaking into the home route):

npm run analyze

That builds with source maps and opens source-map-explorer on the main + chunk bundles in your default browser.


CI/CD

Workflow Trigger What it does
CI PR + push to main Lint, typecheck, audit, unit tests + coverage, production build, Playwright, Lighthouse, plus a CI success fan-in check
CodeQL PR + push to main, weekly schedule SAST on app code (javascript-typescript) and workflow YAML (actions)
Release Please Push to main Opens / updates a release PR; only feat: / fix: / breaking changes bump the version
Publish image Tag v*, push to main touching src/assets/blog/**, or workflow_dispatch Multi-arch Docker build, single-pass Trivy gate, GitOps PR to Homelab (rolling per-lineage branches). Three shapes: tag pushes ship versioned releases; blog pushes ship content-<sha> images without a SemVer bump; scheduled publishes (dispatched by scheduled-publish.yml) ship scheduled-<yyyymmdd> images
Trivy re-scan Weekly schedule Scans the published latest image without .trivyignore to surface CVE drift
Smoke Manual + daily schedule Polls /healthz on the deployed site, then probes the live production CSP (Playwright/Chromium)
GHCR retention Weekly schedule + manual Prunes old content-<sha> and scheduled-<yyyymmdd> images (keeps the 10 most recent in each rolling lineage); untagged versions are deliberately left intact (they back the live multi-arch manifests / SBOM / provenance / attestation); release tags (vX.Y.Z, X.Y, latest) are kept forever

Release conventions

Commit type Release Please Publish image Result
feat: ... / fix: ... Opens release PR Runs on the vX.Y.Z tag after merge New SemVer release, GitOps PR labelled release
docs(blog): ... Silent Runs on the main push if blog files changed New content-<sha> image, GitOps PR labelled content
chore: / refactor: Silent (changelog only) No No deploy until the next feat: / fix:

Image tags in GHCR:

  • vX.Y.Z, X.Y, latest — produced by release publishes (kept forever)
  • content-<short-sha> — produced by blog content publishes (auto-pruned by the GHCR retention workflow)
  • scheduled-<yyyymmdd> — produced by scheduled blog publishes (auto-pruned by the GHCR retention workflow)

Dependabot is configured for weekly updates across npm, Docker, and GitHub Actions dependencies, with grouping for Angular and ESLint packages.

All base images in the Dockerfile are pinned by SHA256 digest for reproducible builds. The CI workflows pin every action by commit SHA.


Project Structure
src/
├── app/
│   ├── core/
│   │   ├── services/          # BlogService, SeoService, ThemeService, etc.
│   │   └── shell/             # Main layout (sidebar + tab bar + content + mobile nav)
│   ├── features/
│   │   ├── hero/              # Landing page
│   │   ├── about/             # About section
│   │   ├── experience/        # Work timeline
│   │   ├── skills/            # Technical skills grid
│   │   ├── projects/          # Project showcase
│   │   ├── certifications/    # Certification cards
│   │   ├── contact/           # Contact form
│   │   ├── blog/              # Blog index + post viewer
│   │   └── not-found/         # Glitch terminal 404 page
│   ├── ui/                    # Reusable components (glass-card, glow-button, sidebar, etc.)
│   └── shared/                # Shared utilities
├── assets/
│   ├── blog/
│   │   ├── posts.json         # Blog post manifest
│   │   └── posts/             # Markdown blog posts
│   ├── icons/                 # PWA icons, skill icons
│   └── images/                # Profile, portfolio, certification images
├── styles.css                 # Global design tokens
└── index.html                 # App shell with meta tags, font loading, favicon links
Routes
Route Component SSG
/ HeroComponent Prerendered
/about AboutComponent Prerendered
/experience ExperienceComponent Prerendered
/skills SkillsComponent Prerendered
/projects ProjectsComponent Prerendered
/projects/:slug ProjectDetailComponent Prerendered (per project)
/projects/tag/:tag ProjectsTagComponent Prerendered (per tag)
/certifications CertificationsComponent Prerendered
/contact ContactComponent Prerendered
/blog BlogComponent Prerendered
/blog/:slug BlogPostComponent Prerendered (dynamic)
/blog/tag/:tag BlogTagComponent Prerendered (per tag)
** NotFoundComponent Client-side (CSR fallback)
Blog Authoring

Posts are Markdown files in src/assets/blog/posts/. Metadata lives in src/assets/blog/posts.json:

{
  "slug": "my-post",
  "title": "My Post Title",
  "date": "2026-04-01",
  "excerpt": "A short description.",
  "tags": ["angular", "typescript"],
  "readingTime": "5 min"
}

The post-build script scripts/inject-meta.mjs injects per-route and post-specific OG/Twitter meta into each prerendered HTML file.

Image assets dropped into src/assets/images/blog/<post>/ can be optimized in-place to WebP via:

npm run optimize:images           # write .webp siblings, keep originals
npm run optimize:images -- --replace   # replace .jpg/.png originals with .webp

The script is idempotent — re-running skips already-optimized files. Remember to update markdown image extensions to .webp after using --replace.

Project Showcase Authoring

/projects is GitHub-driven. To add a project:

1. Tag the repo on github.com. Open the repo page, click the ⚙ on the "About" sidebar, and add portfolio to the Topics field. That's the one mandatory step — a user-scoped topic search (user:<you> topic:portfolio fork:false archived:false) is run out of band by scripts/fetch-projects-github.mjs on the scheduled refresh-projects.yml workflow.

2. (Optional) Override editorial fields locally. src/app/core/data/projects.overrides.json is the curation layer — each repos[] entry is keyed by owner/name and can override any of:

{
  "repo": "faisalkhan91/Bookstore",
  "title": "Bookstore", // defaults to repo name
  "description": "Hand-written blurb.", // falls back to README excerpt, then repo.description
  "image": "assets/images/portfolio/BookStore.png",
  "tags": ["Angular", "TypeScript"], // merged with repo topics
  "featured": true, // glow on the card + homepage strip
  "order": 1 // display order; unset entries sort by pushedAt desc
}

Drop the screenshot under src/assets/images/portfolio/. Overrides are union'd with the topic search — a repo that's both topic-tagged and in repos[] gets both the selection AND the overrides.

3. Refresh the cache. Run npm run fetch:projects locally to hit the GitHub API and write:

  • src/app/core/data/projects.generated.ts — the full Project[] consumed by ProjectsDataService.
  • src/app/core/data/projects.generated.json — a JSON twin for .mjs scripts (sitemap, inject-meta).
  • scripts/cache/projects-github.json — normalised raw GitHub payload; commit this so offline / rate-limited builds keep working.

The fetch runs out of band in refresh-projects.yml (daily cron at 06:00 UTC + workflow_dispatch) with secrets.GITHUB_TOKEN wired in (5000 req/hr), committing the regenerated data via a PR; build:prod itself is network-free and consumes the committed projects.generated.{ts,json} + cache. Locally the script works unauthenticated (60 req/hr) and falls back to the committed cache if the network is unreachable.

4. Non-GitHub entries. projects.overrides.json's manual[] array accepts fully hand-authored entries (company work, talks, gists). Same rendering path as topic-sourced repos; no github block means no meta pills.

Removing a project: untag it on github.com and drop its repos[] entry (if any). Next build, it's gone — no route prerenders, sitemap drops it, detail page 404s.


Author

Faisal Khan

License

MIT — see LICENSE for details.

Changelog

See CHANGELOG.md for a detailed history of releases.

About

This is my personal profile and blogging application.

Topics

Resources

License

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Packages

 
 
 

Contributors