Code

Under the hood — patterns and snippets I'm proud of.

A guided tour of this site's source. The trees show the shape of each side of the stack. The snippets after them are the decisions worth explaining — auth, API conventions, data modeling, and frontend patterns. Generic Rails and Angular is left out; everything here has a reason behind it.

Front-end (Angular 21 + SSR)

Standalone-first, signal-driven, lazy-loaded. Bootstrap 5 with data-bs-theme wired into a ThemeService so light and dark mode swap with a single class on <html>.

Back-end (Rails 8 API-only)

Versioned namespace, every endpoint documented by an rswag request spec that emits swagger/v1/swagger.yaml as a side effect. Auth is a stateless JWT — login returns a Bearer token, the SPA sends it in the Authorization header on every write. No cookies, no CSRF middleware: the header is not auto-attached cross-origin, so CSRF is structurally out of reach.


Authentication & Authorization

Stateless JWTs, no cookies, two login surfaces (password + Google), and a role split so non-admin users can exist without touching the CMS.

Auth-aware scope — one method, every content controller

Public callers see only published records. The same URL returns all records including drafts to an authenticated admin. One helper on BaseController, every content controller calls it. No per-controller conditional, no duplicated scope logic.

JWT auth interceptor — functional, no class boilerplate

One functional interceptor attaches Authorization: Bearer ... to every outbound request when a token is present. No cookies, no withCredentials, no CSRF exposure — the Authorization header is not auto-attached by browsers on cross-origin requests, so an attacker can never ride it.

Stateless JWT — HS256, 7-day expiry, nil on any decode error

Login signs a JWT with secret_key_base; every subsequent request decodes the Authorization: Bearer header in the Authentication concern. No sessions, no cookies, no SameSite / Secure dance to keep dev and prod in sync. rescue returns nil on any decode failure — expired, tampered, or malformed tokens all resolve to unauthenticated without leaking detail.

authGuard & adminGuard — return a UrlTree, not a navigate()

Functional guards. Returning a UrlTree lets Angular own the redirect — no race conditions with the router's own navigation, no imperative router.navigate() buried in a guard. Two tiers: authGuard requires any signed-in user; adminGuard also checks the role and bounces non-admins to home instead of leaving them in a permission limbo.

Login hardening — three independent layers

The admin route is public; the defenses are not. Rate limiting via rack-attack caps brute-force at 5 attempts per IP per 5 minutes. Per-account lockout survives IP rotation — failed_attempts and locked_until on the user record track the counter regardless of which IP is hitting it. Timing-safe auth via Rails 7.1+ User.authenticate_by runs bcrypt whether or not the email exists, so response time can't be used to enumerate accounts. The locked response includes locked_until so the frontend renders a real countdown without a second request.

API Patterns

Tests double as OpenAPI docs. Strong params in the Rails 8 style. N+1s caught at the query layer, not discovered in production.

rswag — the request spec IS the API contract

Every endpoint has a request spec written in the rswag DSL. The same file runs assertions and emits swagger/v1/swagger.yaml as a side effect. "Document the API later" is structurally impossible on this codebase — the spec failing means the doc is wrong, and the doc being wrong means the spec is missing.

Rails 8 params.expect — accepting association ID arrays

Rails 8 replaced params.require.permit with params.expect, which raises on unexpected input rather than silently dropping it. The non-obvious part is accepting technology_ids and tag_ids as typed arrays in the same call — the nested hash at the end of the array is the Rails idiom for "these keys must be arrays." One API call atomically replaces a project's M:M associations without a separate endpoint.

N+1 prevention — includes on reads, re-fetch after writes

includes(:technologies, :tags) on the index query is the obvious half: three queries regardless of how many projects are returned. The less obvious half is the re-fetch after create and update. When you call project.update(technology_ids: [1, 2]), ActiveRecord updates the join table but the in-memory association is stale — returning that object gives the frontend a corrupt data object with missing or outdated associations. The frontend then either renders incorrect state or fires follow-up requests to reconcile. Neither is acceptable. load_project re-fetches once with includes so the response is always complete and correct on the first round-trip — no redundant calls, no stale UI.

Weighted full-text search — 'simple' config, setweight, prefix matching

The projects page searches across four fields with explicit PostgreSQL weights: title and tagline rank highest (A), tech stack second (B), description third (C). Two deliberate config choices make it work for real data. First, 'simple' instead of 'english' — the English config removes stop words and applies stemming, which silently drops acronyms like IHM into an empty tsquery that matches everything. Simple skips that entirely. Second, to_tsquery with a :* suffix on each term enables prefix matching — typing ang finds Angular before the word is complete. A GIN index on the computed expression means the database resolves matches from the index, not a table scan, then eager-loads associations for only the matched rows — no N+1 on filtered results. The frontend debounces 300ms and uses switchMap to cancel in-flight requests when the user keeps typing.

Data Modeling

One polymorphic join table for tags. A position-scoped join for technologies. Frontend models that graduate from interfaces to classes when they need to carry behavior.

Polymorphic tagging — one taggings table, multiple models

Project and CommunityItem both support tags through a single polymorphic Tagging join model. The uniqueness constraint scoped to [taggable_type, taggable_id] enforces no duplicate tags per record at the database level. Blueprinter's association declaration renders both sides with no extra controller work — just includes(:tags) upstream to avoid N+1s.

Ordered join table — scope on the association, not the query

Technologies on a project are displayed in a deliberate order set in the admin. Rather than sorting at render time or adding an ORDER BY on every query, the ordering lives on the project_technologies association itself via a proc scope. Any caller that loads project.technologies gets them ordered by position — no one has to remember to sort.

Frontend Project — interface promoted to class with computed fields

ProjectData describes the raw API shape. Project wraps it as a real class so it can carry behavior. time_ago is computed once in the constructor against project_end and stored as a readonly field — cheaper than a getter, reads the same in templates. The ProjectsService maps every API response through new Project(p) so consumers always receive instances, never plain objects.

Note: Rails exposes computed attributes natively on the backend. This pattern is intentional on the frontend — it demonstrates how TypeScript class models can carry domain behavior and derived state at the boundary between the API and the UI, keeping that logic out of components and templates entirely.

Accessibility

Accessibility is personal to me. I use a wheelchair, and both of my maternal grandparents were blind. I have a lived understanding of what it means to rely on assistive technology — and that shapes how I build. Screen readers are first-class users of this site. Every interactive control has an accessible name, every landmark is in place, and ARIA is used where semantic HTML alone isn't enough — not as a substitute for it.

Skip link + landmark structure

A visually hidden skip link is the first focusable element on every page — keyboard and screen reader users can bypass the nav and jump straight to content. The page shell uses semantic landmarks (<nav>, <main>, <footer>) so screen reader users can jump between regions without navigating every link. The nav carries aria-label="Primary" to distinguish it if a second nav ever appears.

Icon-only controls — aria-label + aria-hidden on the SVG

Any button or link whose visible content is only an SVG needs an aria-label on the control itself. The SVG inside gets aria-hidden="true" so screen readers don't announce it separately — without that, VoiceOver reads the raw path data. Per-row actions carry the row identifier in their label so a screen reader user hears "Delete Ruby on Rails", not an ambiguous "Delete".

Table accessibility — caption, scope, labeled row actions

Every data table has either a visible <caption> or a visually-hidden one — never just aria-label on the <table> element, because that bypasses the caption slot screen readers announce before reading headers. Column headers carry scope="col"; row headers carry scope="row". This gives screen readers the full header association for every cell without a complex ARIA grid.

Expand/collapse — aria-expanded + aria-controls

The code snippet toggle buttons on this very page demonstrate the disclosure pattern. aria-expanded reflects current state so screen readers announce "collapsed" or "expanded" without any visual cue. aria-controls points to the panel ID so assistive technology can navigate directly to the revealed content. aria-label on the button incorporates the snippet name so repeated "Show code" buttons are distinguishable in a landmarks list.

Dialog / lightbox — role, aria-modal, keyboard dismiss

The photo lightbox uses role="dialog" and aria-modal="true" so screen readers know content behind it is inert. aria-label is derived from the image's own alt text — no separate label needed. tabindex="0" makes the overlay focusable so (keydown.escape) fires reliably from keyboard. Clicking the image itself calls stopPropagation() so users inspecting the photo don't accidentally close it.

SEO

Most companies and founders care deeply about search visibility. Every page on this site sets its own title, description, Open Graph, and Twitter Card tags server-side — crawlers see the complete metadata before JavaScript runs. JSON-LD structured data on the home page gives search engines a machine-readable identity card for the site.

SeoService — one call per page, server-side rendered

Angular SSR runs the component constructor on the server, so Title.setTitle() and Meta.updateTag() fire before the HTML response leaves the server. Crawlers and social previewers receive fully populated meta tags without executing JavaScript. The service also writes a <link rel="canonical"> using the current router URL so duplicate-content signals are never sent to search engines.

JSON-LD — machine-readable identity for search engines

The home page injects a schema.org/Person block into the document <head> at SSR time. Google, Bing, and AI overview engines parse this to understand who the site belongs to, what they do, and where they are — independently of the visible page content. The service manages the script tag lifecycle: it creates it on first call, updates it on subsequent calls, and removes it when navigating to a page that doesn't need structured data.

Frontend Patterns

Signals for state, afterNextRender for browser-only setup, CSS custom properties for theming so not a single style recalc in JS is needed to swap modes.

ThemeService — one effect() syncs three things

signal() for state, effect() for side-effects. Every theme change updates the custom-property class, Bootstrap's data-bs-theme attribute, and localStorage in one pass. SSR-safe via isPlatformBrowser — the localStorage write is guarded, the class and attribute changes are not (they're DOM operations that SSR renders as HTML).

Theme tokens — CSS custom properties, not Sass variables

Sass variables resolve at build time. CSS custom properties resolve at runtime. Switching one class on <html> recolors every component without touching JavaScript. No re-render, no style recalculation triggered by code — the browser handles it natively.

ParallaxDirective — afterNextRender, reduced-motion, rAF throttle

Three things in one directive. afterNextRender replaces the isPlatformBrowser guard — it only runs in the browser, so the scroll listener never registers during SSR. prefers-reduced-motion: reduce exits early before adding any listener at all — the directive respects the OS accessibility preference without a separate flag. requestAnimationFrame with a ticking guard caps work to one update per frame regardless of scroll event rate.

Lightbox — signal<string | null> with no modal library

A signal<string | null> is the entire state machine. null means closed; a URL means open — no boolean flag, no separate alt signal needed at the call site. The overlay renders with @if so it's absent from the DOM entirely when closed, not just hidden. Clicking the backdrop closes; clicking the image calls stopPropagation() so it doesn't. The tabindex="0" and (keydown.escape) binding make it keyboard-dismissible without a focus-trap library.