Website
Overview
This website was built with the help of Claude Code, and inspired by Steph Ango’s use of Obsidian and Jekyll to create a markdown-based static site hosted on GitHub Pages.
Technology Stack
Core Framework
- Jekyll - Static site generator with GitHub Pages compatibility
- GitHub Pages - Free hosting with automatic builds on push to main branch
Content Management
- Obsidian - Primary markdown editor for content creation
- Custom Obsidian Plugin - Converts
[[wiki-links]]to Jekyll-compatible URLs - Git - Version control with detailed commit history
Development Tools
- Claude Code and Cowork - AI-powered development assistant for implementation, edits, and content prototyping.
- Figma - Design prototyping, reference and specifications. Collaborating with Claude using the Figma MCP
- Umami - Privacy-first, cookieless analytics
- Loops - Email newsletter, the signup form on the Follow page, and audience/contact management
Design System
The site’s colors, typography, spacing, and components are documented in the Design System note — the single source of truth shared across humancrafted.co and sub-brand tools. In short: IBM Plex Mono for text and IBM Plex Sans for the wordmark and calls to action, a warm paper/ink palette with a yellow brand accent, and a wavy-underline pattern for every interactive link.
Project Thumbnail Creation
- Fusion 360 Drawing: Create technical drawing and export as DXF
- Fusion 360 Render: Simple render to export PNG with shadow
- Adobe Illustrator: Clean up vector illustration and composite with shadow
- Final Format: SVG for crisp display at any size with automatic dark mode conversion
Development Details
Jekyll Collections Structure
- _projects/ - Portfolio items with image galleries and lightbox functionality
- _docs/ - Documentation pages including tools, processes, and notes
- _posts/ - Blog-style content (if needed)
- _music/ - Track and artist notes, front matter only,
output: false(no per-track pages). The Music note renders them as one table straight fromsite.music. The.basefiles beside them drive Obsidian’s own table view and are excluded from the build, so the two are maintained separately. - _places/ - One note per place worth visiting (restaurants, breweries, farms, coffee shops, shops, stays), front matter only,
output: false— same pattern as music. The Places note renders them as pins on a map.places.basegives Obsidian its own table, to-visit, and map views.
Track previews
Tracks with an apple_music_url get a play button in the Music table — a thirty-second preview, the same one Apple’s own embed would give a listener who isn’t signed in. Full playback would mean MusicKit JS, a paid developer membership, and each visitor authenticating with their own Apple Music subscription, so it isn’t on the table.
The i= parameter in an apple_music_url is the track’s iTunes id, so script/fetch-previews.py can look up preview links with no searching or matching and write preview_url: into each note. Re-run it after adding tracks, or when previews go stale — they’re CDN assets and the links do rot. It rewrites only that one line, and --dry-run reports what it would change.
Playback is a plain <audio> element in assets/js/music-player.js, loaded only on pages whose front matter sets has_audio: true. One shared element means starting a track stops whatever was playing. The play/pause icons are injected by the JS rather than written into the note, for the same reason the STL viewer injects its hint icon: kramdown escapes inline SVG that comes out of markdown.
Custom Features
Reference Sections
- Two-column layout: a section label beside an auto-populated, comma-separated list
- Pulls live from Jekyll collections (projects, notes, tools, services, posts)
- Manual item lists with optional links and configurable limits
- Used on the Core page and the Design System note
Obsidian Integration Plugin
One linking convention, authored in Obsidian and resolved to Jekyll URLs at build time:
- Internal links are
[[slug|Display Text]](or[[slug]]) — a note’s filename is its URL - Targets are normalized, so
[[Shop V3]],[[shop_v3]], and[[shop-v3]]all land on the same page - Images embed with
![[image.ext]], with options as pipe segments —![[plan.svg|width=500]]sets a display width, and![[a.svg|column=3]]![[b.svg]]![[c.svg]]lays a line of embeds out as an equal-column grid (stacking to one column on phones) - 3D models embed the same way —
![[model.stl]]becomes a spinnable viewer - The same links work in Obsidian’s editor and graph view and on the live site
- A build-time safeguard skips stray or empty notes, so a broken link can’t take down the site
3D Model Viewer
Some project pages show a real 3D model you can grab and spin, rendered right in the browser
- Authored just like an image —
![[cord-keeper.stl]]— with the samewidth=option to size the viewer - Drag to rotate, pinch to zoom; it turns slowly on its own until you touch it
- The 3D library loads only on pages that have a model, and only once you scroll it into view, so every other page stays light
Places Map
The Places note plots the _places collection on an interactive map — every pin a place featured on Wisconsin Foodie, opened by a Top Chef contestant, recognized by the James Beard Foundation, or simply visited and liked. A place is one note; shows and awards are just fields on it, so a spot with three claims to fame is one pin with three source lines, each linking out (a Wisconsin Foodie pin links to the episode on their YouTube channel — timestamped to the moment the place appears, using the chapter markers the show publishes on its own uploads — the site is an unofficial index that points at the owners’ own video, never a copy of it).
- Leaflet vendored in
assets/js/lib/leaflet/(same.gitignorereason as three.js: notvendor/), no API key. Tiles are Esri’s free “Canvas” basemap (World_Light/Dark_Gray_Baseonly — not paired with Esri’s own_Referencelayer, which bundles county lines, city dots, and eventually roads on top of the state-level boundaries + labels the Base layer already bakes in), shown at its native neutral gray with no tint. Light and dark are genuinely different tile sets, not one filtered to fake the other —setTileTheme()removes and re-adds the layer on theme flip. Map zoom is capped atMAX_ZOOM = 10(tiles,fitBounds, and single-pinsetViewall share the constant): past ~11 even the Base layer starts rendering real street grids, so the cap keeps every view — auto-fit or a manual scroll/pinch — inside the clean range while still allowing enough zoom to separate pins clustered in one city. (History: Carto Positron + sepia tint → clashed with the paper background; Esri Base+Reference, no tint → state boundaries “from the theme” but reintroduced clutter; Esri Base alone, zoom-capped, no tint → a warm-tint retry read as “no road definition,” reverted; landed on plain neutral gray.) - Pins are
circleMarkers with a--foregroundring and a fill set by each place’s owncolor:front-matter field — a design-system hue name (olivefor Wisconsin Foodie,yellowfor Top Chef,grayfor anywhere permanently closed;gold/slatereserved for future categories) or a literal color. The color lives in the data, not in a rule buried in the JS; a place with nocolorfalls back to--accent. Named hues resolve through theme-aware--ds-*tokens, so a MutationObserver ondata-themeswaps every pin to its darker variant the moment the theme flips - A filter bar above the map (built by the JS from whatever sources exist in the data) toggles each source on and off, each label carrying a color-key dot in that category’s dominant pin hue; places with no source count as “personal”, and a multi-source place stays visible while any of its sources is on
- Pages opt in with
has_map: truefront matter;_includes/places-data.htmlemits the collection’s front matter as JSON andassets/js/places-map.jsbuilds the map from it - Wheel-zoom needs ctrl/meta, matching the STL viewer — plain page scroll is never hijacked. Fullscreen is the exception: there’s no page scroll to protect there (body is scroll-locked), so plain wheel zooms directly
- The map height uses
clamp(480px, 65vh, 820px)rather than a fixed pixel height, so a tall desktop window gets real vertical space instead of a letterboxed strip; the mobile breakpoint still pins it to a flat 360px - Per-source detail rides in flat front-matter keys (
wf_episode,tc_season,jb_award, …) beside a simplesources:list — deliberately not nested YAML, so Obsidian Bases and Liquid can both filter on it - A fullscreen toggle sits top-right next to the zoom control (Leaflet
L.Control, Lucide maximize/minimize icons). It’s a fixed-position overlay (inset: 0), not the Fullscreen API, since iOS Safari won’t fullscreen arbitrary elements — Escape or the button exits, andbodyscroll locks while it’s open. Leaflet setscontainer.style.position = "relative"inline at map init, which beats any stylesheet rule short of!important, so the.is-fullscreenoverride needs one.map.invalidateSize()fires ~60ms after the class toggle so tiles redraw at the new size instead of leaving gray gaps
Key Decisions
Why Jekyll?
- GitHub Pages compatibility for free hosting
- Simple markdown-based content management
- Ruby ecosystem with extensive plugins
- Static generation for fast performance
Why Obsidian?
- Natural wiki-style linking
- Visual graph view for content relationships
- Local-first with cloud sync options
- Markdown preview while writing
Why Custom Plugin?
- Maintains natural Obsidian workflow
- Automatic URL conversion without manual editing
- Supports both pages and collections
- Preserves link functionality in both environments
Resources
Documentation
- Jekyll Documentation - https://jekyllrb.com
- GitHub Pages Guide - https://pages.github.com
- Obsidian Help - https://help.obsidian.md
- Steph Ango - https://stephango.com/about