Namaste Salesforce — a Ghost theme for a Salesforce learning site

Back to project

An open-source Ghost theme (forked from Casper) built into an LMS for Salesforce learners — courses, training tracks, a docs help center, dark mode, all on Tailwind CSS v4.

Build log 12 Jul 2026 1 min read

Part 3 — Building the course model: nested URLs, JSON-LD, curriculum

The biggest single session so far: turning the theme into an actual LMS. Courses, lessons, a catalog with filters, JSON-LD for search engines, and a proper content model behind all of it.

What I did

  • Built a full internal tag taxonomy in dummy-content/: price (#free/#paid), level (#level-beginner/intermediate/advanced), duration buckets, hero variants (#hero-1..5), lesson type, preview flags — all as Ghost hash- slugs.
  • Rebuilt post-course.hbs and post-lesson.hbs: hero variants, animated count-up stats, instructor cards, curriculum in 3 styles (list/cards/timeline), member/paid locking with preview lessons.
  • Wired nested permalinks in routes.yaml: /courses/ → catalog, /courses/{slug}/ → course, /courses/{primary_tag}/{slug}/ → lesson.
  • Added per-page JSON-LD (partials/jsonld/course.hbs, lesson.hbs) — Course/CourseInstance/LearningResource/VideoObject, linked via hasPart/isPartOf.

What broke / what was tricky

Two bugs cost the most time:

  1. Course slug vs. tag slug. Lessons nest under {primary_tag}, but the course page itself was permalinked off {slug} — if a course's post slug didn't exactly match its own course tag's slug, lessons would resolve fine but the course page would 301 to the wrong course. Fix: every course post's slug now has to equal its own tag's slug, enforced by convention, and existing courses had to be patched in sqlite to match.
  2. The course body silently not rendering. {{#if content}} always evaluated false because content is a Ghost helper, not a boolean — needed {{#if html}} instead. Separately, paid course posts had visibility: paid set on the whole post, which gated the marketing overview along with the lessons; the fix was to keep the course post itself public and only gate the lessons.
{{#if html}}
  {{content}}
{{/if}}
When a Ghost helper name (like content) shadows what looks like a plain field, an {{#if}} on it will silently be false forever — check whether you're testing a helper's truthiness or the underlying value.

Advertisement