seo_helpers.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. """
  2. Helpers SEO pour le blog Django
  3. Génération automatique de métadonnées structurées et optimisées
  4. """
  5. from django.utils.html import strip_tags
  6. from django.utils.text import Truncator
  7. from django.urls import reverse
  8. class SEOMetadata:
  9. """Classe pour gérer les métadonnées SEO d'une page"""
  10. def __init__(self, request, obj=None):
  11. self.request = request
  12. self.obj = obj
  13. self.site_name = "Mr Duhaz"
  14. self.site_url = "https://www.duhaz.fr"
  15. # Image par défaut optimisée pour Open Graph (1200x630px recommandé)
  16. self.default_image = f"{self.site_url}/static/logo-txt-Mrduhaz.png"
  17. self.default_image_alt = "Logo Mr Duhaz - Blog de développement web"
  18. self.twitter_handle = "@mrduhaz" # Remplacez par votre vrai handle Twitter
  19. def get_absolute_url(self, path=''):
  20. """Génère une URL absolue"""
  21. if path:
  22. return f"{self.site_url}{path}"
  23. return self.request.build_absolute_uri()
  24. def get_image_url(self, image_field):
  25. """
  26. Génère une URL absolue pour une image
  27. Vérifie si l'URL est déjà absolue ou relative
  28. """
  29. if not image_field:
  30. return self.default_image
  31. # Si l'image commence par http/https, c'est déjà une URL absolue
  32. if image_field.startswith(('http://', 'https://')):
  33. return image_field
  34. # Sinon, construire l'URL absolue
  35. if image_field.startswith('/'):
  36. return f"{self.site_url}{image_field}"
  37. else:
  38. return f"{self.site_url}/{image_field}"
  39. def clean_description(self, text, max_length=160):
  40. """Nettoie et tronque une description pour le SEO"""
  41. if not text:
  42. return ""
  43. # Supprime les balises HTML
  44. clean_text = strip_tags(text)
  45. # Tronque intelligemment
  46. return Truncator(clean_text).chars(max_length, truncate='...')
  47. def get_blog_metadata(self, article):
  48. """Génère les métadonnées complètes pour un article de blog"""
  49. if not article:
  50. return self.get_default_metadata()
  51. # URL absolue de l'article
  52. article_url = self.get_absolute_url(
  53. reverse('blog_play', args=[article.b_titre_slugify])
  54. )
  55. # Image de l'article (ou image par défaut)
  56. image_url = self.get_image_url(article.b_description_img)
  57. image_alt = f"Image de l'article : {article.b_titre}"
  58. # Description nettoyée
  59. description = self.clean_description(article.b_description, 160)
  60. # Catégories pour les keywords
  61. categories = [cat.cb_titre for cat in article.b_cat.all()]
  62. keywords = article.b_mots_clefs if article.b_mots_clefs else ', '.join(categories)
  63. # Dates de publication et modification au format ISO 8601
  64. published_time = article.b_publdate.isoformat() if article.b_publdate else None
  65. # Pour l'instant, on utilise la même date pour modified_time
  66. # Plus tard, vous pourrez ajouter un champ b_modifdate dans votre modèle
  67. modified_time = article.b_publdate.isoformat() if article.b_publdate else None
  68. metadata = {
  69. # Basiques
  70. 'title': f"{article.b_titre} | {self.site_name}",
  71. 'description': description,
  72. 'keywords': keywords,
  73. 'canonical_url': article_url,
  74. 'image': image_url,
  75. 'image_alt': image_alt,
  76. # Open Graph (Facebook)
  77. 'og': {
  78. 'type': 'article',
  79. 'title': article.b_titre,
  80. 'description': description,
  81. 'url': article_url,
  82. 'image': image_url,
  83. 'image:alt': image_alt,
  84. 'image:width': '1200',
  85. 'image:height': '630',
  86. 'site_name': self.site_name,
  87. 'locale': 'fr_FR',
  88. 'article:published_time': published_time,
  89. 'article:modified_time': modified_time,
  90. 'article:author': 'Mr Duhaz',
  91. 'article:section': categories[0] if categories else 'Blog',
  92. 'article:tag': categories,
  93. },
  94. # Twitter Cards
  95. 'twitter': {
  96. 'card': 'summary_large_image',
  97. 'title': article.b_titre,
  98. 'description': description,
  99. 'image': image_url,
  100. 'image:alt': image_alt,
  101. 'site': self.twitter_handle,
  102. 'creator': self.twitter_handle,
  103. },
  104. # Schema.org JSON-LD
  105. 'schema': self.get_article_schema(article, article_url, image_url, description, modified_time),
  106. }
  107. return metadata
  108. def get_article_schema(self, article, url, image, description, modified_time=None):
  109. """Génère le schema JSON-LD pour un article"""
  110. categories = [cat.cb_titre for cat in article.b_cat.all()]
  111. published_time = article.b_publdate.isoformat() if article.b_publdate else None
  112. schema = {
  113. "@context": "https://schema.org",
  114. "@type": "BlogPosting",
  115. "headline": article.b_titre,
  116. "description": description,
  117. "image": {
  118. "@type": "ImageObject",
  119. "url": image,
  120. "width": 1200,
  121. "height": 630
  122. },
  123. "author": {
  124. "@type": "Person",
  125. "name": "Mr Duhaz",
  126. "url": self.site_url
  127. },
  128. "publisher": {
  129. "@type": "Organization",
  130. "name": self.site_name,
  131. "logo": {
  132. "@type": "ImageObject",
  133. "url": f"{self.site_url}/static/logo-txt-Mrduhaz.png"
  134. }
  135. },
  136. "url": url,
  137. "datePublished": published_time,
  138. "dateModified": modified_time or published_time,
  139. "articleSection": categories[0] if categories else "Blog",
  140. "keywords": article.b_mots_clefs if article.b_mots_clefs else ', '.join(categories),
  141. "wordCount": len(strip_tags(article.b_contenu).split()),
  142. "articleBody": self.clean_description(article.b_contenu, 500),
  143. }
  144. return schema
  145. def get_listing_metadata(self, category=None):
  146. """Génère les métadonnées pour la page de listing"""
  147. if category:
  148. title = f"Articles dans {category.cb_titre} | {self.site_name}"
  149. description = f"Découvrez tous les articles de la catégorie {category.cb_titre} sur le blog de {self.site_name}"
  150. url = self.get_absolute_url(reverse('blog_tag', args=[category.cb_titre_slgify]))
  151. else:
  152. title = f"Blog | {self.site_name}"
  153. description = f"Découvrez tous les articles du blog de {self.site_name} : tutoriels, actualités et guides"
  154. url = self.get_absolute_url(reverse('blog_index'))
  155. metadata = {
  156. 'title': title,
  157. 'description': description,
  158. 'canonical_url': url,
  159. 'og': {
  160. 'type': 'website',
  161. 'title': title,
  162. 'description': description,
  163. 'url': url,
  164. 'image': f"{self.site_url}/static/logo-txt-Mrduhaz.png",
  165. 'site_name': self.site_name,
  166. 'locale': 'fr_FR',
  167. },
  168. 'twitter': {
  169. 'card': 'summary',
  170. 'title': title,
  171. 'description': description,
  172. 'image': f"{self.site_url}/static/logo-txt-Mrduhaz.png",
  173. },
  174. }
  175. return metadata
  176. def get_default_metadata(self):
  177. """Métadonnées par défaut du site"""
  178. return {
  179. 'title': self.site_name,
  180. 'description': "Blog de Mr Duhaz : développement web, tutoriels et astuces",
  181. 'canonical_url': self.site_url,
  182. }