Features: 1) Add dynamic static page generation in StaticPagesSitemap by integrating active blog posts marked as static pages; 2) Introduce SitemapLanguageMixin to handle language-based URL generation across sitemaps; 3) Add is_static_page field to Post model for designating posts as static pages;

Fixes: 1) Correct router naming in `blog/urls.py` from `payment_router` to `blog_router` for clarity;

Extra: 1) Refactor obsolete `StaticPagesSitemap.PAGES` structure with a dynamic `items` method; 2) Create placeholder 404 URLs for non-existent slugs; 3) Update and simplify docstring for `Post` class, replacing inline details with a concise translation-aware docstring.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-10-20 22:56:11 +03:00
parent 2712ccdeb7
commit 06290c0278
3 changed files with 59 additions and 92 deletions

View file

@ -1,4 +1,4 @@
from django.db.models import CASCADE, CharField, FileField, ForeignKey, ManyToManyField
from django.db.models import CASCADE, CharField, FileField, ForeignKey, ManyToManyField, BooleanField
from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import AutoSlugField
from markdown.extensions.toc import TocExtension
@ -8,26 +8,13 @@ from core.abstract import NiceModel
class Post(NiceModel): # type: ignore [django-manager-missing]
"""
Represents a blog post model extending NiceModel.
The Post class defines the structure and behavior of a blog post. It includes
attributes for author, title, content, optional file attachment, slug,
and associated tags. The class enforces constraints such as requiring either
content or a file attachment but not both simultaneously. It also supports
automatic slug generation based on the title. This model can be used in
a blogging platform to manage posts created by users.
Attributes:
is_publicly_visible (bool): Specifies whether the post is visible to the public.
author (ForeignKey): A reference to the user who authored the post.
title (CharField): The title of the post. Must be unique and non-empty.
content (MarkdownField): The content of the post written in Markdown format.
file (FileField): An optional file attachment for the post.
slug (AutoSlugField): A unique, automatically generated slug based on the title.
tags (ManyToManyField): Tags associated with the post for categorization.
"""
__doc__ = _(
"Represents a blog post model. "
"The Post class defines the structure and behavior of a blog post. "
"It includes attributes for author, title, content, optional file attachment, slug, and associated tags. "
"The class enforces constraints such as requiring either content or a file attachment but not both simultaneously. "
"It also supports automatic slug generation based on the title."
)
is_publicly_visible = True
@ -76,6 +63,11 @@ class Post(NiceModel): # type: ignore [django-manager-missing]
slug = AutoSlugField(populate_from="title", allow_unicode=True, unique=True, editable=False)
tags = ManyToManyField(to="blog.PostTag", blank=True, related_name="posts")
meta_description = CharField(max_length=150, blank=True, null=True)
is_static_page = BooleanField(
default=False,
verbose_name=_("is static page"),
help_text=_("is this a post for a page with static URL (e.g. `/help/delivery`)?"),
)
def __str__(self):
return f"{self.title} | {self.author.first_name} {self.author.last_name}"

View file

@ -5,9 +5,9 @@ from blog.viewsets import PostViewSet
app_name = "blog"
payment_router = DefaultRouter()
payment_router.register(prefix=r"posts", viewset=PostViewSet, basename="posts")
blog_router = DefaultRouter()
blog_router.register(prefix=r"posts", viewset=PostViewSet, basename="posts")
urlpatterns = [
path(r"", include(payment_router.urls)),
path(r"", include(blog_router.urls)),
]

View file

@ -2,51 +2,52 @@ from django.conf import settings
from django.contrib.sitemaps import Sitemap
from django.utils.translation import gettext_lazy as _
from blog.models import Post
from core.models import Brand, Category, Product
from core.utils.seo_builders import any_non_digital
from evibes.settings import LANGUAGE_CODE
class StaticPagesSitemap(Sitemap): # type: ignore [type-arg]
class SitemapLanguageMixin:
def _lang(self) -> str:
req = getattr(self, "request", None)
return getattr(req, "LANGUAGE_CODE", settings.LANGUAGE_CODE)
class StaticPagesSitemap(SitemapLanguageMixin, Sitemap): # type: ignore [type-arg]
protocol = "https"
changefreq = "monthly"
priority = 0.8
limit = 1000
PAGES = [
{
"name": _("Home"),
"path": f"/{LANGUAGE_CODE}",
"lastmod": settings.RELEASE_DATE,
},
{
"name": _("Contact Us"),
"path": f"/{LANGUAGE_CODE}/contact-us",
"lastmod": settings.RELEASE_DATE,
},
{
"name": _("About Us"),
"path": f"/{LANGUAGE_CODE}/about-us",
"lastmod": settings.RELEASE_DATE,
},
{
"name": _("Payment Information"),
"path": f"/{LANGUAGE_CODE}/help/payments",
"lastmod": settings.RELEASE_DATE,
},
]
if any_non_digital():
PAGES.append(
{
"name": _("Delivery"),
"path": f"/{LANGUAGE_CODE}/help/delivery",
"lastmod": settings.RELEASE_DATE,
}
)
def items(self):
return self.PAGES
lang = self._lang()
pages = [
{
"name": _("Home"),
"path": f"/{lang}",
"lastmod": settings.RELEASE_DATE,
},
{
"name": _("Contact Us"),
"path": f"/{lang}/contact-us",
"lastmod": settings.RELEASE_DATE,
},
{
"name": _("About Us"),
"path": f"/{lang}/about-us",
"lastmod": settings.RELEASE_DATE,
},
]
for static_post_page in Post.objects.filter(is_static_page=True, is_active=True).only("title", "slug", "modified"):
pages.append(
{
"name": static_post_page.title,
"path": f"/{lang}/information/{static_post_page.slug}",
"lastmod": static_post_page.modified,
}
)
return pages
def location(self, obj):
return obj["path"]
@ -55,33 +56,7 @@ class StaticPagesSitemap(Sitemap): # type: ignore [type-arg]
return obj.get("lastmod")
# class FeaturedProductsSitemap(Sitemap): # type: ignore [type-arg]
# protocol = "https"
# changefreq = "daily"
# priority = 0.9
# limit = 25000
#
# def items(self):
# return (
# Product.objects.filter(
# is_active=True,
# brand__is_active=True,
# category__is_active=True,
# stocks__isnull=False,
# stocks__vendor__is_active=True,
# )
# .only("uuid", "name", "modified", "slug")
# .order_by("-modified")
# )
#
# def lastmod(self, obj):
# return obj.modified
#
# def location(self, obj):
# return f"/{LANGUAGE_CODE}/product/{obj.slug}"
class ProductSitemap(Sitemap): # type: ignore [type-arg]
class ProductSitemap(SitemapLanguageMixin, Sitemap): # type: ignore [type-arg]
protocol = "https"
changefreq = "daily"
priority = 0.9
@ -104,10 +79,10 @@ class ProductSitemap(Sitemap): # type: ignore [type-arg]
return obj.modified
def location(self, obj):
return f"/{LANGUAGE_CODE}/product/{obj.slug}"
return f"/{self._lang()}/product/{obj.slug if obj.slug else '404-non-existent-product'}"
class CategorySitemap(Sitemap): # type: ignore [type-arg]
class CategorySitemap(SitemapLanguageMixin, Sitemap): # type: ignore [type-arg]
protocol = "https"
changefreq = "weekly"
priority = 0.7
@ -120,10 +95,10 @@ class CategorySitemap(Sitemap): # type: ignore [type-arg]
return obj.modified
def location(self, obj):
return f"/{LANGUAGE_CODE}/catalog/{obj.slug}"
return f"/{self._lang()}/catalog/{obj.slug if obj.slug else '404-non-existent-category'}"
class BrandSitemap(Sitemap): # type: ignore [type-arg]
class BrandSitemap(SitemapLanguageMixin, Sitemap): # type: ignore [type-arg]
protocol = "https"
changefreq = "weekly"
priority = 0.6
@ -136,4 +111,4 @@ class BrandSitemap(Sitemap): # type: ignore [type-arg]
return obj.modified
def location(self, obj):
return f"/{LANGUAGE_CODE}/brand/{obj.slug}"
return f"/{self._lang()}/brand/{obj.slug if obj.slug else '404-non-existent-brand'}"