Namaste Salesforce — a Ghost theme for a Salesforce learning site
Back to projectAn 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.
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 Ghosthash-slugs. - Rebuilt
post-course.hbsandpost-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 viahasPart/isPartOf.
What broke / what was tricky
Two bugs cost the most time:
- 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. - The course body silently not rendering.
{{#if content}}always evaluated false becausecontentis a Ghost helper, not a boolean — needed{{#if html}}instead. Separately, paid course posts hadvisibility: paidset on the whole post, which gated the marketing overview along with the lessons; the fix was to keep the course post itselfpublicand only gate the lessons.
{{#if html}}
{{content}}
{{/if}}
When a Ghost helper name (likecontent) 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