Skip to main content

A comprehensive Wagtail CMS extension providing theming and foundational features.

Project description

Wagtail Feathers

A comprehensive Wagtail CMS extension providing foundational functionality for building sophisticated content management systems.

Python Version Django Version Wagtail Version License

[!CAUTION] This package is a beta version which means it is almost stable and definitely lacks some documentation and tests The package is aimed at new installations

Features

Wagtail Feathers provides a comprehensive set of reusable components and patterns:

๐Ÿ—๏ธ Advanced Page Architecture

  • FeatherBasePage: Foundation for all custom pages with comprehensive SEO capabilities
  • Flexible Content Blocks: Rich StreamField blocks for dynamic content composition
  • Page Relationships: Built-in related page functionality and cross-references

๐Ÿ“‚ Taxonomy & Classification

  • Hierarchical Categories: Tree-structured content classification
  • Flexible Subjects: Tagging and subject matter organization
  • Custom Attributes: Extensible metadata and properties system
  • Admin Integration: Beautiful select widgets and management interface

๐Ÿงญ Dynamic Navigation

  • Multiple Menu Types: Traditional hierarchical and StreamField-based navigation
  • Flexible Menu Items: Support for both page links and external URLs

๐Ÿ” SEO & Metadata

  • Built on Wagtail SEO: Extends Wagtail's built-in seo_title and search_description fields
  • Social Media Ready: Open Graph and Twitter Card support with dedicated image field
  • Structured Data: Automatic JSON-LD generation for articles, organizations, and more
  • Smart Fallbacks: Intelligent content detection for missing SEO fields
  • Search Engine Control: No-index/no-follow directives and robots meta tags
  • Reading Time: Automatic reading time calculation with SEO integration

๐ŸŽจ Theme System

  • Multi-Site Theming: Different themes per site
  • Template Discovery: Automatic theme-based template resolution
  • Asset Management: Theme-specific CSS/JS handling
  • Configuration: Site-wide settings and theme preferences

๐ŸŒ Internationalization (Optional)

  • Locale-Aware Ready: Models ready for i18n when you need it
  • Simple Translation: Use Wagtail's built-in translation features
  • Advanced Workflows: Optional wagtail-localize integration (recommended)

๐Ÿ‘ฅ Authorship & People

  • Author Management: Flexible author attribution system with AuthorType and PageAuthor models
  • People Directory: Staff and contributor profiles with PersonGroup organization
  • Social Integration: Social media links and profiles with SocialMediaSettings

โŒ Error Pages

  • Custom Error Pages: Configurable 404, and 500 error pages (TBC: 400, 403)
  • ErrorPage Model: Editable error pages with custom messages and images

โ“ FAQ System

  • FAQ Management: Structured FAQ system with FAQCategory and FAQ models
  • Categorized Content: Organize frequently asked questions by category

๐Ÿ”— Page Relationships

  • Related Content: RelatedPage, RelatedDocument, and RelatedExternalLink models
  • Cross-References: Built-in page relationship functionality
  • Geographic Tagging: PageCountry model for location-based content

Installation

Basic Installation (No i18n)

pip install wagtail-feathers

With Advanced Translation Workflows

pip install wagtail-feathers[localize]

Quick Start

  1. Add to Django Settings:

Basic Setup:

INSTALLED_APPS = [
    # ... your apps
    "wagtail_feathers",
    "wagtailmarkdown",
    "django_extensions",
    # ... other apps
]

Theming

# create a folder named "themes" at the 'BASE_DIR' of your project
...
@cached_property
def themes_dir(self) -> Path:
    """Get the themes directory from Django settings."""
    return getattr(settings, "BASE_DIR", Path.cwd()) / "themes"
...

Add all the themes you want, see the demo/themes folder for an example. Adapt the json file accordingly.

With Simple Translation:

INSTALLED_APPS = [
    # ... your apps
    "wagtail_feathers",
    "wagtail.contrib.simple_translation",  # Wagtail built-in
    # ... other apps
]

With Advanced Translation:

INSTALLED_APPS = [
    # ... your apps
    "wagtail_feathers",
    "wagtail_localize",
    "wagtail_localize.locales",
    # ... other apps
]
  1. Run Migrations:
python manage.py migrate
  1. Create Your First Page:
from wagtail_feathers.models import FeatherBasePage

class MyPage(FeatherBasePage):
    # Your custom fields here
    pass

Advanced Usage

Custom Page Models

from wagtail_feathers.models import FeatherBasePage, ItemPage
from wagtail_feathers.blocks import CommonContentBlock
from wagtail.fields import StreamField

class ArticlePage(ItemPage):
    body = StreamField([
        ("content", CommonContentBlock()),
    ], use_json_field=True)
    
    content_panels = ItemPage.content_panels + [
        FieldPanel("body"),
    ]

Taxonomy Integration

from wagtail_feathers.models import Category, Classifier, ClassifierGroup

# Create categories
category = Category.objects.create(name="Technology", slug="tech")

# Create classifier groups and classifiers
group = ClassifierGroup.objects.create(name="Topics", max_selections=3)
classifier = Classifier.objects.create(
    name="AI & Machine Learning",
    slug="ai-ml",
    group=group
)

Navigation Menus

from wagtail_feathers.models import Menu, MenuItem, FlatMenu, NestedMenu

# Create a flat menu
flat_menu = FlatMenu.objects.create(name="Main Navigation")

# Create a nested menu
nested_menu = NestedMenu.objects.create(name="Footer Navigation")

# Create menu items
MenuItem.objects.create(
    menu=flat_menu,
    link_page=home_page,
    link_text="Home",
    sort_order=1
)

Error Pages

from wagtail_feathers.models import ErrorPage

# Create custom error pages
error_404 = ErrorPage.objects.create(
    title="404",
    error_code="404",
    heading="Page Not Found",
    message="<p>The page you are looking for does not exist.</p>"
)

FAQ System

from wagtail_feathers.models import FAQCategory, FAQ

# Create FAQ categories and items
category = FAQCategory.objects.create(
    title="General Questions",
    slug="general"
)

faq = FAQ.objects.create(
    question="How do I get started?",
    answer="<p>Follow our quick start guide...</p>",
    category=category
)

SEO & Metadata

from wagtail_feathers.models import FeatherBasePage
from wagtail_feathers.models.seo import SeoContentType, TwitterCardType

class ArticlePage(FeatherBasePage):
    # SEO fields are automatically available via SeoMixin
    pass

# In your templates - add to <head> section:
# {% load seo_tags %}
# {% include "wagtail_feathers/seo/meta.html" %}

# In your templates - add before </body>:
# {% include "wagtail_feathers/seo/structured_data.html" %}

# Programmatic SEO control:
page = ArticlePage.objects.get(pk=1)
page.seo_title = "Custom SEO Title"  # Uses Wagtail's built-in field
page.search_description = "Custom meta description for search engines"  # Uses Wagtail's built-in field
page.seo_content_type = SeoContentType.ARTICLE
page.twitter_card_type = TwitterCardType.SUMMARY_LARGE_IMAGE
page.save()

Reading Time Features

All ItemPage models automatically include reading time calculation:

from wagtail_feathers.models import ItemPage

class BlogPost(ItemPage):
    # Reading time is automatically calculated from:
    # - title, introduction, body content
    # - Configurable words-per-minute (default: 200 WPM)
    pass

# In templates:
# {% load reading_time_tags %}
# {% reading_time page %}  # "5 min read"
# {% reading_time page "long" %}  # "5 minutes read"  
# {% reading_time_badge page %}  # Badge with clock icon

# Custom reading time logic:
class TechnicalArticle(ItemPage):
    summary = RichTextField(blank=True)
    
    def get_additional_word_sources(self):
        """Include summary in word count."""
        words = 0
        if self.summary:
            words += self._count_text_words(str(self.summary))
        return words
    
    # Slower reading speed for technical content
    words_per_minute = 150

# Reading time extends SEO automatically:
# - Twitter Cards: "Reading time: 5 min read"
# - Structured Data: "timeRequired": "PT5M"
# - Meta tags: article:reading_time

Extending SEO Features

The SEO functionality in wagtail_feathers is designed to be extensible. Here are several ways to enhance it:

1. Adding Custom SEO Fields

from wagtail_feathers.models import FeatherBasePage, SeoMixin
from wagtail.admin.panels import FieldPanel

class ArticlePage(FeatherBasePage):
    # Add your own SEO-related fields
    meta_keywords = models.CharField(
        max_length=255, 
        blank=True,
        help_text="Comma-separated keywords (optional for modern SEO)"
    )
    
    facebook_app_id = models.CharField(
        max_length=50,
        blank=True,
        help_text="Facebook App ID for Open Graph"
    )
    
    # Extend the SEO panels
    seo_panels = SeoMixin.seo_panels + [
        FieldPanel("meta_keywords"),
        FieldPanel("facebook_app_id"),
    ]
    
    # Override SEO methods for custom behavior
    def get_seo_description(self):
        """Custom SEO description logic."""
        # Try your custom field first
        if hasattr(self, "excerpt") and self.excerpt:
            return self.excerpt[:160]
        
        # Fall back to parent implementation
        return super().get_seo_description()

2. Custom Structured Data

import json
from wagtail_feathers.models import SeoMixin

class ProductPage(FeatherBasePage):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    brand = models.CharField(max_length=100)
    sku = models.CharField(max_length=50)
    
    def get_structured_data(self):
        """Override to add Product schema."""
        if self.seo_content_type == "product":
            site = self.get_site()
            base_url = site.root_url if site.root_url else f"https://{site.hostname}"
            
            schema = {
                "@context": "https://schema.org",
                "@type": "Product",
                "name": self.get_seo_title(),
                "description": self.get_seo_description(),
                "sku": self.sku,
                "brand": {
                    "@type": "Brand",
                    "name": self.brand
                },
                "offers": {
                    "@type": "Offer",
                    "price": str(self.price),
                    "priceCurrency": "USD",
                    "availability": "https://schema.org/InStock"
                }
            }
            
            # Add image if available
            if self.get_seo_image():
                schema["image"] = f"{base_url}{self.get_seo_image().file.url}"
                
            return json.dumps(schema, ensure_ascii=False)
        
        # Fall back to parent implementation
        return super().get_structured_data()

3. SEO-Focused Page Types

from wagtail_feathers.models import ItemPage
from wagtail_feathers.models.seo import SeoContentType, TwitterCardType

class SEOOptimizedArticle(ItemPage):
    """Article page optimized for SEO best practices."""
    
    # Set SEO defaults
    seo_content_type = SeoContentType.ARTICLE
    twitter_card_type = TwitterCardType.SUMMARY_LARGE_IMAGE
    
    # Additional SEO fields
    focus_keyword = models.CharField(
        max_length=100,
        blank=True,
        help_text="Primary keyword/phrase for this article"
    )
    
    reading_time = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text="Estimated reading time in minutes"
    )
    
    content_panels = ItemPage.content_panels + [
        FieldPanel("focus_keyword"),
        FieldPanel("reading_time"),
    ]
    
    def get_structured_data(self):
        """Enhanced article schema with reading time."""
        schema_str = super().get_structured_data()
        if schema_str and self.seo_content_type == SeoContentType.ARTICLE:
            schema = json.loads(schema_str)
            
            # Add reading time
            if self.reading_time:
                schema["timeRequired"] = f"PT{self.reading_time}M"
            
            # Add focus keyword as about
            if self.focus_keyword:
                schema["about"] = {
                    "@type": "Thing",
                    "name": self.focus_keyword
                }
                
            return json.dumps(schema, ensure_ascii=False)
        
        return schema_str

4. Custom SEO Template Tags

# In your app"s templatetags/custom_seo_tags.py
from django import template
from django.utils.safestring import mark_safe

register = template.Library()

@register.simple_tag
def custom_meta_tags(page):
    """Add custom meta tags based on page type."""
    tags = []
    
    # Add article-specific tags
    if hasattr(page, "focus_keyword") and page.focus_keyword:
        tags.append(f"<meta name='keywords' content='{page.focus_keyword}'>")
    
    # Add Facebook App ID if available
    if hasattr(page, "facebook_app_id") and page.facebook_app_id:
        tags.append(f"<meta property='fb:app_id' content='{page.facebook_app_id}'>")
    
    # Add reading time for articles
    if hasattr(page, "reading_time") and page.reading_time:
        tags.append(f"<meta name='twitter:label1' content="Reading time">")
        tags.append(f"<meta name='twitter:data1' content='{page.reading_time} min read'>")
    
    return mark_safe("\n".join(tags))

# In your templates:
# {% load custom_seo_tags %}
# {% custom_meta_tags page %}

5. SEO Analytics Integration

class AnalyticsEnhancedPage(FeatherBasePage):
    """Page with SEO analytics tracking."""
    
    google_analytics_id = models.CharField(
        max_length=20,
        blank=True,
        help_text="Google Analytics tracking ID (e.g., GA-XXXXX-X)"
    )
    
    track_scroll_depth = models.BooleanField(
        default=False,
        help_text="Enable scroll depth tracking for SEO insights"
    )
    
    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        
        # Add SEO tracking context
        context.update({
            "enable_seo_tracking": self.track_scroll_depth,
            "ga_tracking_id": self.google_analytics_id,
        })
        
        return context

6. Multi-language SEO

# If using wagtail-localize
from wagtail_feathers.models.seo import SeoMixin
from wagtail_localize.fields import TranslatableField

class MultilingualSEOPage(FeatherBasePage):
    """Page with enhanced multilingual SEO support."""
    
    # Additional translatable SEO fields
    local_keywords = models.CharField(
        max_length=255,
        blank=True,
        help_text="Keywords in local language"
    )
    
    cultural_context = models.TextField(
        blank=True,
        help_text="Cultural context for this market"
    )
    
    # Add to translatable fields
    if SeoMixin.WAGTAIL_LOCALIZE_AVAILABLE:
        translatable_fields = SeoMixin.translatable_fields + [
            TranslatableField("local_keywords"),
            TranslatableField("cultural_context"),
        ]
    
    def get_seo_description(self):
        """Localized SEO description."""
        if self.cultural_context:
            return f"{super().get_seo_description()} {self.cultural_context}"[:160]
        return super().get_seo_description()

7. Using Extension Points

The SeoMixin provides several extension points for easy customization:

class BlogArticlePage(ItemPage):
    """Blog article with enhanced SEO using extension points."""
    
    author_twitter = models.CharField(max_length=50, blank=True)
    focus_keyword = models.CharField(max_length=100, blank=True)
    # Note: reading_time is automatically provided by ItemPage
    
    def get_custom_meta_tags(self):
        """Add custom meta tags."""
        tags = {}
        
        if self.focus_keyword:
            tags["keywords"] = self.focus_keyword
        
        # Reading time is automatically added via ReadingTimeMixin
        # but you can customize it here if needed
            
        return tags
    
    def get_custom_twitter_tags(self):
        """Add custom Twitter Card tags."""
        tags = {}
        
        if self.author_twitter:
            tags["twitter:creator"] = f"@{self.author_twitter}"
        
        # Reading time Twitter tags are automatically added via ReadingTimeMixin
        # Additional Twitter tags can be added here
            
        return tags
    
    def get_custom_og_tags(self):
        """Add custom Open Graph tags."""
        tags = {}
        
        if self.author_twitter:
            tags["article:author"] = f"https://twitter.com/{self.author_twitter}"
            
        return tags
    
    def get_sitemap_priority(self):
        """Higher priority for recent articles and longer reads."""
        from django.utils import timezone
        from datetime import timedelta
        
        base_priority = super().get_sitemap_priority()
        
        # Boost recent articles
        if hasattr(self, "publication_date"):
            recent_threshold = timezone.now().date() - timedelta(days=30)
            if self.publication_date >= recent_threshold:
                base_priority += 0.1
        
        # Boost longer articles (more comprehensive content)
        if self.reading_time_minutes and self.reading_time_minutes >= 10:
            base_priority += 0.1
                
        return min(1.0, base_priority)  # Cap at 1.0

These examples show how to build upon wagtail_feathers SEO foundation to create powerful, customized SEO solutions for specific use cases.

People & Authors

from wagtail_feathers.models import Person, PersonGroup, AuthorType, PageAuthor

# Create person groups and people
group = PersonGroup.objects.create(name="Editorial Team")
person = Person.objects.create(
    first_name="John",
    last_name="Doe",
    email="john@example.com"
)
person.groups.add(group)

# Create author types and assign to pages
author_type = AuthorType.objects.create(name="Writer")
PageAuthor.objects.create(
    page=my_page,
    person=person,
    author_type=author_type
)

Requirements

  • Python 3.11+
  • Django 5.0+
  • Wagtail 5.2+

See pyproject.toml for complete dependency list.

Development

Setup Development Environment

# Clone the repository
git clone https://github.com/softquantum/wagtail-feathers.git
cd wagtail-feathers

# Navigate to demo directory
cd demo

# Install demo dependencies (includes editable package install)
pip install -r requirements.txt

# Set up database and demo data
python manage.py migrate
python manage.py setup_demo_data

# Start the demo server
python manage.py runserver
# Run tests
pytest

# Run tests across multiple environments
tox

Try the Demo Site

Demo Access:

The demo includes (WORK IN PROGRESS):

  • Sample pages using wagtail-feathers features
  • SEO optimization examples
  • Reading time calculation
  • Taxonomy and navigation examples
  • Live package development (changes reflect immediately)

Testing

The package includes some tests covering:

  • Model functionality and relationships
  • Template rendering and themes
  • Navigation generation
  • Taxonomy management
  • Multi-site behavior
# Run all tests
pytest

Code Quality

# Lint code
ruff check src/

# Format code  
ruff format src/

# Run via tox
tox -e lint
tox -e format

Architecture

Wagtail Feathers follows a modular architecture:

wagtail_feathers/
โ”œโ”€โ”€ models/           # Core model definitions
โ”‚   โ”œโ”€โ”€ base.py      # FeatherBasePage and foundation classes
โ”‚   โ”œโ”€โ”€ author.py    # AuthorType and PageAuthor models
โ”‚   โ”œโ”€โ”€ errors.py    # ErrorPage model
โ”‚   โ”œโ”€โ”€ faq.py       # FAQ and FAQCategory models  
โ”‚   โ”œโ”€โ”€ geographic.py # PageCountry model
โ”‚   โ”œโ”€โ”€ inline.py    # Related content models
โ”‚   โ”œโ”€โ”€ navigation.py # Menu and navigation models
โ”‚   โ”œโ”€โ”€ person.py    # Person and PersonGroup models
โ”‚   โ”œโ”€โ”€ settings.py  # SiteSettings model
โ”‚   โ”œโ”€โ”€ social.py    # Social media models
โ”‚   โ”œโ”€โ”€ specialized_pages.py # Specialized page types
โ”‚   โ”œโ”€โ”€ taxonomy.py  # Category and Classifier models
โ”‚   โ””โ”€โ”€ utils.py     # Model utilities
โ”œโ”€โ”€ blocks.py        # StreamField content blocks
โ”œโ”€โ”€ templatetags/    # Template tag libraries
โ”œโ”€โ”€ viewsets/        # Wagtail admin viewsets
โ”œโ”€โ”€ themes.py        # Theme system
โ””โ”€โ”€ management/      # Django management commands

Key Models

Based on the migration, wagtail_feathers provides these core models:

  • Page Foundation: FeatherBasePage, FeatherBasePageTag
  • Taxonomy: Category, Classifier, ClassifierGroup, PageCategory, PageClassifier
  • Navigation: Menu, MenuItem, FlatMenu, NestedMenu, Footer, FooterNavigation
  • People & Authors: Person, PersonGroup, AuthorType, PageAuthor
  • Content Organization: FAQ, FAQCategory, ErrorPage
  • Relationships: RelatedPage, RelatedDocument, RelatedExternalLink
  • Geography: PageCountry
  • Social: SocialMediaSettings, SocialMediaLink
  • Settings: SiteSettings

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

BSD-3-Clause

Credits & Attributions

  • โœ… Wagtail Feathers is Developed by Maxime Decooman.
  • โœ… Built on the excellent Wagtail CMS (License BSD-3-Clause)
  • โœ… The icons prefixed "heroicons-" are sourced from version 2.2.0 of Heroicons, the beautiful hand-crafted SVG icons library, by the makers of Tailwind CSS (License MIT).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wagtail_feathers-1.0b5.tar.gz (660.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wagtail_feathers-1.0b5-py3-none-any.whl (558.3 kB view details)

Uploaded Python 3

File details

Details for the file wagtail_feathers-1.0b5.tar.gz.

File metadata

  • Download URL: wagtail_feathers-1.0b5.tar.gz
  • Upload date:
  • Size: 660.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.32.3

File hashes

Hashes for wagtail_feathers-1.0b5.tar.gz
Algorithm Hash digest
SHA256 ee0b44ccbd372a5aea846b2c313374b79003fa4a96025d22b276f6aa6c06fe02
MD5 653ff58ad9a220d005b2adf4686b9200
BLAKE2b-256 bbbdfa8b902b128d6f01cf12f91671806621f08792db1cff0284b6f12844c987

See more details on using hashes here.

File details

Details for the file wagtail_feathers-1.0b5-py3-none-any.whl.

File metadata

File hashes

Hashes for wagtail_feathers-1.0b5-py3-none-any.whl
Algorithm Hash digest
SHA256 5944218d66fc3ed0debdaa4fe010c678269722d91863aa2e8268853163ba5316
MD5 48a42fe03a83ea4a595e4b79cd9c0ad1
BLAKE2b-256 1c3d0ac1838f3e1b7ab12d9fc34537bc4e819f1fa0365080bfa8e3b948cfe633

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page