A fictional small-chain indoor climbing gym — Atlas Climbing — built to argue that WordPress is a real LMS at zero plugin cost, not just a marketing site that links out to Teachable. Fourteenth demo on the platform and the first of the Tier 2 batch (heavier, more architecturally diverse demos). Live at atlas.wp.philiprehberger.com.
The challenge
Coaches, gyms, training programs, and course creators reach for LearnDash by default — \$300+/year — without knowing that Tutor LMS Free covers 90% of what they actually need. The other 10% — certificate customization, advanced quiz logic, multi-instructor commissions — is genuinely worth the price for an established course business. The mistake is treating Pro as the starting line. Most agencies recommending LearnDash on the discovery call haven’t actually compared the free alternatives this decade.
The second mistake — deeper, harder to reverse — is letting the LMS plugin own the visual identity of the site. The default Tutor LMS course archive, single-course page, and student dashboard read as Tutor, not as your brand. Most demos shipping Tutor as a portfolio piece accept that and move on; ours doesn’t. The point of Atlas is that a third-party LMS can disappear into your brand without forking templates or breaking on plugin updates.
The approach
Three moves. First, a per-demo theme (atlas) on the chassis with the "summit" aesthetic baked in — granite charcoal primary (#2A2D31), lichen-green accent (#6B8A5A), chalk-white base (#F4F0E8), Inter Display headings, Inter body. Outdoor brand grown up; references are Patagonia.com and Black Diamond product pages, not Crossfit affiliate sites.
Second, two new shared blocks in the platform’s sfp-blocks library, neither of which is Atlas-specific. sfp-blocks/course-card is a server-rendered grid that queries any course CPT (defaults to Tutor’s courses) and renders cover, level badge, instructor row, lesson count, duration, and enroll CTA — filterable by featured-meta, level-meta, and course-category taxonomy. sfp-blocks/lesson-progress is a student-dashboard block that lists the logged-in user’s enrollments with progress bars and next-lesson CTAs, sourced from Tutor’s get_completed_lesson_count_by_course and get_lesson_count_by_course APIs. Logged-out visitors see a sign-in CTA; users with no enrollments see a configurable empty message. Both blocks ship with the platform and are now available to every future demo — the same shared-platform discipline that’s carried thirteen prior builds.
Third, surgical Tutor LMS theme overrides via assets/style-tutor-overrides.css, loaded conditionally only on Tutor routes (single course, lesson player, dashboard). The override targets stable Tutor class names (.tutor-btn, .tutor-course-topic, .tutor-progress-bar) rather than nested div chains — the rule we’d defend on any third-party-plugin-theming engagement. A second mu-plugin (atlas-tutor-setup.php) dequeues Tutor’s 120KB of CSS+JS on non-Tutor routes — Lighthouse perf went from 66 to 91 on the home page with that one change.
What’s on the site
- Home. Full-bleed hero (climber on slab, late-afternoon golden light, zero overlay text), eyebrow naming the three locations, a single declarative headline ("Skills that travel with you — from your first crag to your fiftieth."), featured-courses strip via
sfp-blocks/course-cardwithfeaturedOnly=true, instructor strip with Sam’s portrait + bio, and a free-to-start CTA that names the pricing explicitly ("Movement Fundamentals is free; the other two are \$12/mo or included with a gym membership") - /courses/. Themed Tutor LMS archive showing all three courses with level badges + instructor + duration + enroll CTAs. Tutor’s own template; our CSS overrides make it indistinguishable from theme-native markup
- /instructors/. Three instructor cards with portraits, datelines (Bend / Boulder / Salt Lake City), and substantive bios that name credentials (AMGA Single Pitch Instructor, AMGA Rock Guide) and pedagogical style ("won’t let you bail on a sequence until you’ve tried it three different ways") rather than the "passionate about climbing" default. Includes Kudzai who is named as the next-course instructor in development — signals that the curriculum grows, slowly, on purpose
- /dashboard/. The
sfp-blocks/lesson-progressblock in "stacked" layout. For the seededdemo-studentaccount (password insecrets/atlas.env) the dashboard shows three enrollments at 100% / 60% / 0% — Movement Fundamentals complete with certificate available, Anchor Building 3 of 5 lessons with the next-lesson CTA pointing at "Single-piece anchors", Multi-Pitch just enrolled. Logged-out visitors see a sign-in CTA - /about/. Three-section brand story ("three gyms, one curriculum, no filler"), explicit anti-pattern stance ("we don’t have a Tier 3 add-on, we don’t try to upsell you"), and three location cards with photos and one-line each on what makes each gym distinct — Bend bouldering-focused, Boulder taller pitches, Salt Lake home of the regional youth team
- Three single-course pages at
/courses/movement-fundamentals/,/courses/anchor-building-fundamentals/,/courses/multi-pitch-leading/. Tutor’s template themed via overrides — cover, level badge, instructor card, curriculum accordion, enroll CTA - 15 lessons + 3 quizzes nested under topics under courses. Each lesson is ~300 words of instructional prose written to the tonal register of a coaching session, not a marketing site. The Movement Fundamentals lessons cover footwork, hip position, reading slab, putting it together; Anchor Building covers why anchors fail through field inspection; Multi-Pitch covers planning through descents. Quizzes are stubbed (Tutor’s quiz UI works) — the lesson content is what carries the demo
- Demo banner across the top making it explicit that Atlas is fictional and pointing back to the platform
The stack in detail
_chassisparent theme — templates, parts, patterns, gotcha-busting CSS, filemtime-based stylesheet enqueue. Inherited from the platform via symlinkatlasper-demo theme (this repo) — declaresTemplate: _chassis, ships the granite + lichen + chalk + dawn-pinktheme.json, Inter Display for headings, Inter for body, JetBrains Mono for code. Ownsparts/header.html+parts/footer.html+templates/front-page.html+templates/archive-courses.html+templates/single-courses.html+ the 404. Loadsassets/style-tutor-overrides.cssconditionally on Tutor routes onlysfp-blocks/course-card— new shared block. Server-rendered, configurable post-type, level filter, featured-only filter, column count, instructor + lesson-count toggles. Reads_atlas_level+_atlas_featuredpost meta +_atlas_instructor_portrait_iduser meta. Available to every future LMS demosfp-blocks/lesson-progress— new shared block. Server-rendered, queries the logged-in user’s Tutor enrollments and renders progress bars + next-lesson CTAs. Two layouts (stacked, compact), optional certificate display, configurable empty + sign-in messages. Falls back gracefully when Tutor isn’t installed- Tutor LMS (free) — the engine. Course / topic / lesson / quiz CPTs, instructor + student roles, progress tracking, certificate generation, free enrollment flow. Installed via
wp plugin install tutor --activate, configured via the demo’s setup mu-plugin atlas-tutor-setup.phpmu-plugin — registers_atlas_*meta with REST + closure-wrapped sanitize_callbacks (PHP 8 strict-arity safety), tunes Tutor settings (free monetization, disabled email notifications, registration enabled), dequeues Tutor’s CSS+JS on non-Tutor routes (Lighthouse perf 66 → 91 from this one change), addsatlas-tutor-routebody classes for CSS scoping- Four
sfp-*mu-plugins inherited from the platform;atlas-banner-config.phpfilters the demo banner copy for LMS context - WordPress 6.x · PHP 8.3 · MySQL 8 on Apache 2.4 (the same EC2 t3.small that hosts every other demo on the hub)
.scripts/seed-pages.sh— idempotent seed creating 3 instructors + 1 sample student + 3 courses + 5 topics + 15 lessons + 3 quizzes + 3 enrollments with mixed progress + 5 pages + featured-image pins + front-page assignment. Re-runnable; preserves real IDs across runs viaensure_*helpers- Standard repo additions:
needed-images.mdspecifying the 11 site content images + 4 portfolio screenshots; real photos inphotos/for the hero, course covers, instructor portraits, gym interiors, and student avatar
Where we made tradeoffs
Tutor archive vs. FSE template. WordPress’s FSE template hierarchy looks for archive-courses.html — which we shipped, populated with the course-card block. Tutor LMS hijacks template_include and renders its own archive instead. We could have unhooked the filter and reclaimed our FSE template, but Tutor’s archive isn’t bad — once the CSS overrides land it’s indistinguishable from theme-native markup — and unhooking Tutor’s template-include is the kind of thing that breaks on a plugin update. The home page’s featured-courses strip uses our course-card block where we’re fully in control; /courses/ uses Tutor’s archive themed via CSS. Pragmatic call — the value of course-card is in any context other than the post-type archive, where Tutor’s own template is already the right answer.
Stripe test mode vs. free enrollment. Tutor’s built-in monetization in the free tier supports WooCommerce + EDD + Restrict Content Pro integrations, but not standalone Stripe. We could have layered WooCommerce on top for a real test-mode checkout flow, but the LMS pattern is the demo — not the payment plumbing — and adding WooCommerce would have doubled the surface area and the page weight. Set monetization to free, document the upgrade path explicitly in the case study ("production clients would integrate Stripe via Tutor Pro or layer WC on top"). The same logic applies to certificate customization — Tutor Free ships one certificate template, Pro ships a designer. The demo accepts the free template and names the limit in writing.
Lesson player JS bundle weight. Tutor’s lesson player ships ~80KB of CSS + ~120KB of JS gzipped. On the lesson player route that’s acceptable — users are committed to a course session, not bouncing. On the home page, the marketing strip, the about page, the instructors grid — pure waste, and the price was 25 perf points. The mu-plugin’s dequeue runs at priority 100 on wp_enqueue_scripts and wholesale-strips any handle prefixed tutor- / tutor_ on non-Tutor routes. Same pattern the bench demo uses for Paid Memberships Pro. Pattern documented for every future demo with a heavy frontend plugin.
Outcomes
- LMS demo live at atlas.wp.philiprehberger.com, end-to-end provisioned via the platform’s
provision-demo.shwith Tutor LMS installed viawp plugin installand configured via the demo’s setup mu-plugin - Lighthouse: 91 / 96 / 96 / 92 (perf / a11y / best-practices / SEO) after the dequeue. First version was 66 perf with Tutor’s assets loading on the home page; the conditional dequeue lifted perf 25 points without touching the visual design
- Two new shared blocks (
course-card,lesson-progress) join the platform’s sfp-blocks library — available to every future demo that needs a course catalog or a student dashboard, not just the next LMS demo - Sample student account demonstrates the full progress-tracking flow end-to-end: enroll, complete lessons, advance to next, complete course, certificate available. The dashboard renders three distinct states (100% / 60% / 0%) from one block instance — the kind of demo capture that lets the case-study screenshot do the selling
- Surgical-CSS-override pattern for third-party plugin theming documented in writing: target stable plugin class names (not nested div chains), load conditionally on plugin routes only, dequeue plugin assets on routes the plugin doesn’t render. Pattern available for the next demo that themes a WC, a Polylang, an SSA, or any other third-party UI
- Fourteenth demo on the platform; first of the Tier 2 batch (the next eight demos targeting architectural diversity rather than service-business breadth). Identity lives in the demo, primitives live in the platform, neither side compromises — and now the platform has the LMS primitives ready for the next coach / gym / training-program client
