Greetings, intrepid static site architect! You stand before the great Hall of Collections—a mystical chamber where scattered content transforms into organized, filterable, and dynamic displays. In this quest, you shall master the ancient arts of Jekyll collections, forging powerful layouts that respond to frontmatter incantations and user interactions alike.
The knowledge you gain here will allow you to build quest tracking systems, portfolio galleries, documentation hubs, and any content collection that demands organization and discovery. Whether you seek to catalog your own adventures or create portals for fellow travelers, this quest will arm you with the spells needed to succeed.
📜 The Legend Behind This Quest
In the realm of static site generators, Jekyll reigns as the venerable wizard-king—simple yet powerful, transforming markdown into magnificent web pages. But many who wield Jekyll’s power never venture beyond basic posts and pages, missing the treasure trove of collections.
Collections are Jekyll’s secret weapon for organizing related content: quests, products, team members, portfolio pieces—anything that shares a common structure. Combined with Liquid templating and frontmatter metadata, collections become the foundation for dynamic, data-driven interfaces that rival server-rendered applications.
This quest was born from a real implementation: building a quest tracking system for IT-Journey that displays quests by tier, allows filtering by type/difficulty/technology, shows statistics, and supports level-specific views. You will recreate this system, learning production-ready patterns along the way.
🎯 Quest Objectives
By the time you complete this epic journey, you will have mastered:
Primary Objectives (Required for Quest Completion)
- Configure Jekyll Collections - Set up a quests collection with custom output and permalinks
- Build Collection Layouts - Create
quest-collection.htmllayout with tier-based grouping - Create Reusable Includes - Build modular components (cards, filters, stats) for flexibility
- Implement Client-Side Filtering - Add JavaScript-powered filtering using data attributes
- Automate Frontmatter Management - Write Python script to ensure consistent metadata
Secondary Objectives (Bonus Achievements)
- Add Level-Aware Filtering - Make filters scope to specific levels when viewing level pages
- Handle Edge Cases - Gracefully manage mixed data types (Integer/String levels) in Liquid
- Create Responsive Layouts - Ensure the system works beautifully on all screen sizes
- Document the System - Update READMEs with usage examples and architecture
Mastery Indicators
You’ll know you’ve truly mastered this quest when you can:
- Explain the difference between Jekyll pages, posts, and collections
- Debug Liquid template errors using
--traceand strategic output - Extend the system to new collection types without modifying core layouts
- Mentor others in building collection-based interfaces
🗺️ Quest Prerequisites
📋 Knowledge Requirements
- Basic understanding of Jekyll site structure (
_config.yml,_layouts/,_includes/) - Familiarity with YAML frontmatter syntax and structure
- Completion of foundational Docker/Jekyll setup quests
- Basic JavaScript for DOM manipulation and event handling
🛠️ System Requirements
- Modern operating system (Windows 10+, macOS 10.14+, or Linux)
- Jekyll development environment (Docker recommended for consistency)
- Text editor with YAML/Liquid syntax highlighting (VS Code recommended)
- Python 3.x for automation scripts
- Git for version control
🧠 Skill Level Indicators
This 🔴 Hard quest expects:
- Solid foundation in HTML, CSS, and basic JavaScript
- Comfortable working with Jekyll and Liquid templating
- Ready for 4-6 hours of focused, hands-on learning
🌍 Choose Your Adventure Platform
Different platforms offer unique advantages for this quest. Choose the path that best fits your current setup and learning goals.
🐳 Docker Kingdom Path (Recommended)
# Start Jekyll development environment
docker-compose up -d
# Verify Jekyll is running
docker-compose exec jekyll bundle exec jekyll --version
# Build site with trace for debugging
docker-compose exec jekyll bundle exec jekyll build --trace
# Serve with live reload
docker-compose exec jekyll bundle exec jekyll serve --host 0.0.0.0 --port 4000 --livereload
Docker ensures consistent Ruby/Jekyll versions across all team members and CI/CD pipelines. This is the path of the wise.
🍎 macOS Kingdom Path
# Install Jekyll via Homebrew
brew install ruby
gem install bundler jekyll
# Navigate to project
cd ~/github/it-journey
# Install dependencies
bundle install
# Build and serve
bundle exec jekyll serve --port 4000 --livereload
macOS provides a native development experience. Ensure you’re using a Ruby version manager (rbenv or rvm) to avoid permission issues.
🪟 Windows Empire Path
# Using WSL2 (recommended) or RubyInstaller
wsl --install # If WSL not installed
# In WSL terminal
sudo apt update
sudo apt install ruby-full build-essential zlib1g-dev
gem install bundler jekyll
# Navigate and serve
cd /mnt/c/Users/YourName/github/it-journey
bundle install
bundle exec jekyll serve --port 4000
WSL2 provides a Linux environment within Windows, making Jekyll development smooth.
🐧 Linux Territory Path
# Install Ruby and Jekyll (Ubuntu/Debian)
sudo apt update
sudo apt install ruby-full build-essential zlib1g-dev
gem install bundler jekyll
# Navigate to project
cd ~/github/it-journey
# Install and serve
bundle install
bundle exec jekyll serve --port 4000 --livereload
Linux is Jekyll’s native habitat. Most commands work identically to macOS.
🧙♂️ Chapter 1: Understanding Jekyll Collections
Before we forge our quest tracking system, we must understand the mystical nature of Jekyll collections—repositories of related content that share structure and purpose.
⚔️ Skills You’ll Forge in This Chapter
- Jekyll collection configuration and output settings
- Collection-specific frontmatter defaults
- Understanding
site.collectionsandsite.<collection_name>
🏗️ The Collection Configuration Spell
Open your _config.yml and examine (or add) the collections configuration:
# _config.yml
# Collections configuration
collections_dir: pages # Optional: group collections in a subdirectory
collections:
quests:
output: true # Generate individual pages for each quest
permalink: /:collection/:categories/:name/
sort_by: level # Optional: default sort order
# Default frontmatter for quests
defaults:
- scope:
path: ""
type: quests
values:
layout: journals # Default layout for quest pages
fmContentType: quest
Key Configuration Options:
output: true- Generates individual HTML pages for each documentpermalink- URL structure for collection itemssort_by- Default sorting (overridable in templates)
📁 Collection Directory Structure
pages/
└── _quests/
├── README.md # Collection index
├── templates/ # Quest templates
│ └── main-quest-template.md
├── 0000/ # Level 0000 quests
│ ├── README.md # Level index
│ └── hello-world.md
├── 0001/ # Level 0001 quests
│ └── ...
└── 0101/ # Level 0101 quests (your current level!)
├── README.md
└── jekyll-quest-tracking.md # This quest!
🔍 Accessing Collection Data in Liquid
{%- comment -%} All quests in the collection {%- endcomment -%}
{% assign all_quests = site.quests %}
{%- comment -%} Filter quests with a specific attribute {%- endcomment -%}
{% assign hard_quests = site.quests | where: "difficulty", "🔴 Hard" %}
{%- comment -%} Filter using expressions {%- endcomment -%}
{% assign level_0101 = site.quests | where: "level", "0101" %}
{%- comment -%} Map to extract specific values {%- endcomment -%}
{% assign all_levels = site.quests | map: "level" | compact | uniq | sort %}
{%- comment -%} Count quests {%- endcomment -%}
{% assign quest_count = site.quests | size %}
🔮 Knowledge Check: Collection Fundamentals
- Can you explain why
output: trueis essential for individual quest pages? - What happens if a quest file lacks required frontmatter fields?
- How would you add a new collection (e.g.,
achievements) to your site?
⚡ Quick Wins
- Config Verified:
_config.ymlhas quests collection configured - Structure Understood: You can navigate the
_questsdirectory hierarchy - Data Access: You can output
168in a template
🧙♂️ Chapter 2: Building the Quest Collection Layout
Now we forge the heart of our system—a layout that transforms raw quest data into an organized, tier-based display.
⚔️ Skills You’ll Forge in This Chapter
- Creating Jekyll layouts that extend
default - Grouping quests by binary level tiers
- Conditional rendering based on page context
🏗️ The Collection Layout Architecture
Create _layouts/quest-collection.html:
---
layout: default
---
{%- comment -%}
Quest Collection Layout
Displays quests from the site.quests collection using frontmatter data.
Supports filtering by level (pass level parameter to filter to specific level).
{%- endcomment -%}
<div class="quest-collection">
<header class="quest-collection-header">
<h1>{{ page.title | default: "Quest Collection" }}</h1>
{% if page.description %}
<p class="lead">{{ page.description }}</p>
{% endif %}
</header>
{%- comment -%} Quest Statistics {%- endcomment -%}
{% include quest-stats.html level=page.level %}
{%- comment -%} Quest Filters {%- endcomment -%}
{% include quest-filters.html level=page.level %}
{%- comment -%}
Determine which quests to display:
- If page.level is set, filter to that level only
- Otherwise show all quests
{%- endcomment -%}
{% if page.level %}
{% assign filtered_quests = site.quests | where: "level", page.level %}
{% else %}
{% assign filtered_quests = site.quests %}
{% endif %}
{%- comment -%} Group quests by level tier {%- endcomment -%}
{% assign level_0000 = filtered_quests | where: "level", "0000" %}
{% assign level_0001 = filtered_quests | where: "level", "0001" %}
{% assign level_0010 = filtered_quests | where: "level", "0010" %}
{% assign level_0011 = filtered_quests | where: "level", "0011" %}
{%- comment -%} Continue for all 16 levels... {%- endcomment -%}
{%- comment -%} Apprentice Tier (0000-0011) {%- endcomment -%}
{% assign apprentice_quests = level_0000 | concat: level_0001 | concat: level_0010 | concat: level_0011 %}
{% if apprentice_quests.size > 0 and page.level == nil %}
<section class="quest-tier quest-tier-apprentice" data-tier="apprentice">
<h2 class="tier-header">
<span class="tier-icon">🌱</span>
<span class="tier-name">Apprentice Tier</span>
<span class="tier-levels">(Levels 0000-0011)</span>
<span class="tier-count">{{ apprentice_quests.size }} quests</span>
</h2>
<p class="tier-description">Foundation skills for beginning your IT journey.</p>
<div class="quest-grid">
{% for quest in apprentice_quests %}
{% include quest-card.html quest=quest %}
{% endfor %}
</div>
</section>
{% endif %}
{%- comment -%} Repeat for Adventurer, Warrior, Master tiers... {%- endcomment -%}
{%- comment -%} Page Content (for additional markdown content) {%- endcomment -%}
{% if content != "" %}
<section class="quest-collection-content">
{{ content }}
</section>
{% endif %}
</div>
🎨 Tier Styling Magic
/* Quest Tier Sections */
.quest-tier {
margin-bottom: 3rem;
padding: 1.5rem;
border-radius: 8px;
background: var(--bg-secondary, #f8f9fa);
}
/* Tier Colors */
.quest-tier-apprentice { border-left: 4px solid #28a745; } /* 🌱 Green */
.quest-tier-adventurer { border-left: 4px solid #fd7e14; } /* ⚔️ Orange */
.quest-tier-warrior { border-left: 4px solid #dc3545; } /* 🔥 Red */
.quest-tier-master { border-left: 4px solid #6f42c1; } /* 👑 Purple */
/* Quest Grid */
.quest-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
🔍 Knowledge Check: Layout Architecture
- Why do we pass
level=page.levelto the includes? - How does
| concat:work to combine arrays in Liquid? - What’s the purpose of
page.level == nilin conditionals?
⚡ Quick Wins
- Layout Created:
quest-collection.htmlexists in_layouts/ - Tier Grouping: Quests are grouped by binary level ranges
- Responsive Grid: Cards display in a flexible grid layout
🧙♂️ Chapter 3: Forging the Quest Card Component
Each quest deserves a beautifully crafted card that displays its vital statistics. We shall create a reusable include component.
⚔️ Skills You’ll Forge in This Chapter
- Creating Jekyll includes with parameters
- Using data attributes for JavaScript filtering
- Displaying frontmatter data elegantly
🏗️ The Quest Card Include
Create _includes/quest-card.html:
{% comment %}
Quest Card Include
Usage: {% include quest-card.html quest=quest %}
Required frontmatter in quest pages:
- title, difficulty, estimated_time, quest_type, level, description
{% endcomment %}
{% assign quest = include.quest %}
<article class="quest-card"
data-difficulty="{{ quest.difficulty | default: 'Unknown' }}"
data-quest-type="{{ quest.quest_type | default: 'main_quest' }}"
data-level="{{ quest.level | default: '0000' }}"
data-technology="{{ quest.primary_technology | default: '' }}"
data-skill-focus="{{ quest.skill_focus | default: '' }}"
data-title="{{ quest.title | escape }}"
data-description="{{ quest.description | default: '' | strip_html | escape }}">
<div class="quest-card-header">
{% if quest.difficulty %}
{% assign diff = quest.difficulty | split: ' ' | first %}
<span class="quest-difficulty" title="{{ quest.difficulty }}">{{ diff }}</span>
{% endif %}
{% if quest.quest_type %}
{% case quest.quest_type %}
{% when 'main_quest' %}
<span class="quest-type quest-type-main" title="Main Quest">🏰</span>
{% when 'side_quest' %}
<span class="quest-type quest-type-side" title="Side Quest">⚔️</span>
{% when 'bonus_quest' %}
<span class="quest-type quest-type-bonus" title="Bonus Quest">🎁</span>
{% when 'epic_quest' %}
<span class="quest-type quest-type-epic" title="Epic Quest">👑</span>
{% endcase %}
{% endif %}
</div>
<h3 class="quest-card-title">
<a href="{{ quest.url | relative_url }}">{{ quest.title | default: 'Untitled Quest' }}</a>
</h3>
{% if quest.description %}
<p class="quest-card-description">{{ quest.description | truncate: 120 }}</p>
{% endif %}
<div class="quest-card-meta">
{% if quest.estimated_time %}
<span class="quest-time" title="Estimated Time">🕐 {{ quest.estimated_time }}</span>
{% endif %}
{% if quest.level %}
<span class="quest-level" title="Level {{ quest.level }}">📊 Lvl {{ quest.level }}</span>
{% endif %}
{% if quest.primary_technology %}
<span class="quest-tech" title="Primary Technology">🛠️ {{ quest.primary_technology }}</span>
{% endif %}
</div>
<a href="{{ quest.url | relative_url }}" class="quest-card-link">Begin Quest →</a>
</article>
💡 The Power of Data Attributes
Data attributes (data-*) are the bridge between server-rendered HTML and client-side JavaScript:
<article class="quest-card"
data-difficulty="🔴 Hard"
data-quest-type="side_quest"
data-level="0101"
data-technology="jekyll">
JavaScript can then read these attributes:
const card = document.querySelector('.quest-card');
console.log(card.dataset.difficulty); // "🔴 Hard"
console.log(card.dataset.questType); // "side_quest" (camelCase conversion)
🔍 Knowledge Check: Component Design
- Why use
| escapewhen outputting to data attributes? - What’s the difference between
include.questandquest? - How would you add a new data attribute for filtering by tags?
⚡ Quick Wins
- Include Created:
quest-card.htmlexists in_includes/ - Data Attributes: Card outputs all filterable data as attributes
- Fallbacks: Component handles missing frontmatter gracefully
🧙♂️ Chapter 4: The Filtering Enchantment
Now we weave JavaScript magic to enable real-time filtering of our quest collection.
⚔️ Skills You’ll Forge in This Chapter
- Building filter UI with Liquid-generated options
- Implementing client-side filtering with JavaScript
- Handling the Integer/String level sorting edge case
🏗️ The Quest Filters Include
Create _includes/quest-filters.html:
{%- comment -%}
Quest Filters Include
Provides interactive filtering UI for quest collections.
{%- endcomment -%}
{%- comment -%} Collect unique values for filter dropdowns {%- endcomment -%}
{% assign all_quests = site.quests | where_exp: "q", "q.title != nil" %}
{%- if include.level and include.level != '' -%}
{% assign all_quests = all_quests | where: "level", include.level %}
{%- endif -%}
{%- comment -%} Get unique quest types {%- endcomment -%}
{% assign quest_types = all_quests | map: "quest_type" | compact | uniq | sort %}
{%- comment -%} Get unique difficulties {%- endcomment -%}
{% assign difficulties = all_quests | map: "difficulty" | compact | uniq | sort %}
{%- comment -%}
IMPORTANT: Level sorting edge case fix!
Some frontmatter has level: 1100 (Integer), others level: "1100" (String).
Liquid's sort filter fails on mixed types. We coerce all to strings.
{%- endcomment -%}
{% assign levels_raw = all_quests | map: "level" | compact | uniq %}
{% assign levels_joined = "" %}
{% for lval in levels_raw %}
{% assign levels_joined = levels_joined | append: lval | append: "," %}
{% endfor %}
{% assign levels = levels_joined | split: "," | uniq | sort %}
<div class="quest-filters" id="quest-filters">
<div class="filters-header">
<h3>🔍 Filter Quests</h3>
<button type="button" class="btn-reset-filters" onclick="resetAllFilters()">
Reset All
</button>
</div>
<div class="filters-grid">
{%- comment -%} Quest Type Filter {%- endcomment -%}
<div class="filter-group">
<label for="filter-quest-type">Quest Type</label>
<select id="filter-quest-type" onchange="applyFilters()">
<option value="">All Types</option>
{% for type in quest_types %}
<option value="{{ type }}">
{% case type %}
{% when 'main_quest' %}🏰 Main Quest
{% when 'side_quest' %}⚔️ Side Quest
{% when 'bonus_quest' %}🎁 Bonus Quest
{% when 'epic_quest' %}👑 Epic Quest
{% else %}{{ type | replace: "_", " " | capitalize }}
{% endcase %}
</option>
{% endfor %}
</select>
</div>
{%- comment -%} Additional filters: difficulty, level, technology, skill_focus, search {%- endcomment -%}
{%- comment -%} ... (similar pattern for each filter) ... {%- endcomment -%}
</div>
</div>
<script>
function applyFilters() {
const questType = document.getElementById('filter-quest-type').value.toLowerCase();
const difficulty = document.getElementById('filter-difficulty').value.toLowerCase();
const level = document.getElementById('filter-level').value.toLowerCase();
const technology = document.getElementById('filter-technology').value.toLowerCase();
const skillFocus = document.getElementById('filter-skill-focus').value.toLowerCase();
const searchTerm = document.getElementById('filter-search').value.toLowerCase().trim();
const questCards = document.querySelectorAll('.quest-card');
let visibleCount = 0;
questCards.forEach(card => {
const cardType = (card.dataset.questType || '').toLowerCase();
const cardDifficulty = (card.dataset.difficulty || '').toLowerCase();
const cardLevel = (card.dataset.level || '').toLowerCase();
const cardTechnology = (card.dataset.technology || '').toLowerCase();
const cardSkillFocus = (card.dataset.skillFocus || '').toLowerCase();
const cardTitle = (card.dataset.title || '').toLowerCase();
const cardDescription = (card.dataset.description || '').toLowerCase();
let show = true;
// Apply each filter
if (questType && !cardType.includes(questType)) show = false;
if (show && difficulty && !cardDifficulty.includes(difficulty)) show = false;
if (show && level && cardLevel !== level) show = false;
if (show && technology && !cardTechnology.includes(technology)) show = false;
if (show && skillFocus && !cardSkillFocus.includes(skillFocus)) show = false;
if (show && searchTerm) {
const matchesSearch = cardTitle.includes(searchTerm) ||
cardDescription.includes(searchTerm) ||
cardTechnology.includes(searchTerm);
if (!matchesSearch) show = false;
}
if (show) {
card.classList.remove('filtered-out');
visibleCount++;
} else {
card.classList.add('filtered-out');
}
});
// Update tier visibility
updateTierVisibility();
// Update results count
document.getElementById('results-count').textContent =
`Showing ${visibleCount} of ${questCards.length} quests`;
}
function updateTierVisibility() {
const tiers = document.querySelectorAll('.quest-tier');
tiers.forEach(tier => {
const visibleCards = tier.querySelectorAll('.quest-card:not(.filtered-out)');
tier.classList.toggle('filtered-empty', visibleCards.length === 0);
});
}
function resetAllFilters() {
document.getElementById('filter-quest-type').value = '';
document.getElementById('filter-difficulty').value = '';
document.getElementById('filter-level').value = '';
document.getElementById('filter-technology').value = '';
document.getElementById('filter-skill-focus').value = '';
document.getElementById('filter-search').value = '';
applyFilters();
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', applyFilters);
</script>
⚠️ The Integer/String Sorting Trap
The Problem: Jekyll’s Liquid will fail with comparison of Integer with String failed when sorting an array containing both 1100 (Integer) and "1100" (String).
The Solution: Coerce all values to strings using the join/split pattern:
{% assign levels_raw = all_quests | map: "level" | compact | uniq %}
{% assign levels_joined = "" %}
{% for lval in levels_raw %}
{% assign levels_joined = levels_joined | append: lval | append: "," %}
{% endfor %}
{% assign levels = levels_joined | split: "," | uniq | sort %}
This forces all values through string concatenation, ensuring uniform types.
🔍 Knowledge Check: Filtering Logic
- Why use
.toLowerCase()when comparing filter values? - How does
card.classList.toggle('filtered-empty', condition)work? - What would break if we used
==instead ofincludes()for difficulty?
⚡ Quick Wins
- Filters Work: Selecting a filter updates visible quests
- Reset Works: Reset button clears all filters
- Count Updates: Results count reflects current filter state
🧙♂️ Chapter 5: Python Automation for Frontmatter
Manual frontmatter updates are tedious and error-prone. We shall forge a Python spell to automate the task.
⚔️ Skills You’ll Forge in This Chapter
- Python file path manipulation with
pathlib - YAML frontmatter parsing and modification
- Batch processing directory contents
🏗️ The Frontmatter Update Script
Create scripts/development/update_level_readmes.py:
#!/usr/bin/env python3
"""
Script to ensure all level README files have consistent frontmatter:
- layout: quest-collection
- level: <directory_name>
- categories: quests
Only updates README.md files in direct child directories of pages/_quests
where the directory name is a 4-digit binary string (e.g., 0101, 1100).
"""
import os
import re
from pathlib import Path
# Calculate repo root: script is in scripts/development/
ROOT = Path(__file__).resolve().parents[2]
QUESTS_DIR = ROOT / 'pages' / '_quests'
# Regex to match binary level directories (0000-1111)
LEVEL_DIR_RE = re.compile(r'^[01]{4}$')
def process_readme(path: Path, level: str):
"""Update a README.md with required frontmatter fields."""
text = path.read_text(encoding='utf-8')
# Check for frontmatter
if not text.startswith('---'):
print(f"Skipping {path}: no frontmatter detected")
return
# Split into header and body
parts = text.split('---', 2)
header = parts[1]
body = parts[2] if len(parts) > 2 else ''
# Parse header into lines
lines = [line.rstrip('\n') for line in header.split('\n')]
# Check for required keys
has_layout = any(re.match(r'\s*layout\s*:', line) for line in lines)
has_level = any(re.match(r'\s*level\s*:', line) for line in lines)
has_categories = any(re.match(r'\s*categories\s*:', line) for line in lines)
# Append missing keys
new_lines = lines.copy()
appended = False
if not has_layout:
new_lines.append('layout: quest-collection')
appended = True
if not has_level:
new_lines.append(f'level: {level}')
appended = True
if not has_categories:
new_lines.append('categories: quests')
appended = True
# Write back if changes made
if appended:
new_header = '\n'.join(new_lines)
new_text = '---\n' + new_header + '\n---' + body
path.write_text(new_text, encoding='utf-8')
print(f"Updated frontmatter for {path}")
else:
print(f"No changes for {path}")
def main():
"""Process all level directories."""
if not QUESTS_DIR.exists():
print("Quests directory not found at:", QUESTS_DIR)
return
for entry in QUESTS_DIR.iterdir():
if entry.is_dir() and LEVEL_DIR_RE.match(entry.name):
readme = entry / 'README.md'
if readme.exists():
process_readme(readme, entry.name)
else:
print(f"Skipping {entry}: README.md not present")
if __name__ == '__main__':
main()
🔧 Running the Script
# From repo root with Python virtual environment
python scripts/development/update_level_readmes.py
# Or using the venv explicitly
.venv/bin/python scripts/development/update_level_readmes.py
Expected Output:
Updated frontmatter for /Users/you/github/it-journey/pages/_quests/0000/README.md
Updated frontmatter for /Users/you/github/it-journey/pages/_quests/0001/README.md
No changes for /Users/you/github/it-journey/pages/_quests/0101/README.md
Skipping /Users/you/github/it-journey/pages/_quests/1001: README.md not present
🔍 Knowledge Check: Automation Patterns
- Why use
Path(__file__).resolve().parents[2]instead of a hardcoded path? - What happens if a README has no frontmatter delimiter (
---)? - How would you extend this to add other frontmatter fields?
⚡ Quick Wins
- Script Runs: No errors when executing the script
- Files Updated: Level READMEs have consistent frontmatter
- Idempotent: Running twice produces no additional changes
🎮 Mastery Challenges
🟢 Novice Challenge: Add a New Filter
Goal: Add a “Learning Style” filter to the quest filters
Requirements:
- Add a new dropdown for
learning_style(hands-on, conceptual, project-based) - Add
data-learning-styleattribute to quest cards - Update the JavaScript
applyFilters()function
Success Criteria: Filtering by learning style works correctly
Estimated Time: 30 minutes
🟡 Apprentice Challenge: Statistics Enhancement
Goal: Add technology distribution to quest statistics
Requirements:
- Count quests by
primary_technologyin the stats include - Display top 5 technologies with counts
- Style the technology badges appropriately
Success Criteria: Stats show technology breakdown
Estimated Time: 45 minutes
🔴 Expert Challenge: Pagination
Goal: Implement pagination for large quest collections
Requirements:
- Limit displayed quests per tier to 6 initially
- Add “Show More” button per tier
- Track expanded state in localStorage
Success Criteria: Pagination improves page load and UX for large collections
Estimated Time: 1-2 hours
⚔️ Master Challenge: Search Autocomplete
Goal: Add autocomplete suggestions to the search filter
Requirements:
- As user types, show matching quest titles as suggestions
- Click suggestion to filter to that quest
- Keyboard navigation (up/down arrows, enter to select)
Success Criteria: Professional-quality search autocomplete
Estimated Time: 2-3 hours
🏆 Quest Completion Validation
Portfolio Artifacts Created
_layouts/quest-collection.html- Collection layout with tier grouping_includes/quest-card.html- Reusable quest card component_includes/quest-filters.html- Interactive filter UI with JavaScript_includes/quest-stats.html- Statistics display componentscripts/development/update_level_readmes.py- Frontmatter automation
Skills Demonstrated
- Created multi-component Jekyll layout system
- Implemented client-side filtering with data attributes
- Handled edge cases (Integer/String sorting, missing data)
- Wrote Python automation for batch frontmatter updates
Knowledge Gained
- Jekyll collections configuration and access patterns
- Liquid templating with includes and parameters
- JavaScript DOM manipulation and filtering logic
- Python pathlib and file processing
Final Validation
Build the site and verify:
docker-compose exec jekyll bundle exec jekyll build --trace
Check for:
- No Liquid errors in build output
- Quest collection pages render correctly
- Filters function as expected
- Statistics display accurate counts
🗺️ Quest Network Position
graph TB
subgraph Prerequisites
A[Level 0100: Frontend Docker] --> B[This Quest]
C[Level 0001: GitHub Pages] -.-> B
D[Level 0010: Terminal Enhancement] -.-> B
end
subgraph Current
B[Level 0101: Jekyll Quest Tracking]
end
subgraph Unlocks
B --> E[Level 1010: Automation & Testing]
B --> F[Level 1011: Feature Development]
B --> G[Level 1010: Hyperlink Guardian]
end
subgraph Parallel
B ~~~ H[Level 0101: Docker Mastery]
B ~~~ I[Level 0101: LazyTeX CV]
end
style B fill:#6f42c1,color:#fff
style A fill:#28a745,color:#fff
style E fill:#dc3545,color:#fff
style F fill:#dc3545,color:#fff
⚙️ Implementation Flow Diagram
flowchart LR
subgraph Data Layer
A[Quest Markdown Files] --> B[YAML Frontmatter]
B --> C[site.quests Collection]
end
subgraph Template Layer
C --> D[quest-collection.html Layout]
D --> E[quest-stats.html Include]
D --> F[quest-filters.html Include]
D --> G[quest-card.html Include]
end
subgraph Output Layer
E --> H[Statistics Display]
F --> I[Filter Dropdowns]
G --> J[Quest Cards with Data Attrs]
end
subgraph Client Layer
I --> K[JavaScript applyFilters]
J --> K
K --> L[Filtered Quest Display]
end
subgraph Automation
M[Python Script] --> B
end
style A fill:#0d6efd,color:#fff
style D fill:#6f42c1,color:#fff
style K fill:#28a745,color:#fff
style M fill:#fd7e14,color:#fff
🎉 Congratulations, Collection Architect!
You have successfully forged the Jekyll Quest Tracking system! Your mastery of collections, Liquid templating, and client-side filtering has created a powerful, reusable system for organizing and discovering content.
🌟 What’s Next?
Your newfound powers open several paths:
- Deepen Your Mastery: Explore Jekyll plugins for advanced functionality
- Expand Your Toolkit: Apply these patterns to portfolios, documentation sites, or blogs
- Apply Your Skills: Build a production quest tracking system for your own learning platform
- Join the Community: Share your implementations and help others learn
📚 Additional Resources
| Resource | Description |
|---|---|
| Jekyll Collections Docs | Official documentation |
| Liquid Reference | Complete Liquid syntax |
| CSS Grid Guide | Grid layout mastery |
| JavaScript Dataset API | Data attribute access |
📓 AI Collaboration Log
This quest was developed through AI-assisted content generation:
| Phase | AI Contribution | Human Validation |
|---|---|---|
| Planning | Analyzed open commits to understand implementation | Confirmed scope and objectives |
| Structure | Generated quest outline following templates | Reviewed for accuracy |
| Code Examples | Created Liquid/JS/Python snippets | Tested in Jekyll environment |
| Diagrams | Generated Mermaid visualizations | Verified relationships |
Key AI Insights:
- Identified Integer/String sorting issue from actual build error
- Suggested data attribute pattern for filtering
- Proposed Python automation based on observed frontmatter inconsistencies
🧠 Lessons & Next Steps
Key Takeaways
- Collections are powerful - They transform Jekyll from a blog engine into a CMS
- Data attributes bridge static/dynamic - Server-render data, filter client-side
- Automation saves time - Batch frontmatter updates prevent manual errors
- Edge cases matter - The Integer/String issue only surfaces with mixed data
Future Enhancements
- Add LocalStorage to persist filter preferences
- Implement lazy loading for large collections
- Add animated transitions for filter changes
- Create a “Favorites” feature for quest bookmarking
README Updates Required
- Update
/pages/_quests/README.mdwith this quest link - Update
/pages/_quests/0101/README.mdwith quest entry - Verify cross-references in related quests
✅ Quest Validation Checklist
Before committing, verify:
- Frontmatter Complete: All required fields populated with valid values
- Mermaid Renders: Both diagrams render correctly in preview
- Commands Tested: Jekyll build completes without errors
- README Updated: Level 0101 README includes this quest
- Links Work: All internal links resolve correctly
- Fantasy Theme: Content maintains RPG terminology and emojis
🔧 Kaizen Hooks
Suggested Improvements for Future Revisions
- Add more detailed CSS styling examples
- Include unit tests for the Python automation script
- Create a video walkthrough companion
- Add troubleshooting section for common Jekyll errors
Metrics to Monitor
- Quest completion rate (via feedback or analytics)
- Time to complete vs. estimated time
- Common stumbling points (via support questions)
Derivative Quest Ideas
- Side Quest: Custom Jekyll Plugins for Collections
- Bonus Quest: SEO Optimization for Collection Pages
- Epic Quest: Building a Full Learning Management System with Jekyll
May your collections be organized, your filters be fast, and your frontmatter be consistent! Ready for your next adventure? Check the Quest Map for your next challenge! ⚔️✨