Zach Young — PortfolioCape Girardeau, MO · CDT

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.

Codebase Structure

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.


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 is a reusable role="dialog" / aria-modal="true" component. On open it moves focus into the dialog (the close button) and remembers the trigger; on close it returns focus there, so keyboard users never lose their place. (keydown.escape) dismisses, and (keydown.tab) / (keydown.shift.tab) are trapped to the single close button so focus can't escape to the inert content behind the modal. The overlay is tabindex="-1" — programmatically focusable, but not a tab stop. Clicking the image calls stopPropagation() so inspecting the photo doesn't close it.

Backend

Stateless JWTs, no cookies, password login over a single session endpoint, 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.

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.

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.

Full-text search — PostgreSQL tsvector, weighted fields, GIN index, signal-driven frontend

Search is not a nice-to-have. Every production application — from e-commerce platforms to AI-powered tools — depends on fast, relevant search to drive user engagement and retention. Modern AI search systems (semantic search, vector embeddings, LLM re-ranking) sit on top of the same foundational layer: a structured, weighted document corpus. This implementation uses PostgreSQL's native full-text search engine — no external service, no Elasticsearch, no Algolia — and produces the same weighted, indexed result set that AI ranking layers consume upstream.

Four fields are searched with explicit relevance weights: title and tagline at A (highest), tech stack at B, description at C. Two deliberate configuration choices make it work on real-world data. First, 'simple' instead of 'english' — the English config applies stop word removal and stemming, which silently collapses acronyms like IHM into an empty tsquery that matches everything. Simple skips normalization entirely: every token survives intact. Second, to_tsquery with a :* prefix suffix enables incremental matching — typing ang returns Angular results before the word is complete, the same behavior users expect from Google. A GIN index on the computed tsvector expression means PostgreSQL resolves matching IDs entirely from the index — no table scan — then eager-loads associations only for those rows. The frontend pipeline debounces at 300ms and uses switchMap to cancel any in-flight request the moment the user keeps typing, preventing stale responses from landing out of order.

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.

Frontend

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 — a reusable, signal-driven dialog with real focus management

A signal<string | null> is the entire state machine — null means closed, a URL means open. The whole thing is extracted into a reusable LightboxComponent: any page drops <app-lightbox #lightbox /> and calls lightbox.open(src, alt) from a real <button> trigger (keyboard-accessible, with its own aria-label; the image is decorative alt=""). The overlay renders with @if so it's absent from the DOM when closed. On open the component remembers the trigger and moves focus to the close button; on close it restores focus — and a single-control trapFocus keeps Tab inside the dialog. Real focus management, no modal library.

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.