""" Helpers SEO pour le blog Django Génération automatique de métadonnées structurées et optimisées """ from django.utils.html import strip_tags from django.utils.text import Truncator from django.urls import reverse class SEOMetadata: """Classe pour gérer les métadonnées SEO d'une page""" def __init__(self, request, obj=None): self.request = request self.obj = obj self.site_name = "Mr Duhaz" self.site_url = "https://www.duhaz.fr" # Image par défaut optimisée pour Open Graph (1200x630px recommandé) self.default_image = f"{self.site_url}/static/logo-txt-Mrduhaz.png" self.default_image_alt = "Logo Mr Duhaz - Blog de développement web" self.twitter_handle = "@mrduhaz" # Remplacez par votre vrai handle Twitter def get_absolute_url(self, path=''): """Génère une URL absolue""" if path: return f"{self.site_url}{path}" return self.request.build_absolute_uri() def get_image_url(self, image_field): """ Génère une URL absolue pour une image Vérifie si l'URL est déjà absolue ou relative """ if not image_field: return self.default_image # Si l'image commence par http/https, c'est déjà une URL absolue if image_field.startswith(('http://', 'https://')): return image_field # Sinon, construire l'URL absolue if image_field.startswith('/'): return f"{self.site_url}{image_field}" else: return f"{self.site_url}/{image_field}" def clean_description(self, text, max_length=160): """Nettoie et tronque une description pour le SEO""" if not text: return "" # Supprime les balises HTML clean_text = strip_tags(text) # Tronque intelligemment return Truncator(clean_text).chars(max_length, truncate='...') def get_blog_metadata(self, article): """Génère les métadonnées complètes pour un article de blog""" if not article: return self.get_default_metadata() # URL absolue de l'article article_url = self.get_absolute_url( reverse('blog_play', args=[article.b_titre_slugify]) ) # Image de l'article (ou image par défaut) image_url = self.get_image_url(article.b_description_img) image_alt = f"Image de l'article : {article.b_titre}" # Description nettoyée description = self.clean_description(article.b_description, 160) # Catégories pour les keywords categories = [cat.cb_titre for cat in article.b_cat.all()] keywords = article.b_mots_clefs if article.b_mots_clefs else ', '.join(categories) # Dates de publication et modification au format ISO 8601 published_time = article.b_publdate.isoformat() if article.b_publdate else None # Pour l'instant, on utilise la même date pour modified_time # Plus tard, vous pourrez ajouter un champ b_modifdate dans votre modèle modified_time = article.b_publdate.isoformat() if article.b_publdate else None metadata = { # Basiques 'title': f"{article.b_titre} | {self.site_name}", 'description': description, 'keywords': keywords, 'canonical_url': article_url, 'image': image_url, 'image_alt': image_alt, # Open Graph (Facebook) 'og': { 'type': 'article', 'title': article.b_titre, 'description': description, 'url': article_url, 'image': image_url, 'image_alt': image_alt, 'image_width': '1200', 'image_height': '630', 'image_type': 'image/jpeg', 'site_name': self.site_name, 'locale': 'fr_FR', 'article_published_time': published_time, 'article_modified_time': modified_time, 'article_author': 'Mr Duhaz', 'article_section': categories[0] if categories else 'Blog', 'article_tags': categories, }, # Twitter Cards 'twitter': { 'card': 'summary_large_image', 'title': article.b_titre, 'description': description, 'image': image_url, 'image_alt': image_alt, 'site': self.twitter_handle, 'creator': self.twitter_handle, }, # Schema.org JSON-LD 'schema': self.get_article_schema(article, article_url, image_url, description, modified_time), } return metadata def get_article_schema(self, article, url, image, description, modified_time=None): """Génère le schema JSON-LD pour un article""" categories = [cat.cb_titre for cat in article.b_cat.all()] published_time = article.b_publdate.isoformat() if article.b_publdate else None schema = { "@context": "https://schema.org", "@type": "BlogPosting", "headline": article.b_titre, "description": description, "image": { "@type": "ImageObject", "url": image, "width": 1200, "height": 630 }, "author": { "@type": "Person", "name": "Mr Duhaz", "url": self.site_url }, "publisher": { "@type": "Organization", "name": self.site_name, "logo": { "@type": "ImageObject", "url": f"{self.site_url}/static/logo-txt-Mrduhaz.png" } }, "url": url, "datePublished": published_time, "dateModified": modified_time or published_time, "articleSection": categories[0] if categories else "Blog", "keywords": article.b_mots_clefs if article.b_mots_clefs else ', '.join(categories), "wordCount": len(strip_tags(article.b_contenu).split()), "articleBody": self.clean_description(article.b_contenu, 500), } return schema def get_listing_metadata(self, category=None): """Génère les métadonnées pour la page de listing""" if category: title = f"Articles dans {category.cb_titre} | {self.site_name}" description = f"Découvrez tous les articles de la catégorie {category.cb_titre} sur le blog de {self.site_name}" url = self.get_absolute_url(reverse('blog_tag', args=[category.cb_titre_slgify])) else: title = f"Blog | {self.site_name}" description = f"Découvrez tous les articles du blog de {self.site_name} : tutoriels, actualités et guides" url = self.get_absolute_url(reverse('blog_index')) metadata = { 'title': title, 'description': description, 'canonical_url': url, 'og': { 'type': 'website', 'title': title, 'description': description, 'url': url, 'image': f"{self.site_url}/static/logo-txt-Mrduhaz.png", 'site_name': self.site_name, 'locale': 'fr_FR', }, 'twitter': { 'card': 'summary', 'title': title, 'description': description, 'image': f"{self.site_url}/static/logo-txt-Mrduhaz.png", }, } return metadata def get_default_metadata(self): """Métadonnées par défaut du site""" return { 'title': self.site_name, 'description': "Blog de Mr Duhaz : développement web, tutoriels et astuces", 'canonical_url': self.site_url, }