1
0

seo_helpers.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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. 'image_type': 'image/jpeg',
  87. 'site_name': self.site_name,
  88. 'locale': 'fr_FR',
  89. 'article_published_time': published_time,
  90. 'article_modified_time': modified_time,
  91. 'article_author': 'Mr Duhaz',
  92. 'article_section': categories[0] if categories else 'Blog',
  93. 'article_tags': categories,
  94. },
  95. # Twitter Cards
  96. 'twitter': {
  97. 'card': 'summary_large_image',
  98. 'title': article.b_titre,
  99. 'description': description,
  100. 'image': image_url,
  101. 'image_alt': image_alt,
  102. 'site': self.twitter_handle,
  103. 'creator': self.twitter_handle,
  104. },
  105. # Schema.org JSON-LD
  106. 'schema': self.get_article_schema(article, article_url, image_url, description, modified_time),
  107. }
  108. return metadata
  109. def get_article_schema(self, article, url, image, description, modified_time=None):
  110. """Génère le schema JSON-LD pour un article"""
  111. categories = [cat.cb_titre for cat in article.b_cat.all()]
  112. published_time = article.b_publdate.isoformat() if article.b_publdate else None
  113. schema = {
  114. "@context": "https://schema.org",
  115. "@type": "BlogPosting",
  116. "headline": article.b_titre,
  117. "description": description,
  118. "image": {
  119. "@type": "ImageObject",
  120. "url": image,
  121. "width": 1200,
  122. "height": 630
  123. },
  124. "author": {
  125. "@type": "Person",
  126. "name": "Mr Duhaz",
  127. "url": self.site_url
  128. },
  129. "publisher": {
  130. "@type": "Organization",
  131. "name": self.site_name,
  132. "logo": {
  133. "@type": "ImageObject",
  134. "url": f"{self.site_url}/static/logo-txt-Mrduhaz.png"
  135. }
  136. },
  137. "url": url,
  138. "datePublished": published_time,
  139. "dateModified": modified_time or published_time,
  140. "articleSection": categories[0] if categories else "Blog",
  141. "keywords": article.b_mots_clefs if article.b_mots_clefs else ', '.join(categories),
  142. "wordCount": len(strip_tags(article.b_contenu).split()),
  143. "articleBody": self.clean_description(article.b_contenu, 500),
  144. }
  145. return schema
  146. def get_listing_metadata(self, category=None):
  147. """Génère les métadonnées pour la page de listing"""
  148. if category:
  149. title = f"Articles dans {category.cb_titre} | {self.site_name}"
  150. description = f"Découvrez tous les articles de la catégorie {category.cb_titre} sur le blog de {self.site_name}"
  151. url = self.get_absolute_url(reverse('blog_tag', args=[category.cb_titre_slgify]))
  152. else:
  153. title = f"Blog | {self.site_name}"
  154. description = f"Découvrez tous les articles du blog de {self.site_name} : tutoriels, actualités et guides"
  155. url = self.get_absolute_url(reverse('blog_index'))
  156. metadata = {
  157. 'title': title,
  158. 'description': description,
  159. 'canonical_url': url,
  160. 'og': {
  161. 'type': 'website',
  162. 'title': title,
  163. 'description': description,
  164. 'url': url,
  165. 'image': f"{self.site_url}/static/logo-txt-Mrduhaz.png",
  166. 'site_name': self.site_name,
  167. 'locale': 'fr_FR',
  168. },
  169. 'twitter': {
  170. 'card': 'summary',
  171. 'title': title,
  172. 'description': description,
  173. 'image': f"{self.site_url}/static/logo-txt-Mrduhaz.png",
  174. },
  175. }
  176. return metadata
  177. def get_default_metadata(self):
  178. """Métadonnées par défaut du site"""
  179. return {
  180. 'title': self.site_name,
  181. 'description': "Blog de Mr Duhaz : développement web, tutoriels et astuces",
  182. 'canonical_url': self.site_url,
  183. }