Pathways & Content Blocks
Create structured learning journeys that guide learners through multiple courses and modules. This module covers creating pathways with content blocks that add narrative context, callouts, and course/module references to enhance the learning experience.
Learning Objectives
- Understand the role of pathways in the content hierarchy
- Create pathway JSON files with courses or modules
- Use content blocks to add narrative structure (text, callout, course_ref, module_ref)
- Sync and publish pathways to HubDB
- Verify pathway rendering and troubleshoot common issues
- Apply best practices for pathway design and learner guidance
Prerequisites
- Completion of "Authoring Basics: Modules, Front Matter, and Sync" module
- Completion of "Assembling Courses from Modules" module (recommended)
- Understanding of JSON syntax and structure
- Access to the hh-learn repository
- Node 18+ installed
- HubSpot private app token configured in
.env
file
Scenario: Creating a Learning Pathway
You're a learning architect who has created several courses and modules. Now you want to assemble them into a curated pathway that guides learners through a comprehensive learning journey. You'll create a pathway with narrative content blocks, sync it to HubDB, and verify it renders correctly.
Step 1: Understand Pathway Structure
Pathways are the top level of the content hierarchy:
Pathways (Learning Journeys)
└─ Courses (Structured Units)
└─ Modules (Atomic Lessons)
Pathways are defined in JSON files at content/pathways/<slug>.json
:
# Navigate to pathways directory
cd content/pathways
# List existing pathways
ls -la
You'll see examples like:
getting-started.json
(references modules directly)getting-started-with-courses.json
(references courses)
Key characteristics:
- Group 1-5 courses or modules
- Add narrative structure with content blocks
- Award badges upon completion
- Track learner progress
Step 2: Choose Your Pathway Type
Pathways can reference either courses or modules (or both):
Type 1: Course-based pathway (recommended)
{
"slug": "kubernetes-mastery",
"title": "Kubernetes Mastery",
"summary_markdown": "Comprehensive Kubernetes learning journey...",
"courses": [
"kubernetes-essentials",
"kubernetes-advanced-networking",
"kubernetes-production"
]
}
Type 2: Module-based pathway (fallback)
{
"slug": "docker-basics",
"title": "Docker Basics",
"summary_markdown": "Learn Docker fundamentals...",
"modules": [
"intro-to-docker",
"docker-images",
"docker-compose"
]
}
Type 3: Mixed (both courses and modules)
{
"slug": "cloud-native-journey",
"title": "Cloud Native Journey",
"summary_markdown": "Master cloud-native technologies...",
"courses": [
"kubernetes-essentials"
],
"modules": [
"intro-to-containers",
"kubernetes-networking"
]
}
Decision guide:
- Use
courses
when you have structured multi-module learning units - Use
modules
for simpler pathways or when courses don't exist yet - Templates prefer courses when both are present
Step 3: Create Your Pathway JSON File
Create a new pathway file:
# Create pathway file
cat > content/pathways/cloud-native-fundamentals.json << 'EOF'
{
"slug": "cloud-native-fundamentals",
"title": "Cloud Native Fundamentals",
"summary_markdown": "Master the essential technologies and practices for building cloud-native applications.\n\nThis pathway covers containerization with Docker, orchestration with Kubernetes, and production best practices. You'll gain hands-on experience with the core tools used by modern DevOps teams.",
"courses": [
"getting-started-virtual-lab",
"hedgehog-lab-foundations"
],
"badge_image_url": "",
"display_order": 5,
"tags": "cloud-native,kubernetes,docker,getting-started"
}
EOF
Required fields:
slug
: Unique identifier (lowercase, hyphen-separated)title
: Display name for the pathwaysummary_markdown
: Rich description (2-4 paragraphs, Markdown supported)courses
ORmodules
: At least one must be present
Optional fields:
badge_image_url
: Completion badge graphicdisplay_order
: Sorting weight (lower = earlier in list)tags
: Comma-separated topics
Verify JSON syntax:
cat content/pathways/cloud-native-fundamentals.json | jq .
# Should parse and pretty-print without errors
Step 4: Add Content Blocks for Narrative Structure
Content blocks add rich narrative between courses/modules. Let's enhance the pathway:
cat > content/pathways/cloud-native-fundamentals.json << 'EOF'
{
"slug": "cloud-native-fundamentals",
"title": "Cloud Native Fundamentals",
"summary_markdown": "Master the essential technologies and practices for building cloud-native applications.\n\nThis pathway covers containerization with Docker, orchestration with Kubernetes, and production best practices. You'll gain hands-on experience with the core tools used by modern DevOps teams.",
"courses": [
"getting-started-virtual-lab",
"hedgehog-lab-foundations"
],
"badge_image_url": "",
"display_order": 5,
"tags": "cloud-native,kubernetes,docker,getting-started",
"content_blocks": [
{
"id": "intro",
"type": "text",
"title": "Welcome to Cloud Native Fundamentals",
"body_markdown": "Cloud-native applications are designed to run in dynamic, distributed environments like Kubernetes. This pathway will teach you the foundational skills needed to build, deploy, and manage cloud-native applications.\n\nYou'll start by setting up your learning environment, then progress through containerization and orchestration fundamentals."
},
{
"id": "prerequisites",
"type": "callout",
"title": "Before You Begin",
"body_markdown": "**Prerequisites:**\n- Basic command-line familiarity\n- Understanding of web applications and servers\n- A cloud account (Google Cloud, AWS, or Azure)\n\nEstimated total time: 90 minutes"
},
{
"id": "section-setup",
"type": "text",
"title": "Part 1: Setting Up Your Environment",
"body_markdown": "First, you'll learn how to access the Hedgehog Virtual Lab across different cloud providers. Choose the course that matches your cloud provider."
},
{
"id": "course-vlab",
"type": "course_ref",
"course_slug": "getting-started-virtual-lab"
},
{
"id": "section-foundations",
"type": "text",
"title": "Part 2: Kubernetes Fundamentals",
"body_markdown": "Now that your environment is ready, dive into Kubernetes. This course covers the essential concepts and hands-on labs you need to deploy containerized applications."
},
{
"id": "course-foundations",
"type": "course_ref",
"course_slug": "hedgehog-lab-foundations"
},
{
"id": "next-steps",
"type": "callout",
"title": "What's Next?",
"body_markdown": "Congratulations on completing Cloud Native Fundamentals! You're now ready to explore:\n\n- Advanced Kubernetes networking\n- Storage and persistence\n- Security and RBAC\n- CI/CD with GitOps"
}
]
}
EOF
Content block types:
1. text - Narrative paragraphs
{
"id": "unique-id",
"type": "text",
"title": "Section Title (optional)",
"body_markdown": "Markdown content..."
}
2. callout - Highlighted info boxes
{
"id": "unique-id",
"type": "callout",
"title": "Important Notice",
"body_markdown": "**Warning:** or **Tip:**..."
}
3. course_ref - Reference to a course
{
"id": "unique-id",
"type": "course_ref",
"course_slug": "course-slug-here"
}
4. module_ref - Reference to a module
{
"id": "unique-id",
"type": "module_ref",
"module_slug": "module-slug-here"
}
Validate updated JSON:
cat content/pathways/cloud-native-fundamentals.json | jq '.content_blocks[] | {id, type}'
# Should show all blocks with their IDs and types
Step 5: Validate Required Fields
Check that all required fields are present:
# Validate pathway structure
cat content/pathways/cloud-native-fundamentals.json | jq '{
slug,
title,
has_summary: (.summary_markdown != null),
has_courses: (.courses != null),
has_modules: (.modules != null),
course_count: (.courses | length // 0),
module_count: (.modules | length // 0),
content_block_count: (.content_blocks | length // 0)
}'
Expected output:
{
"slug": "cloud-native-fundamentals",
"title": "Cloud Native Fundamentals",
"has_summary": true,
"has_courses": true,
"has_modules": false,
"course_count": 2,
"module_count": 0,
"content_block_count": 7
}
Validation checklist:
- ✅
slug
present and unique - ✅
title
present - ✅
summary_markdown
present - ✅ Either
courses
ORmodules
present (at least one) - ✅ All content block IDs unique within pathway
- ✅ All
course_ref
slugs match existing courses - ✅ All
module_ref
slugs match existing modules
Step 6: Sync to HubDB (Dry Run)
Test the sync without making changes:
# Run dry-run sync
npm run sync:pathways -- --dry-run
Expected output:
📝 DRY RUN MODE - no changes will be made to HubDB
📄 Pathway: Cloud Native Fundamentals (cloud-native-fundamentals)
Courses: 2
Modules: 0
Module count: 6 (computed from courses)
Estimated minutes: 75
Content blocks: 7
Payload: {
"slug": "cloud-native-fundamentals",
...
}
✅ Dry run complete!
Summary: 1 succeeded, 0 failed
What the script does:
- Reads pathway JSON files
- Validates required fields
- Computes total module count from courses
- Computes total estimated minutes
- Converts Markdown to HTML
- Serializes arrays to JSON strings
- Shows payload that would be sent
If validation fails, check error message and fix the JSON.
Step 7: Sync to HubDB (Live)
Perform the live sync:
npm run sync:pathways
Expected output:
🔄 Starting pathways sync to HubDB...
Found 4 pathway(s) to sync:
✓ Updated: Getting Started
✓ Updated: Getting Started (Courses Demo)
✓ Updated: Getting Started with Hedgehog Lab
✓ Created: Cloud Native Fundamentals
📤 Publishing HubDB table...
✅ Sync complete! Table published.
Summary: 4 succeeded, 0 failed
The script:
- Creates/updates HubDB rows by slug (idempotent)
- Computes
module_count
from courses or modules - Computes
total_estimated_minutes
from modules - Publishes the pathways table
Step 8: Verify Pathway Rendering
Visit your pathway page to verify it renders correctly:
echo "Live URL: https://hedgehog.cloud/learn/pathways/cloud-native-fundamentals"
echo "Debug URL: https://hedgehog.cloud/learn/pathways/cloud-native-fundamentals?debug=1"
What to check:
- ✅ Pathway title and summary display
- ✅ Course/module count is correct
- ✅ Estimated time computed correctly
- ✅ Content blocks render in order
- ✅ Text blocks show title and body
- ✅ Callout blocks are highlighted
- ✅ Course/module references link properly
- ✅ Progress bar is visible (if logged in)
In debug view (?debug=1
):
- ✅
pathway_slug
matches your slug - ✅
course_slugs_json
ormodule_slugs_json
populated - ✅
content_blocks_json
contains all blocks - ✅
module_count
andtotal_estimated_minutes
computed
Step 9: Design Content Block Flow
Content blocks should tell a story. Follow this pattern:
1. Introduction (text block)
{
"id": "intro",
"type": "text",
"title": "Welcome",
"body_markdown": "Brief overview of what learners will achieve..."
}
2. Prerequisites (callout block)
{
"id": "prereqs",
"type": "callout",
"title": "Before You Begin",
"body_markdown": "List prerequisites, estimated time, tools needed..."
}
3. Learning sections (alternating text + course/module refs)
{
"id": "section-1",
"type": "text",
"title": "Part 1: Foundation",
"body_markdown": "Context for this section..."
},
{
"id": "course-1",
"type": "course_ref",
"course_slug": "foundation-course"
}
4. Transitions (text blocks between sections)
{
"id": "transition",
"type": "text",
"body_markdown": "Now that you understand X, let's explore Y..."
}
5. Conclusion (callout block)
{
"id": "next-steps",
"type": "callout",
"title": "What's Next?",
"body_markdown": "Suggested next pathways, resources, or topics..."
}
Step 10: Use Callouts Effectively
Callouts draw attention to important information:
Prerequisites callout:
{
"type": "callout",
"title": "Prerequisites",
"body_markdown": "**Required:**\n- Git installed\n- GitHub account\n- Basic shell skills\n\n**Recommended:**\n- Docker Desktop\n- VS Code"
}
Warning callout:
{
"type": "callout",
"title": "⚠️ Important",
"body_markdown": "This pathway assumes you have completed the Getting Started pathway. If you haven't, start there first."
}
Tip callout:
{
"type": "callout",
"title": "💡 Pro Tip",
"body_markdown": "Complete the modules in order for the best learning experience. Each builds on concepts from the previous one."
}
Time estimate callout:
{
"type": "callout",
"title": "⏱️ Time Commitment",
"body_markdown": "This pathway takes approximately 3 hours to complete. You can pause and resume anytime—your progress is automatically saved."
}
Step 11: Reference Courses vs Modules
Choose the right reference type:
Use course_ref when:
- Content is organized into structured courses
- You want learners to complete multiple related modules
- Courses provide narrative context
Example:
{
"type": "course_ref",
"course_slug": "kubernetes-essentials"
}
Use module_ref when:
- Pathway is module-based (no courses)
- You want to reference a specific module from a course
- Module provides a specific skill needed for the journey
Example:
{
"type": "module_ref",
"module_slug": "intro-to-kubernetes"
}
Mixing both:
{
"courses": ["kubernetes-essentials"],
"content_blocks": [
{
"type": "course_ref",
"course_slug": "kubernetes-essentials"
},
{
"type": "text",
"body_markdown": "For networking deep dive, complete this bonus module:"
},
{
"type": "module_ref",
"module_slug": "kubernetes-advanced-networking"
}
]
}
Step 12: Avoid Common Mistakes
❌ Mistake 1: No H1 headings in summary or body_markdown
{
"summary_markdown": "# Welcome\n\nThis pathway..."
}
✅ Fix: Use H2-H6 or bold text, never H1
{
"summary_markdown": "**Welcome**\n\nThis pathway..."
}
❌ Mistake 2: Duplicate content block IDs
{
"content_blocks": [
{"id": "intro", "type": "text", ...},
{"id": "intro", "type": "callout", ...}
]
}
✅ Fix: Ensure unique IDs
{
"content_blocks": [
{"id": "intro-text", "type": "text", ...},
{"id": "intro-callout", "type": "callout", ...}
]
}
❌ Mistake 3: Missing required course/module slug
{
"type": "course_ref"
}
✅ Fix: Include slug field
{
"type": "course_ref",
"course_slug": "getting-started"
}
❌ Mistake 4: Summary too long
{
"summary_markdown": "This pathway covers... (500 words of text)"
}
✅ Fix: Keep summary to 2-4 paragraphs (150-300 words)
Step 13: Test Cross-References
Verify all course and module references are valid:
# Extract all course references
cat content/pathways/cloud-native-fundamentals.json | jq -r '.content_blocks[] | select(.type=="course_ref") | .course_slug'
# Check each course exists
for slug in $(cat content/pathways/cloud-native-fundamentals.json | jq -r '.content_blocks[]? | select(.type=="course_ref")? | .course_slug'); do
if [ -f "content/courses/${slug}.json" ]; then
echo "✓ Course exists: $slug"
else
echo "✗ Course missing: $slug"
fi
done
# Extract all module references
cat content/pathways/cloud-native-fundamentals.json | jq -r '.content_blocks[] | select(.type=="module_ref") | .module_slug'
# Check each module exists
for slug in $(cat content/pathways/cloud-native-fundamentals.json | jq -r '.content_blocks[]? | select(.type=="module_ref")? | .module_slug'); do
if [ -d "content/modules/${slug}" ]; then
echo "✓ Module exists: $slug"
else
echo "✗ Module missing: $slug"
fi
done
All references should show ✓ (exists).
Step 14: Update or Modify a Pathway
Pathways are idempotent—you can resync with changes:
# Edit your pathway
nano content/pathways/cloud-native-fundamentals.json
# Make changes:
# - Add/remove courses or modules
# - Reorder content blocks
# - Update narrative text
# - Change metadata
# Sync again (updates existing row by slug)
npm run sync:pathways
Common updates:
- Adding new courses as they're created
- Reordering sections for better flow
- Updating callouts with new information
- Adding bonus modules
- Changing display_order for list sorting
After syncing, verify changes on the live page.
Concepts & Deep Dive
Content Hierarchy
The three-tier hierarchy provides different granularities:
Pathways (months-long journeys)
├─ Courses (weeks-long units)
│ └─ Modules (hour-long lessons)
│
└─ Modules (direct reference)
Design principles:
- Modules: Atomic, self-contained lessons (20-45 min)
- Courses: Thematic groups of 3-8 modules (2-6 hours)
- Pathways: Comprehensive journeys of 1-5 courses (5-20 hours)
Pathway vs Course Precedence
When a pathway has both courses
and modules
:
{
"courses": ["kubernetes-essentials"],
"modules": ["intro-to-docker"]
}
Templates prefer course_slugs_json
when rendering. The modules
array serves as fallback or supplement.
Best practice: Use one or the other, not both, unless adding bonus/prerequisite modules.
HubDB Pathways Schema
Pathways are stored in HubDB with this schema:
Field | Type | Purpose |
---|---|---|
slug |
Text | Unique identifier |
title |
Text | Display name |
summary_markdown |
Rich Text | HTML description |
course_slugs_json |
Rich Text | JSON array of course slugs |
module_slugs_json |
Rich Text | JSON array of module slugs |
module_count |
Number | Total modules (computed) |
total_estimated_minutes |
Number | Total time (computed) |
badge_image_url |
Text | Completion badge |
display_order |
Number | Sorting weight |
tags |
Text | Comma-separated topics |
content_blocks_json |
Rich Text | JSON array of blocks |
The sync script automatically computes:
module_count
: Sum of modules across coursestotal_estimated_minutes
: Sum of all module durations
Content Block Rendering
Templates process content blocks sequentially and render them based on type:
text blocks:
<div class="pathway-text-block">
<h3>Section Title</h3>
<p>Body markdown rendered as HTML...</p>
</div>
callout blocks:
<div class="pathway-callout">
<h4>Important</h4>
<p>Highlighted content...</p>
</div>
course_ref blocks:
<div class="course-card">
<h3><a href="/learn/courses/slug">Course Title</a></h3>
<p>Course summary, module count, estimated time</p>
</div>
module_ref blocks:
<div class="module-card">
<h3><a href="/learn/slug">Module Title</a></h3>
<p>Description, difficulty, estimated time</p>
</div>
Progress Tracking
Pathways include localStorage-based progress tracking:
How it works:
- Learner visits pathway page
- JavaScript stores progress in
localStorage
- Pathway page shows progress bar
- Module/course pages show "Back to pathway" link
- Completion tracked per learner session
For authors:
- No configuration needed
- Templates handle progress UI
- Works without authentication
- Future: sync to CRM for authenticated users
Badge Images
Badge images reward pathway completion:
Specifications:
- Size: 200 × 200 pixels (square)
- Format: PNG with transparency
- File size: < 50 KB
- Style: Icon or badge graphic
Adding badges:
{
"badge_image_url": "https://hedgehog.cloud/hubfs/badges/kubernetes-mastery.png"
}
If empty, no badge is displayed (learners still complete the pathway).
Troubleshooting
Pathway Not Syncing
Symptom: Sync fails with validation error
Cause: Missing required field or invalid JSON
Fix:
# Validate JSON syntax
cat content/pathways/your-pathway.json | jq .
# If error, check line number for syntax issue
# Check required fields
cat content/pathways/your-pathway.json | jq '{slug, title, summary_markdown, courses, modules}'
# All should show values, at least one of courses/modules must exist
Content Blocks Not Rendering
Symptom: Pathway page shows courses/modules but no narrative blocks
Cause: content_blocks
array missing or invalid
Fix:
# Check content_blocks structure
cat content/pathways/your-pathway.json | jq '.content_blocks[] | {id, type}'
# Validate each block has required fields:
# - id (unique)
# - type (text, callout, course_ref, or module_ref)
# - For course_ref: course_slug
# - For module_ref: module_slug
Course/Module Reference Broken
Symptom: Reference block shows "Course not found" or doesn't link
Cause: Referenced slug doesn't exist
Fix:
# Check course exists
ls content/courses/ | grep "your-course-slug"
# Check module exists
ls content/modules/ | grep "your-module-slug"
# Fix slug typo or create missing content
Module Count is 0
Symptom: Pathway shows "0 modules" even though courses have modules
Cause: Sync script couldn't read module front matter
Fix:
# Verify courses have modules
cat content/courses/your-course.json | jq '.modules'
# Check module front matter has estimated_minutes
grep "estimated_minutes" content/modules/intro-to-kubernetes/README.md
# Sync modules first, then pathways
npm run sync:content
npm run sync:pathways
Estimated Minutes Wrong
Symptom: Total time doesn't match expected duration
Cause: Module front matter missing estimated_minutes
or not synced
Fix:
# Check module durations
for module in $(cat content/pathways/your-pathway.json | jq -r '.modules[]?'); do
echo -n "$module: "
grep "estimated_minutes:" "content/modules/$module/README.md" | head -1
done
# Update missing values and sync
npm run sync:content
npm run sync:pathways
Summary Has H1 Heading
Symptom: Pathway page has multiple H1 tags (accessibility/SEO issue)
Cause: summary_markdown
contains # Heading
Fix:
# Check for H1 in summary
cat content/pathways/your-pathway.json | jq -r '.summary_markdown' | grep "^# "
# Replace with H2 or bold text
# ❌ "# Welcome\n\nThis pathway..."
# ✅ "**Welcome**\n\nThis pathway..."
Duplicate Content Block IDs
Symptom: Sync succeeds but blocks render incorrectly
Cause: Multiple blocks with same ID
Fix:
# Check for duplicate IDs
cat content/pathways/your-pathway.json | jq -r '.content_blocks[].id' | sort | uniq -d
# Any output indicates duplicates
# Rename to make unique:
# intro-1, intro-2, etc.
Resources
- Course Authoring Guide - Comprehensive authoring reference (see § Pathways)
- Content Sync Runbook - Pathways sync details (see § Pathways Mapping)
- Existing Pathways - Learn by example
- Existing Courses - Course examples for references
- JSON Validator - Online JSON syntax validator
- jq Documentation - Command-line JSON processor