From eded036ee2cf4c9bce974b9f58ea9f8e2a1a746b Mon Sep 17 00:00:00 2001 From: pilot <657434@03b.ru> Date: Sun, 30 Aug 2026 11:08:19 +0800 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20Open=20Graph=20meta-=D1=82=D0=B5=D0=B3=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BF=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B4=D0=B8=D0=BD=D0=B3=D0=B0=20=D0=B2=20=D0=BC?= =?UTF-8?q?=D0=B5=D1=81=D1=81=D0=B5=D0=BD=D0=B4=D0=B6=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены og:title, og:description, og:image, og:url и Twitter Card в landing.html. - Реализованы normalize_logo_url и build_landing_meta для абсолютных HTTPS-ссылок на логотип. - landing_page передаёт meta-данные и единый logo_url в шаблон. - Добавлены тесты нормализации логотипа и проверки OG-тегов. --- restoran_max_bot/content_bot/landing_utils.py | 53 +++++++++++++++++++ restoran_max_bot/content_bot/tests.py | 37 ++++++++++++- restoran_max_bot/content_bot/views.py | 19 +++++-- .../templates/max_bot/landing.html | 27 +++++++--- 4 files changed, 124 insertions(+), 12 deletions(-) diff --git a/restoran_max_bot/content_bot/landing_utils.py b/restoran_max_bot/content_bot/landing_utils.py index 1a5e9cb..55cfc37 100644 --- a/restoran_max_bot/content_bot/landing_utils.py +++ b/restoran_max_bot/content_bot/landing_utils.py @@ -1,8 +1,13 @@ import json from typing import Callable, Optional, Tuple +from django.conf import settings + from max_bot.models import Client, Integration +CDN_BASE_URL = 'https://cdn.telefon-ip.ru' +DEFAULT_OG_DESCRIPTION = 'Выберите удобный мессанджер.' + def normalize_telegram_url(url: Optional[str]) -> Optional[str]: """Приводит ссылку на Telegram-бота к виду https://t.me/bot_name.""" @@ -104,6 +109,54 @@ def cleanup_max_bot_integration(client: Client) -> None: _save_settings(integration, settings) +def normalize_logo_url(logo: Optional[str]) -> Optional[str]: + """Приводит путь логотипа к абсолютному HTTPS URL для og:image и img src.""" + if not logo or not str(logo).strip(): + return None + + value = str(logo).strip() + if value.startswith('https://'): + return value + if value.startswith('http://'): + return f'https://{value[7:]}' + + return f'{CDN_BASE_URL}/{value.lstrip("/")}' + + +def build_landing_meta( + brand: str, + description: str, + logo: str, + client_id: int, + page_url: str, +) -> dict: + """Формирует meta/OG-данные для превью ссылки в мессенджерах.""" + title = brand or 'Ресторан' + meta_description = (description or DEFAULT_OG_DESCRIPTION).strip() + if len(meta_description) > 200: + meta_description = f'{meta_description[:197]}...' + + logo_url = normalize_logo_url(logo) + + return { + 'page_title': title, + 'meta_description': meta_description, + 'logo_url': logo_url, + 'og_title': title, + 'og_description': meta_description, + 'og_image': logo_url, + 'og_url': page_url, + 'og_site_name': title, + 'og_type': 'website', + 'twitter_card': 'summary_large_image' if logo_url else 'summary', + 'twitter_title': title, + 'twitter_description': meta_description, + 'twitter_image': logo_url, + 'landing_base_url': settings.BASE_URL, + 'client_id': client_id, + } + + def get_landing_bot_urls(client: Client) -> Tuple[Optional[str], Optional[str]]: """Возвращает (max_bot_url, telegram_url) для лендинга.""" cleanup_max_bot_integration(client) diff --git a/restoran_max_bot/content_bot/tests.py b/restoran_max_bot/content_bot/tests.py index cec7f02..6d608ac 100644 --- a/restoran_max_bot/content_bot/tests.py +++ b/restoran_max_bot/content_bot/tests.py @@ -4,6 +4,8 @@ from unittest.mock import patch, MagicMock from content_bot.landing_utils import ( normalize_telegram_url, normalize_max_bot_url, + normalize_logo_url, + build_landing_meta, get_landing_bot_urls, ) @@ -39,6 +41,33 @@ class LandingUrlNormalizationTests(TestCase): 'https://max.ru/d0323350531_bot', ) + def test_normalize_logo_cdn_path(self): + self.assertEqual( + normalize_logo_url('/cdn/image/kn7m7u.png'), + 'https://cdn.telefon-ip.ru/cdn/image/kn7m7u.png', + ) + + def test_normalize_logo_full_url(self): + self.assertEqual( + normalize_logo_url('https://saper639.ru/storage/app/media/temp/letobar_logo.png'), + 'https://saper639.ru/storage/app/media/temp/letobar_logo.png', + ) + + def test_build_landing_meta_includes_og_tags(self): + meta = build_landing_meta( + brand='Бар Лето', + description='Описание заведения', + logo='/cdn/image/kn7m7u.png', + client_id=2262, + page_url='https://maxbot.telefon-ip.ru/content/landing/2262/', + ) + + self.assertEqual(meta['og_title'], 'Бар Лето') + self.assertEqual(meta['og_description'], 'Описание заведения') + self.assertEqual(meta['og_image'], 'https://cdn.telefon-ip.ru/cdn/image/kn7m7u.png') + self.assertEqual(meta['og_url'], 'https://maxbot.telefon-ip.ru/content/landing/2262/') + self.assertEqual(meta['twitter_card'], 'summary_large_image') + class LandingBotUrlsTests(TestCase): @patch('content_bot.landing_utils.Integration') @@ -58,11 +87,13 @@ class LandingBotUrlsTests(TestCase): qs.first.return_value = telegram_integration else: qs.first.return_value = None + qs.select_related.return_value = qs return qs mock_integration.objects.filter.side_effect = filter_side_effect - with patch('content_bot.landing_utils._save_settings') as mock_save: + with patch('content_bot.landing_utils.cleanup_max_bot_integration'), \ + patch('content_bot.landing_utils._save_settings') as mock_save: max_url, telegram_url = get_landing_bot_urls(client) self.assertEqual(max_url, 'https://max.ru/d0323350531_bot') @@ -97,3 +128,7 @@ class LandingPageViewTests(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, 'https://max.ru/d0323350531_bot') self.assertContains(response, 'https://t.me/cheguevaraclub_bot') + self.assertContains(response, 'property="og:title"') + self.assertContains(response, 'property="og:description"') + self.assertContains(response, 'property="og:image"') + self.assertContains(response, 'property="og:url"') diff --git a/restoran_max_bot/content_bot/views.py b/restoran_max_bot/content_bot/views.py index 30d7987..2e1b087 100644 --- a/restoran_max_bot/content_bot/views.py +++ b/restoran_max_bot/content_bot/views.py @@ -3,7 +3,7 @@ from django.core.handlers.wsgi import WSGIRequest from django.views.decorators.csrf import csrf_exempt from django.shortcuts import render, get_object_or_404 from max_bot.models import Client, Qr -from content_bot.landing_utils import get_landing_bot_urls +from content_bot.landing_utils import build_landing_meta, get_landing_bot_urls, normalize_logo_url # Create your views here. @@ -28,14 +28,25 @@ def landing_page(request, client_id): client = get_object_or_404(Client, id=client_id, status=True) qr = Qr.objects.filter(client=client).first() max_bot_url, telegram_url = get_landing_bot_urls(client) + brand = qr.brand if qr and qr.brand else client.brand or 'Ресторан' + logo = qr.logo if qr and qr.logo else client.logo or '' + description = qr.description if qr and qr.description else '' context = { 'client_id': client.id, - 'brand': qr.brand if qr and qr.brand else client.brand or 'Ресторан', - 'logo': qr.logo if qr and qr.logo else client.logo or '', - 'description': qr.description if qr and qr.description else '', + 'brand': brand, + 'logo': logo, + 'logo_url': normalize_logo_url(logo), + 'description': description, 'max_bot_url': max_bot_url, 'telegram_url': telegram_url, } + context.update(build_landing_meta( + brand=brand, + description=description, + logo=logo, + client_id=client.id, + page_url=request.build_absolute_uri(), + )) return render(request, 'max_bot/landing.html', context) diff --git a/restoran_max_bot/templates/max_bot/landing.html b/restoran_max_bot/templates/max_bot/landing.html index c7ae944..ee53907 100644 --- a/restoran_max_bot/templates/max_bot/landing.html +++ b/restoran_max_bot/templates/max_bot/landing.html @@ -3,7 +3,24 @@ - {{ brand|default:"Ресторан" }} + {{ page_title|default:brand|default:"Ресторан" }} + + + + + + + {% if og_image %} + + + + {% endif %} + + + + {% if twitter_image %} + + {% endif %}