Complete Tutorial: Creating Categories and Subcategories Using Pages in Pelican
Introduction: Why Static Websites Can Beat Dynamic Ones
In the web development world, dynamic websites built with platforms like WordPress, Flask, or Laravel often get all the attention. They provide databases, admin dashboards, and real-time interactivity. However, thereβs a quiet champion that often goes overlooked: the static website.
Static websites, like the ones built with Pelican, are incredibly fast, secure, and require very little maintenance. Thereβs no backend database to worry about, no server-side code that can break or get hacked, and the speed is unmatched because the server simply delivers pre-built HTML files.
Whatβs even better? You can simulate categories and subcategories using only pages and folders, without complex taxonomies or dynamic systems. Let me walk you through the process step-by-step, using my actual setup.
1. Setting Up Your Python Environment and Pelican Project
Letβs start from scratch.
•Create a new Python environment: Itβs always a good idea to isolate your project.
python3 -m venv env
source env/bin/activate
•Install Pelican:
pip install "pelican[markdown]"
pip install pelican-sitemap pelican-related-posts pelican-neighbors pelican-render-math pelican-jinja2content
•Create your Pelican project:
pelican-quickstart
Youβll be asked a few questions:
- The site title
- Your author name
- The site URL (you can leave it empty for now)
Pelican will create a basic directory structure for you.
my-pelican-site/
βββ content/
β βββ articles/ # Optional if you write articles
β βββ pages/ # General pages (about, contact, etc.)
β βββ courses/ # Structured content for courses
β βββ images/ # Static images
β βββ pdfs/ # Static PDFs
β βββ extra/ # Additional files
βββ output/ # Generated static site (HTML files) after building
βββ pelicanconf.py # Main configuration file
βββ publishconf.py # Production-specific settings (optional)
βββ tasks.py # Optional automation script
βββ Makefile # For quick commands like `make html` and `make serve`
βββ requirements.txt # Python dependencies (optional but recommended)
βββ theme/ # Your custom Pelican theme
β βββ templates/ # Jinja2 HTML templates
β βββ static/ # Theme-specific CSS, JS, images
βββ pelican-plugins/ # Pelican plugins folder (if local)
2. Understanding the Key Components of a Pelican Project
Hereβs what matters for our goal:
•content/ β where you will write your pages, courses, articles, etc.
•output/ β the generated static website (HTML files, ready to publish).
•pelicanconf.py β the main configuration file.
•themes/ β where you can customize the design of your website.
We will mostly focus on:
Organizing the content/ folder
Writing rich Markdown files with metadata
Configuring pelicanconf.py properly to simulate categories and subcategories using folders.
3. Setting Up Your Pelican Configuration
Hereβs a stripped version of my real configuration focused on pages and folder hierarchy:
AUTHOR = 'your author name'
SITENAME = 'your site name'
SITEURL = ''
PATH = 'content'
TIMEZONE = 'UTC'
DEFAULT_LANG = 'en'
PLUGIN_PATHS = ['pelican-plugins']
PLUGINS = ['sitemap', 'related_posts', 'neighbors', 'render_math', 'jinja2content']
THEME = 'theme'
STATIC_PATHS = ['images', 'courses', 'extra', 'theme/static', 'pdfs']
PAGE_PATHS = ['pages', 'blogs', 'courses']
PAGE_URL = '{slug}.html'
PAGE_SAVE_AS = '{slug}.html'
PAGE_URLS = {'courses': 'courses/{subject}/{schoolyear}/{slug}.html'}
PAGE_SAVE_AS_TEMPLATES = {'courses': 'courses/{subject}/{schoolyear}/{slug}.html'}
DEFAULT_PAGINATION = 7
DIRECT_TEMPLATES = ['index', 'categories', 'tags', 'archives', 'courses']
Key Notes:
βοΈ We tell Pelican where to find pages: 'pages', 'blogs', 'courses'
βοΈ We build URLs for courses based on metadata: subject and school year.
βοΈ We donβt use articles here because weβre simulating categories with pages.
4. How to Create a Page (with Metadata)
You can create a page by writing a Markdown file inside content/pages/
or content/courses/
.
Hereβs a basic example for a course page:
Create it as content/courses/mathematics/course-1.md
Title: Course 1
Date: 2025-06-24
Type: course
Subject: mathematics
SchoolYear: 2025
Save_as: courses/mathematics/2025/course-1.html
URL: courses/mathematics/2025/course-1.html
Welcome to the Calculus Course for the year 2025. This course covers the fundamentals of calculus...
βοΈ The metadata at the top (Type
, Subject
, SchoolYear
) is what allows us to identify a page type, category and subcategory and filter pages later in the templates
βοΈ (URL
, Save_as
, ) gives us more control about saving the files
βοΈ you can add other metadata if you want, like description, path to thumbnail..., and all will be accessible to the template
βοΈ This page will automatically be saved as: output/courses/mathematics/2025/course-1.html
5. Creating the Illusion of Categories and Subcategories
Hereβs the trick:
- Subjects act as categories.
- School years act as subcategories.
And it all comes from the metadata
When you build the site, Pelican will automatically generate:
output/
βββ courses/
β βββ mathematics/
β β βββ 2025/
β β β βββ calculus.html
β β βββ 2024/
β β βββ algebra.html
β βββ physics/
β βββ 2025/
β βββ mechanics.html
Youβve now created categories and subcategories without using Pelicanβs built-in categories.
6. Building the Website
Run the following command to generate your site:
pelican content
Pelican will create the output/
folder containing your static site.
You can preview it using:
pelican --listen --autoreload --port 8001
Visit http://localhost:8001
to see your site in action.
7. Navigating the Structure on Your Website
Since we configured pelican to find a direct template courses
we create it in the templates folder exactly named "courses.html"
and then , go crazy with your code, do something like:
<h2 class="mb-4">Browse All Courses</h2>
<div class="container mt-5">
<h2 class="mb-4">Courses by Subject and School Year</h2>
{% set subjects = pages | selectattr('metadata.type', 'equalto', 'course') | map(attribute='metadata.subject') | unique | sort %}
<ul class="list-group">
{% for subject in subjects %}
<li class="list-group-item mb-3">
<h4 class="mb-2">{{ subject | title }}</h4>
{% set schoolyears = pages
| selectattr('metadata.type', 'equalto', 'course')
| selectattr('metadata.subject', 'equalto', subject)
| map(attribute='metadata.schoolyear')
| unique | sort %}
<ul class="ml-4">
{% for year in schoolyears %}
<li class="mb-2">
<strong>{{ year.replace('-', ' ') | title }}</strong>
{% set courses = pages
| selectattr('metadata.type', 'equalto', 'course')
| selectattr('metadata.subject', 'equalto', subject)
| selectattr('metadata.schoolyear', 'equalto', year)
| sort(attribute='date', reverse=True) %}
<ul class="ml-4">
{% for course in courses %}
<li class="mb-1">
<a href="{{ SITEURL }}/{{ course.url }}" class="text-decoration-none text-primary">
{{ course.title or course.metadata.coursetitle }}
</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
We go directly to http://localhost:8001/courses
to see it in action.

8. Final Thoughts
By combining:
Rich metadata in your Markdown files
A clean folder hierarchy
Dynamic template filtering
You can build the illusion of a fully categorized, subcategorized website without using databases or complex backends.
Itβs fast.
Itβs secure.
Itβs flexible.
You now have the power to create a blazing-fast static site that rivals, and in many ways outperforms, dynamic CMS platforms.
If you want to extend this system later, you can easily:
Add pagination
Track views
Create search pages using plugins like
tipue_search