add: все для sitemaps.xml + robots.txt

This commit is contained in:
2026-09-06 23:42:00 +03:00
parent a969e12233
commit 1a61f36fc1
5 changed files with 178 additions and 0 deletions
+5
View File
@@ -15,11 +15,16 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.contrib.sitemaps.views import sitemap
from django.urls import include, path from django.urls import include, path
from django.conf.urls.static import static from django.conf.urls.static import static
from hypn0_site.sitemaps import sitemaps
from . import settings from . import settings
urlpatterns = [ urlpatterns = [
# Карта сайта sitemap.xml для поисковой индексации
path("sitemap.xml", sitemap, {"sitemaps": sitemaps}, name="django.contrib.sitemaps.views.sitemap"),
# Админ-сайт с переименованными приложениями (переопределен в frontend/apps.py) # Админ-сайт с переименованными приложениями (переопределен в frontend/apps.py)
path(settings.ADMIN_URL, admin.site.urls), path(settings.ADMIN_URL, admin.site.urls),
path('', include('hypn0_site.urls')), path('', include('hypn0_site.urls')),
+5
View File
@@ -1,6 +1,7 @@
import hashlib import hashlib
from django.db import models, transaction, IntegrityError from django.db import models, transaction, IntegrityError
from django.db.models import F from django.db.models import F
from django.urls import reverse
from hashids import Hashids from hashids import Hashids
from hypn0.settings import * from hypn0.settings import *
from django.utils import timezone from django.utils import timezone
@@ -157,6 +158,10 @@ class TbHypn0Item(models.Model):
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.s_title} ({self.s_hash_id})" return f"{self.s_title} ({self.s_hash_id})"
def get_absolute_url(self) -> str:
"""Возвращает канонический URL детальной страницы картины."""
return reverse("hypn0_site:gallery_detail", kwargs={"hash_id": self.s_hash_id})
@property @property
def card_bg_style(self) -> str: def card_bg_style(self) -> str:
""" """
+66
View File
@@ -0,0 +1,66 @@
"""
Карты сайта (Sitemaps) для поисковой индексации HypnoSVG.
"""
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from .models import TbHypn0Item
class StaticViewSitemap(Sitemap):
"""
Карта сайта для ключевых страниц и разделов интерфейса.
"""
changefreq = "daily"
def items(self):
return [
("hypn0_site:index", 1.0, "daily"),
("hypn0_site:gallery_archive", 0.9, "daily"),
]
def location(self, item):
return reverse(item[0])
def priority(self, item):
return item[1]
def changefreq(self, item):
return item[2]
class GalleryItemSitemap(Sitemap):
"""
Карта сайта для публичных гипно-картин галереи сообщества.
"""
changefreq = "weekly"
def items(self):
# Включаем только публичные картины, исключая подозрительные/заблокированные
return (
TbHypn0Item.objects.filter(is_public=True)
.exclude(i_level__in=[TbHypn0Item.Level.SHAMED, TbHypn0Item.Level.SUSPICIOUS])
.order_by("-d_created_at")
)
def lastmod(self, obj: TbHypn0Item):
return obj.d_created_at
def priority(self, obj: TbHypn0Item):
# Динамический приоритет в зависимости от уровня признания в психо-матрице
if obj.i_level == TbHypn0Item.Level.IMMORTAL:
return 0.9
elif obj.i_level == TbHypn0Item.Level.LEVEL_2:
return 0.8
elif obj.i_level == TbHypn0Item.Level.LEVEL_1:
return 0.7
return 0.6
def location(self, obj: TbHypn0Item):
return obj.get_absolute_url()
sitemaps = {
"static": StaticViewSitemap,
"gallery": GalleryItemSitemap,
}
+89
View File
@@ -1058,3 +1058,92 @@ class RescoreCommandTests(BaseMediaTestCase):
self.assertGreater(self.item_immortal.f_score, self.item_candidate.f_score) self.assertGreater(self.item_immortal.f_score, self.item_candidate.f_score)
# Статус бессмертной картины защищен и не должен измениться # Статус бессмертной картины защищен и не должен измениться
self.assertEqual(self.item_immortal.i_level, TbHypn0Item.Level.IMMORTAL) self.assertEqual(self.item_immortal.i_level, TbHypn0Item.Level.IMMORTAL)
class SitemapTests(BaseMediaTestCase):
"""Тестирование автоматической генерации sitemap.xml."""
def setUp(self):
super().setUp()
self.vid = "123e4567-e89b-12d3-a456-426614174000"
svg_bytes = b'<svg><circle/></svg>'
# Создаем публичные картины разных уровней
self.item_immortal = TbHypn0Item(
s_title="Шедевр транса",
file_svg=ContentFile(svg_bytes, name="immortal.svg"),
i_level=TbHypn0Item.Level.IMMORTAL,
is_public=True,
)
self.item_immortal.save(visitor_uuid_or_fp=self.vid)
self.item_curated = TbHypn0Item(
s_title="Одобренная картина",
file_svg=ContentFile(svg_bytes, name="curated.svg"),
i_level=TbHypn0Item.Level.LEVEL_2,
is_public=True,
)
self.item_curated.save(visitor_uuid_or_fp=self.vid)
self.item_fresh = TbHypn0Item(
s_title="Свежий шум",
file_svg=ContentFile(svg_bytes, name="fresh.svg"),
i_level=TbHypn0Item.Level.CANDIDATE,
is_public=True,
)
self.item_fresh.save(visitor_uuid_or_fp=self.vid)
# Создаем непубличную картину и заблокированную (шейминг)
self.item_private = TbHypn0Item(
s_title="Приватная картина",
file_svg=ContentFile(svg_bytes, name="private.svg"),
i_level=TbHypn0Item.Level.CANDIDATE,
is_public=False,
)
self.item_private.save(visitor_uuid_or_fp=self.vid)
self.item_shamed = TbHypn0Item(
s_title="Заблокированная картина",
file_svg=ContentFile(svg_bytes, name="shamed.svg"),
i_level=TbHypn0Item.Level.SHAMED,
is_public=True,
)
self.item_shamed.save(visitor_uuid_or_fp=self.vid)
def test_item_get_absolute_url(self):
expected_url = f"/gallery/{self.item_immortal.s_hash_id}"
self.assertEqual(self.item_immortal.get_absolute_url(), expected_url)
def test_sitemap_xml_renders_valid_xml_with_static_and_gallery_pages(self):
response = self.client.get("/sitemap.xml")
self.assertEqual(response.status_code, 200)
self.assertIn("xml", response["Content-Type"])
content = response.content.decode("utf-8")
self.assertIn("<urlset", content)
# Проверяем наличие главных статических страниц
self.assertIn("<loc>http://testserver/</loc>", content)
self.assertIn("<loc>http://testserver/gallery</loc>", content)
# Проверяем наличие публичных картин
self.assertIn(f"<loc>http://testserver/gallery/{self.item_immortal.s_hash_id}</loc>", content)
self.assertIn(f"<loc>http://testserver/gallery/{self.item_curated.s_hash_id}</loc>", content)
self.assertIn(f"<loc>http://testserver/gallery/{self.item_fresh.s_hash_id}</loc>", content)
# Проверяем отсутствие приватных и заблокированных картин
self.assertNotIn(f"<loc>http://testserver/gallery/{self.item_private.s_hash_id}</loc>", content)
self.assertNotIn(f"<loc>http://testserver/gallery/{self.item_shamed.s_hash_id}</loc>", content)
def test_sitemap_priorities_and_changefreq(self):
response = self.client.get("/sitemap.xml")
self.assertEqual(response.status_code, 200)
content = response.content.decode("utf-8")
# Приоритет главной страницы 1.0, архива 0.9, бессмертной 0.9, кураторской 0.8
self.assertIn("<priority>1.0</priority>", content)
self.assertIn("<priority>0.9</priority>", content)
self.assertIn("<priority>0.8</priority>", content)
self.assertIn("<priority>0.6</priority>", content)
self.assertIn("<changefreq>daily</changefreq>", content)
self.assertIn("<changefreq>weekly</changefreq>", content)
+13
View File
@@ -0,0 +1,13 @@
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /generate
Disallow: /publish
Disallow: /gallery/*/vote
Disallow: /gallery/*/download
Disallow: /gallery/random
Disallow: /*?*
Clean-param: seed&floor&sort&page /
Sitemap: https://hypn0.xyz/sitemap.xml