Browse Source
SEO : seo tools and normalizations added for code base and Article, Hadis (optimized to prefetch transmitters, references, and book authors), Library (Books), Podcast, and Video apps.
master
SEO : seo tools and normalizations added for code base and Article, Hadis (optimized to prefetch transmitters, references, and book authors), Library (Books), Podcast, and Video apps.
master
22 changed files with 588 additions and 35 deletions
-
8apps/article/models.py
-
100apps/article/sitemaps.py
-
8apps/article/views.py
-
8apps/hadis/models/hadis.py
-
10apps/hadis/models/reference.py
-
69apps/hadis/sitemaps.py
-
8apps/hadis/views/hadis.py
-
6apps/library/models.py
-
62apps/library/sitemaps.py
-
10apps/library/views.py
-
11apps/podcast/models.py
-
62apps/podcast/sitemaps.py
-
8apps/podcast/views.py
-
6apps/video/models.py
-
62apps/video/sitemaps.py
-
8apps/video/views.py
-
32config/middleware/site_middleware.py
-
4config/settings/base.py
-
17config/urls.py
-
16templates/sitemap.xml
-
6utils/exceptions.py
-
96utils/mixins.py
@ -0,0 +1,100 @@ |
|||
import datetime |
|||
from django.contrib.sitemaps import Sitemap |
|||
from django.conf import settings |
|||
from django.utils import timezone |
|||
from apps.article.models import Article |
|||
|
|||
|
|||
class BaseCanonicalSitemap(Sitemap): |
|||
""" |
|||
Base Sitemap class that supports absolute frontend URLs. |
|||
Strips out the django-prepended backend scheme and domain from the final URLs |
|||
if the location returned is already an absolute URL. |
|||
Also ensures all `lastmod` datetimes are rendered in strict W3C / ISO 8601 format with timezone offsets. |
|||
""" |
|||
limit = 10000 |
|||
|
|||
def _urls(self, page, protocol, domain): |
|||
urls = super()._urls(page, protocol, domain) |
|||
for url_info in urls: |
|||
# Fix absolute locations |
|||
url_info["location"] = self._fix_absolute_url(url_info["location"]) |
|||
if "alternates" in url_info: |
|||
for alt in url_info["alternates"]: |
|||
alt["location"] = self._fix_absolute_url(alt["location"]) |
|||
|
|||
# Format naive/aware lastmod datetime objects to strict ISO 8601 / W3C format |
|||
lastmod_dt = url_info.get("lastmod") |
|||
if isinstance(lastmod_dt, (datetime.datetime, datetime.date)): |
|||
if isinstance(lastmod_dt, datetime.datetime): |
|||
if timezone.is_naive(lastmod_dt): |
|||
lastmod_dt = timezone.make_aware(lastmod_dt) |
|||
url_info["lastmod"] = lastmod_dt.isoformat() |
|||
return urls |
|||
|
|||
def _fix_absolute_url(self, url): |
|||
# Look for a secondary protocol pattern (e.g., http://testserverhttps://domain.com) |
|||
for scheme in ("https://", "http://"): |
|||
idx = url.find(scheme, 7) # search after the first scheme prefix |
|||
if idx != -1: |
|||
return url[idx:] |
|||
return url |
|||
|
|||
|
|||
class ArticleSitemap(BaseCanonicalSitemap): |
|||
""" |
|||
Sitemap class for Article model. |
|||
Overridden to dynamically build alternate (hreflang) URLs for all supported languages, |
|||
format lastmod to strict W3C / ISO 8601 with timezone offsets, and output directly to XML. |
|||
""" |
|||
changefreq = "weekly" |
|||
priority = 0.6 |
|||
|
|||
def items(self): |
|||
# Only include active/published articles, sorted by updated_at |
|||
return Article.objects.filter(status=True).order_by("-updated_at") |
|||
|
|||
def lastmod(self, obj): |
|||
# Use the article's update timestamp for last modification info |
|||
return obj.updated_at |
|||
|
|||
def location(self, obj): |
|||
# Use the frontend URL via the share_link property |
|||
return obj.share_link |
|||
|
|||
def get_urls(self, page=1, site=None, protocol=None): |
|||
urls = [] |
|||
paginator_page = self.paginator.page(page) |
|||
|
|||
for item in paginator_page.object_list: |
|||
alternates = [] |
|||
|
|||
# Dynamically build alternate URLs for all supported languages |
|||
for lang_code, lang_name in settings.LANGUAGES: |
|||
localized_url = f"{settings.DOVODI_DOMAIN}/{lang_code}/articles/{item.slug}" |
|||
alternates.append({ |
|||
"location": localized_url, |
|||
"lang_code": lang_code, |
|||
}) |
|||
|
|||
# Safely format naive/aware lastmod datetime to strict ISO 8601 / W3C format |
|||
lastmod_dt = item.updated_at |
|||
if lastmod_dt: |
|||
if timezone.is_naive(lastmod_dt): |
|||
lastmod_dt = timezone.make_aware(lastmod_dt) |
|||
lastmod_str = lastmod_dt.isoformat() |
|||
else: |
|||
lastmod_str = None |
|||
|
|||
# Construct URL metadata dictionary |
|||
url_info = { |
|||
"item": item, |
|||
"location": item.share_link, |
|||
"lastmod": lastmod_str, |
|||
"changefreq": self.changefreq, |
|||
"priority": str(self.priority), |
|||
"alternates": alternates, |
|||
} |
|||
urls.append(url_info) |
|||
|
|||
return urls |
|||
@ -0,0 +1,69 @@ |
|||
import datetime |
|||
from django.conf import settings |
|||
from django.utils import timezone |
|||
from apps.article.sitemaps import BaseCanonicalSitemap |
|||
from apps.hadis.models import Hadis |
|||
|
|||
|
|||
class HadisSitemap(BaseCanonicalSitemap): |
|||
""" |
|||
Sitemap class for Hadis model. |
|||
Dynamically builds alternate (hreflang) URLs for all supported languages, |
|||
formats lastmod to strict W3C / ISO 8601 with timezone offsets, and outputs directly to XML. |
|||
""" |
|||
changefreq = "weekly" |
|||
priority = 0.6 |
|||
|
|||
def items(self): |
|||
# Only include active/published hadiths |
|||
return Hadis.objects.filter(status=True).select_related( |
|||
'category', |
|||
'hadis_status' |
|||
).prefetch_related( |
|||
'transmitters__transmitter', |
|||
'transmitters__narrator_layer', |
|||
'references__book_reference__author' |
|||
).order_by("-updated_at") |
|||
|
|||
def lastmod(self, obj): |
|||
return obj.updated_at |
|||
|
|||
def location(self, obj): |
|||
return obj.share_link |
|||
|
|||
def get_urls(self, page=1, site=None, protocol=None): |
|||
urls = [] |
|||
paginator_page = self.paginator.page(page) |
|||
|
|||
for item in paginator_page.object_list: |
|||
alternates = [] |
|||
|
|||
# Dynamically build alternate URLs for all supported languages |
|||
for lang_code, lang_name in settings.LANGUAGES: |
|||
localized_url = f"{settings.DOVODI_DOMAIN}/{lang_code}/arguments/hadith/{item.slug}" |
|||
alternates.append({ |
|||
"location": localized_url, |
|||
"lang_code": lang_code, |
|||
}) |
|||
|
|||
# Safely format naive/aware lastmod datetime to strict ISO 8601 / W3C format |
|||
lastmod_dt = item.updated_at |
|||
if lastmod_dt: |
|||
if timezone.is_naive(lastmod_dt): |
|||
lastmod_dt = timezone.make_aware(lastmod_dt) |
|||
lastmod_str = lastmod_dt.isoformat() |
|||
else: |
|||
lastmod_str = None |
|||
|
|||
# Construct URL metadata dictionary |
|||
url_info = { |
|||
"item": item, |
|||
"location": item.share_link, |
|||
"lastmod": lastmod_str, |
|||
"changefreq": self.changefreq, |
|||
"priority": str(self.priority), |
|||
"alternates": alternates, |
|||
} |
|||
urls.append(url_info) |
|||
|
|||
return urls |
|||
@ -0,0 +1,62 @@ |
|||
import datetime |
|||
from django.conf import settings |
|||
from django.utils import timezone |
|||
from apps.article.sitemaps import BaseCanonicalSitemap |
|||
from apps.library.models import Book |
|||
|
|||
|
|||
class BookSitemap(BaseCanonicalSitemap): |
|||
""" |
|||
Sitemap class for Book model. |
|||
Dynamically builds alternate (hreflang) URLs for all supported languages, |
|||
formats lastmod to strict W3C / ISO 8601 with timezone offsets, and outputs directly to XML. |
|||
""" |
|||
changefreq = "weekly" |
|||
priority = 0.6 |
|||
|
|||
def items(self): |
|||
# Only include active/published books |
|||
return Book.objects.filter(status=True).order_by("-updated_at") |
|||
|
|||
def lastmod(self, obj): |
|||
return obj.updated_at |
|||
|
|||
def location(self, obj): |
|||
return obj.share_link |
|||
|
|||
def get_urls(self, page=1, site=None, protocol=None): |
|||
urls = [] |
|||
paginator_page = self.paginator.page(page) |
|||
|
|||
for item in paginator_page.object_list: |
|||
alternates = [] |
|||
|
|||
# Dynamically build alternate URLs for all supported languages |
|||
for lang_code, lang_name in settings.LANGUAGES: |
|||
localized_url = f"{settings.DOVODI_DOMAIN}/{lang_code}/library/{item.slug}" |
|||
alternates.append({ |
|||
"location": localized_url, |
|||
"lang_code": lang_code, |
|||
}) |
|||
|
|||
# Safely format naive/aware lastmod datetime to strict ISO 8601 / W3C format |
|||
lastmod_dt = item.updated_at |
|||
if lastmod_dt: |
|||
if timezone.is_naive(lastmod_dt): |
|||
lastmod_dt = timezone.make_aware(lastmod_dt) |
|||
lastmod_str = lastmod_dt.isoformat() |
|||
else: |
|||
lastmod_str = None |
|||
|
|||
# Construct URL metadata dictionary |
|||
url_info = { |
|||
"item": item, |
|||
"location": item.share_link, |
|||
"lastmod": lastmod_str, |
|||
"changefreq": self.changefreq, |
|||
"priority": str(self.priority), |
|||
"alternates": alternates, |
|||
} |
|||
urls.append(url_info) |
|||
|
|||
return urls |
|||
@ -0,0 +1,62 @@ |
|||
import datetime |
|||
from django.conf import settings |
|||
from django.utils import timezone |
|||
from apps.article.sitemaps import BaseCanonicalSitemap |
|||
from apps.podcast.models import Podcast |
|||
|
|||
|
|||
class PodcastSitemap(BaseCanonicalSitemap): |
|||
""" |
|||
Sitemap class for Podcast model. |
|||
Dynamically builds alternate (hreflang) URLs for all supported languages, |
|||
formats lastmod to strict W3C / ISO 8601 with timezone offsets, and outputs directly to XML. |
|||
""" |
|||
changefreq = "weekly" |
|||
priority = 0.6 |
|||
|
|||
def items(self): |
|||
# Only include active/published podcasts |
|||
return Podcast.objects.filter(status=True).order_by("-updated_at") |
|||
|
|||
def lastmod(self, obj): |
|||
return obj.updated_at |
|||
|
|||
def location(self, obj): |
|||
return obj.share_link |
|||
|
|||
def get_urls(self, page=1, site=None, protocol=None): |
|||
urls = [] |
|||
paginator_page = self.paginator.page(page) |
|||
|
|||
for item in paginator_page.object_list: |
|||
alternates = [] |
|||
|
|||
# Dynamically build alternate URLs for all supported languages |
|||
for lang_code, lang_name in settings.LANGUAGES: |
|||
localized_url = f"{settings.DOVODI_DOMAIN}/{lang_code}/podcast/{item.slug}" |
|||
alternates.append({ |
|||
"location": localized_url, |
|||
"lang_code": lang_code, |
|||
}) |
|||
|
|||
# Safely format naive/aware lastmod datetime to strict ISO 8601 / W3C format |
|||
lastmod_dt = item.updated_at |
|||
if lastmod_dt: |
|||
if timezone.is_naive(lastmod_dt): |
|||
lastmod_dt = timezone.make_aware(lastmod_dt) |
|||
lastmod_str = lastmod_dt.isoformat() |
|||
else: |
|||
lastmod_str = None |
|||
|
|||
# Construct URL metadata dictionary |
|||
url_info = { |
|||
"item": item, |
|||
"location": item.share_link, |
|||
"lastmod": lastmod_str, |
|||
"changefreq": self.changefreq, |
|||
"priority": str(self.priority), |
|||
"alternates": alternates, |
|||
} |
|||
urls.append(url_info) |
|||
|
|||
return urls |
|||
@ -0,0 +1,62 @@ |
|||
import datetime |
|||
from django.conf import settings |
|||
from django.utils import timezone |
|||
from apps.article.sitemaps import BaseCanonicalSitemap |
|||
from apps.video.models import Video |
|||
|
|||
|
|||
class VideoSitemap(BaseCanonicalSitemap): |
|||
""" |
|||
Sitemap class for Video model. |
|||
Dynamically builds alternate (hreflang) URLs for all supported languages, |
|||
formats lastmod to strict W3C / ISO 8601 with timezone offsets, and outputs directly to XML. |
|||
""" |
|||
changefreq = "weekly" |
|||
priority = 0.6 |
|||
|
|||
def items(self): |
|||
# Only include active/published videos |
|||
return Video.objects.filter(status=True).order_by("-updated_at") |
|||
|
|||
def lastmod(self, obj): |
|||
return obj.updated_at |
|||
|
|||
def location(self, obj): |
|||
return obj.share_link |
|||
|
|||
def get_urls(self, page=1, site=None, protocol=None): |
|||
urls = [] |
|||
paginator_page = self.paginator.page(page) |
|||
|
|||
for item in paginator_page.object_list: |
|||
alternates = [] |
|||
|
|||
# Dynamically build alternate URLs for all supported languages |
|||
for lang_code, lang_name in settings.LANGUAGES: |
|||
localized_url = f"{settings.DOVODI_DOMAIN}/{lang_code}/videos/{item.slug}" |
|||
alternates.append({ |
|||
"location": localized_url, |
|||
"lang_code": lang_code, |
|||
}) |
|||
|
|||
# Safely format naive/aware lastmod datetime to strict ISO 8601 / W3C format |
|||
lastmod_dt = item.updated_at |
|||
if lastmod_dt: |
|||
if timezone.is_naive(lastmod_dt): |
|||
lastmod_dt = timezone.make_aware(lastmod_dt) |
|||
lastmod_str = lastmod_dt.isoformat() |
|||
else: |
|||
lastmod_str = None |
|||
|
|||
# Construct URL metadata dictionary |
|||
url_info = { |
|||
"item": item, |
|||
"location": item.share_link, |
|||
"lastmod": lastmod_str, |
|||
"changefreq": self.changefreq, |
|||
"priority": str(self.priority), |
|||
"alternates": alternates, |
|||
} |
|||
urls.append(url_info) |
|||
|
|||
return urls |
|||
@ -0,0 +1,16 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> |
|||
{% spaceless %} |
|||
{% for url in urlset %} |
|||
<url> |
|||
<loc>{{ url.location }}</loc> |
|||
{% if url.lastmod %}<lastmod>{{ url.lastmod }}</lastmod>{% endif %} |
|||
{% if url.changefreq %}<changefreq>{{ url.changefreq }}</changefreq>{% endif %} |
|||
{% if url.priority %}<priority>{{ url.priority }}</priority>{% endif %} |
|||
{% for alternate in url.alternates %} |
|||
<xhtml:link rel="alternate" hreflang="{{ alternate.lang_code }}" href="{{ alternate.location }}"/> |
|||
{% endfor %} |
|||
</url> |
|||
{% endfor %} |
|||
{% endspaceless %} |
|||
</urlset> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue