Add proper category system for main navigation and content organisation #35

Closed
opened 2026-03-03 11:05:24 +00:00 by mark · 0 comments
Owner

Problem

All articles currently live under a single /articles/ page with flat tag-based filtering (?tag=slug). There is no concept of categories — everything is lumped together under "Articles". For a professional blog/articles site, categories should provide primary content organisation that's visible in the site's main navigation, with tags remaining as secondary cross-cutting labels.

Current State

  • Page tree: HomePage → ArticleIndexPage → ArticlePage (flat, single index)
  • Taxonomy: Tags only (django-taggit + TagMetadata for colours)
  • Navigation: Managed via SiteSettings.navigation_items (orderable, manually configured)
  • URLs: /articles/ (all), /articles/?tag=slug (filtered), /articles/slug/ (detail)
  • Feeds: /feed/ (all articles), /feed/tag/<slug>/ (per-tag)

What's Wrong

  • A visitor landing on the site sees "Articles" as a single bucket — there's no way to browse by content type (e.g. Reviews vs Benchmarks vs Tutorials)
  • The nav is just Home / Articles / About — no content-driven navigation
  • Tags work well for cross-cutting topics (Python, GPT-4, VS Code) but don't serve the role of primary content categorisation
  • This doesn't match how professional blog engines / article sites organise content

Proposed Solution: Category Snippet + RoutablePageMixin

Add a Category Wagtail snippet model and enhance ArticleIndexPage with RoutablePageMixin for clean category URLs.

Why a Snippet, Not a Page Type?

  • Categories are a taxonomy, not content pages — they don't need their own rich body, revisions, or drafts
  • Articles stay under ArticleIndexPage (no URL changes to existing content)
  • Simple to manage via Wagtail Snippets admin UI
  • Can still have per-category metadata (description, icon, colour, hero image)
  • Avoids the complexity of re-parenting articles under category pages

Category vs Tag (After)

Aspect Category Tag
Cardinality One per article (FK) Many per article (M2M)
Purpose Primary content type Cross-cutting topic label
Examples Reviews, Benchmarks, Tutorials, Opinion Python, GPT-4, VS Code, Cursor
Navigation Main nav items Filter buttons / tag cloud
URL /articles/category/<slug>/ /articles/?tag=<slug>

URL Structure (After)

URL Purpose Status
/articles/ All articles Unchanged
/articles/category/<slug>/ Articles filtered by category New
/articles/?tag=<slug> Articles filtered by tag Unchanged
/articles/<article-slug>/ Article detail Unchanged
/feed/ All articles feed Unchanged
/feed/category/<slug>/ Per-category feed New
/feed/tag/<slug>/ Per-tag feed Unchanged

Navigation Integration

Categories auto-populate in the main nav via a new template tag. Two presentation options:

  • Option A — Top-level nav items (recommended for ≤6 categories): Each category appears directly in the header nav (e.g., "Reviews", "Benchmarks", "Tutorials"). More prominent.
  • Option B — Dropdown under "Articles": A single "Articles" nav item with a dropdown showing categories. Cleaner for many categories.

Implementation supports both — a get_categories_nav template tag provides the data and the template decides presentation.


Implementation Plan

1. Create Category Model

New Category snippet model in apps/blog/models.py:

class Category(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    description = models.TextField(blank=True)
    hero_image = models.ForeignKey("wagtailimages.Image", null=True, blank=True, on_delete=SET_NULL)
    colour = models.CharField(max_length=20, choices=COLOUR_CHOICES, default="neutral")
    sort_order = models.IntegerField(default=0)
    show_in_nav = models.BooleanField(default=True)

Register as Wagtail snippet via SnippetViewSet in apps/blog/wagtail_hooks.py.

2. Add Category FK to ArticlePage

  • category = models.ForeignKey("blog.Category", on_delete=PROTECT)
  • Add FieldPanel("category") to content_panels
  • Migration strategy: Nullable initially → data migration creates a "General" default category and assigns it to existing articles → follow-up migration makes FK required

3. Enhance ArticleIndexPage with RoutablePageMixin

  • Add RoutablePageMixin to ArticleIndexPage
  • Add @route(r'^category/(?P<category_slug>[-\w]+)/$') for category filtering
  • Preserve existing tag filtering via query params
  • Support combined filtering: /articles/category/reviews/?tag=python
  • Pass active_category to template context

4. Create Category Navigation Template Tag

  • New get_categories_nav tag in apps/core/templatetags/core_tags.py
  • Returns categories where show_in_nav=True, ordered by sort_order
  • Each item includes: name, slug, url, article count

5. Update Nav Template

  • Add category items to desktop + mobile nav in templates/components/nav.html
  • Active state highlighting for current category
  • Support both dropdown and top-level presentation

6. Update Article Index Template

  • Add category filter section to article_index_page.html (above or alongside tag filters)
  • Show category name + description as page header when filtered by category
  • Update breadcrumbs: Home → Articles → Category Name

7. Update Homepage Template

  • Add category navigation to homepage sidebar in home_page.html
  • Replace or augment the "Explore Topics" section with category links
  • Optionally show latest article per category

8. Add Per-Category RSS Feed

  • New CategoryArticlesFeed class in apps/blog/feeds.py
  • Register at /feed/category/<slug>/ in config/urls.py

9. Update and Add Tests

  • Test Category model and snippet registration
  • Test ArticleIndexPage category routing (200s, 404 for bad slugs)
  • Test category nav template tag
  • Test category RSS feed
  • Test category + tag combo filtering
  • Update any existing tests affected by model changes

10. Seed Data Migration

  • Data migration to create initial categories (suggested: Reviews, Benchmarks, Tutorials, Opinion, News — to be confirmed)
  • Assign default "General" category to any existing articles
  • Optionally seed category nav items

Considerations

  • Backwards compatibility: All existing URLs remain unchanged. Tag filtering via ?tag= continues to work identically.
  • SEO: Category pages get proper canonical URLs, meta descriptions from category.description, and sitemap entries via RoutablePageMixin.
  • Migration strategy: Two-step — nullable FK first, then backfill + make required. No data loss risk.
  • Colour system: Can reuse or extend the existing COLOUR_CHOICES (cyan, pink, neutral) from TagMetadata.
  • Article constraint: Each article belongs to exactly one category. This is intentional — categories represent content type (what it is), while tags represent content topics (what it's about).
## Problem All articles currently live under a single `/articles/` page with flat tag-based filtering (`?tag=slug`). There is no concept of **categories** — everything is lumped together under "Articles". For a professional blog/articles site, categories should provide primary content organisation that's visible in the site's main navigation, with tags remaining as secondary cross-cutting labels. ### Current State - **Page tree**: `HomePage → ArticleIndexPage → ArticlePage` (flat, single index) - **Taxonomy**: Tags only (django-taggit + `TagMetadata` for colours) - **Navigation**: Managed via `SiteSettings.navigation_items` (orderable, manually configured) - **URLs**: `/articles/` (all), `/articles/?tag=slug` (filtered), `/articles/slug/` (detail) - **Feeds**: `/feed/` (all articles), `/feed/tag/<slug>/` (per-tag) ### What's Wrong - A visitor landing on the site sees "Articles" as a single bucket — there's no way to browse by content type (e.g. Reviews vs Benchmarks vs Tutorials) - The nav is just Home / Articles / About — no content-driven navigation - Tags work well for cross-cutting topics (Python, GPT-4, VS Code) but don't serve the role of primary content categorisation - This doesn't match how professional blog engines / article sites organise content --- ## Proposed Solution: Category Snippet + RoutablePageMixin Add a `Category` Wagtail **snippet** model and enhance `ArticleIndexPage` with `RoutablePageMixin` for clean category URLs. ### Why a Snippet, Not a Page Type? - Categories are a **taxonomy**, not content pages — they don't need their own rich body, revisions, or drafts - Articles stay under `ArticleIndexPage` (no URL changes to existing content) - Simple to manage via Wagtail Snippets admin UI - Can still have per-category metadata (description, icon, colour, hero image) - Avoids the complexity of re-parenting articles under category pages ### Category vs Tag (After) | Aspect | Category | Tag | |---|---|---| | Cardinality | One per article (FK) | Many per article (M2M) | | Purpose | Primary content type | Cross-cutting topic label | | Examples | Reviews, Benchmarks, Tutorials, Opinion | Python, GPT-4, VS Code, Cursor | | Navigation | Main nav items | Filter buttons / tag cloud | | URL | `/articles/category/<slug>/` | `/articles/?tag=<slug>` | ### URL Structure (After) | URL | Purpose | Status | |---|---|---| | `/articles/` | All articles | Unchanged | | `/articles/category/<slug>/` | Articles filtered by category | **New** | | `/articles/?tag=<slug>` | Articles filtered by tag | Unchanged | | `/articles/<article-slug>/` | Article detail | Unchanged | | `/feed/` | All articles feed | Unchanged | | `/feed/category/<slug>/` | Per-category feed | **New** | | `/feed/tag/<slug>/` | Per-tag feed | Unchanged | ### Navigation Integration Categories auto-populate in the main nav via a new template tag. Two presentation options: - **Option A — Top-level nav items** (recommended for ≤6 categories): Each category appears directly in the header nav (e.g., "Reviews", "Benchmarks", "Tutorials"). More prominent. - **Option B — Dropdown under "Articles"**: A single "Articles" nav item with a dropdown showing categories. Cleaner for many categories. Implementation supports both — a `get_categories_nav` template tag provides the data and the template decides presentation. --- ## Implementation Plan ### 1. Create Category Model New `Category` snippet model in `apps/blog/models.py`: ```python class Category(models.Model): name = models.CharField(max_length=100) slug = models.SlugField(unique=True) description = models.TextField(blank=True) hero_image = models.ForeignKey("wagtailimages.Image", null=True, blank=True, on_delete=SET_NULL) colour = models.CharField(max_length=20, choices=COLOUR_CHOICES, default="neutral") sort_order = models.IntegerField(default=0) show_in_nav = models.BooleanField(default=True) ``` Register as Wagtail snippet via `SnippetViewSet` in `apps/blog/wagtail_hooks.py`. ### 2. Add Category FK to ArticlePage - `category = models.ForeignKey("blog.Category", on_delete=PROTECT)` - Add `FieldPanel("category")` to `content_panels` - **Migration strategy**: Nullable initially → data migration creates a "General" default category and assigns it to existing articles → follow-up migration makes FK required ### 3. Enhance ArticleIndexPage with RoutablePageMixin - Add `RoutablePageMixin` to `ArticleIndexPage` - Add `@route(r'^category/(?P<category_slug>[-\w]+)/$')` for category filtering - Preserve existing tag filtering via query params - Support combined filtering: `/articles/category/reviews/?tag=python` - Pass `active_category` to template context ### 4. Create Category Navigation Template Tag - New `get_categories_nav` tag in `apps/core/templatetags/core_tags.py` - Returns categories where `show_in_nav=True`, ordered by `sort_order` - Each item includes: name, slug, url, article count ### 5. Update Nav Template - Add category items to desktop + mobile nav in `templates/components/nav.html` - Active state highlighting for current category - Support both dropdown and top-level presentation ### 6. Update Article Index Template - Add category filter section to `article_index_page.html` (above or alongside tag filters) - Show category name + description as page header when filtered by category - Update breadcrumbs: Home → Articles → Category Name ### 7. Update Homepage Template - Add category navigation to homepage sidebar in `home_page.html` - Replace or augment the "Explore Topics" section with category links - Optionally show latest article per category ### 8. Add Per-Category RSS Feed - New `CategoryArticlesFeed` class in `apps/blog/feeds.py` - Register at `/feed/category/<slug>/` in `config/urls.py` ### 9. Update and Add Tests - Test `Category` model and snippet registration - Test `ArticleIndexPage` category routing (200s, 404 for bad slugs) - Test category nav template tag - Test category RSS feed - Test category + tag combo filtering - Update any existing tests affected by model changes ### 10. Seed Data Migration - Data migration to create initial categories (suggested: Reviews, Benchmarks, Tutorials, Opinion, News — to be confirmed) - Assign default "General" category to any existing articles - Optionally seed category nav items --- ## Considerations - **Backwards compatibility**: All existing URLs remain unchanged. Tag filtering via `?tag=` continues to work identically. - **SEO**: Category pages get proper canonical URLs, meta descriptions from `category.description`, and sitemap entries via RoutablePageMixin. - **Migration strategy**: Two-step — nullable FK first, then backfill + make required. No data loss risk. - **Colour system**: Can reuse or extend the existing `COLOUR_CHOICES` (cyan, pink, neutral) from `TagMetadata`. - **Article constraint**: Each article belongs to exactly one category. This is intentional — categories represent content *type* (what it is), while tags represent content *topics* (what it's about).
mark closed this issue 2026-03-03 13:03:27 +00:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: nohype/main-site#35