diff --git a/district/models.py b/district/models.py
index 3907bea03a9f262d18677fc71f24d189bc0c8324..459fcfd400f80a398c3dc9a5898ab226895d8db3 100644
--- a/district/models.py
+++ b/district/models.py
@@ -40,7 +40,6 @@ from maps_utils.const import (
 from maps_utils.validation import validators as maps_validators
 from shared.blocks import (
     DEFAULT_CONTENT_BLOCKS,
-    ButtonBlock,
     ButtonGroupBlock,
     FigureBlock,
     YouTubeVideoBlock,
@@ -53,7 +52,7 @@ from shared.models import (
     MenuMixin,
     SubpageMixin,
 )
-from shared.utils import make_promote_panels
+from shared.utils import make_promote_panels, strip_all_html_tags, trim_to_length
 from tuning import admin_help
 
 from . import blocks
@@ -752,6 +751,18 @@ class DistrictPersonPage(
         context["random_people"] = context["random_people"][:3]
         return context
 
+    def get_meta_image(self):
+        return self.search_image or self.profile_photo
+
+    def get_meta_description(self):
+        if self.search_description:
+            return self.search_description
+
+        if self.text:
+            return trim_to_length(strip_all_html_tags(self.text))
+
+        return None
+
 
 class DistrictPeoplePage(
     ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
@@ -853,6 +864,11 @@ class DistrictPostElectionStrategyPage(
     class Meta:
         verbose_name = "Povolební strategie"
 
+    def get_meta_description(self):
+        if self.search_description:
+            return self.search_description
+        return self.perex
+
 
 class DistrictElectionProgramPage(
     DistrictElectionSubCampaignPageMixin, DistrictElectionBasePage
@@ -886,6 +902,14 @@ class DistrictElectionProgramPage(
     class Meta:
         verbose_name = "Bod programu voleb"
 
+    def get_meta_image(self):
+        return self.search_image or self.image or self.root_page.get_meta_image()
+
+    def get_meta_description(self):
+        if self.search_description:
+            return self.search_description
+        return self.perex
+
 
 class DistrictElectionCampaignPage(DistrictElectionBasePage):
     ### FIELDS
@@ -1007,6 +1031,20 @@ class DistrictElectionCampaignPage(DistrictElectionBasePage):
     def program_points(self):
         return self.get_children().type(DistrictElectionProgramPage).live().specific()
 
+    def get_meta_image(self):
+        return (
+            self.search_image
+            or self.hero_candidates_image
+            or self.hero_image
+            or self.root_page.get_meta_image()
+        )
+
+    def get_meta_description(self):
+        if self.search_description:
+            return self.search_description
+
+        return self.hero_motto
+
 
 class DistrictElectionRootPage(RoutablePageMixin, Page):
     """The election root page only serves as a wrapper for sharing stuff among campaign pages.
@@ -1093,6 +1131,11 @@ class DistrictProgramPage(
         fill_data_from_redmine_for_page(self)
         return super().save(**kwargs)
 
+    def get_meta_description(self):
+        if self.search_description:
+            return self.search_description
+        return self.perex
+
 
 class DistrictCenterPage(
     CalendarMixin, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
@@ -1169,6 +1212,26 @@ class DistrictCenterPage(
     def has_calendar(self):
         return self.calendar_id is not None
 
+    def get_meta_image(self):
+        return (
+            self.search_image
+            or self.background_photo
+            or self.root_page.get_meta_image()
+        )
+
+    def get_meta_description(self):
+        if self.search_description:
+            return self.search_description
+
+        desc = None
+
+        if self.perex:
+            desc = self.perex
+        elif self.text:
+            desc = trim_to_length(strip_all_html_tags(self.text))
+
+        return desc
+
 
 class DistrictCrossroadPage(
     ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
@@ -1404,6 +1467,19 @@ class DistrictGeoFeatureCollectionPage(
 
         return context
 
+    def get_meta_image(self):
+        return (
+            self.search_image
+            or self.logo_image
+            or self.image
+            or self.root_page.get_meta_image()
+        )
+
+    def get_meta_description(self):
+        if self.search_description:
+            return self.search_description
+        return self.perex
+
 
 class DistrictGeoFeatureCollectionCategory(Orderable):
     name = models.CharField("Název", max_length=100)
@@ -1548,13 +1624,11 @@ class DistrictGeoFeatureDetailPage(
         return cached_index
 
     def get_meta_image(self):
-        return self.image
+        return self.search_image or self.image
 
     def get_meta_description(self):
         if self.search_description:
             return self.search_description
-        if len(self.perex) > 150:
-            return str(self.perex)[:150] + "..."
         return self.perex
 
     def get_context(self, request):
diff --git a/elections2021/parser.py b/elections2021/parser.py
index 90584f6d0c668ce9ac2d3e996454505731df0d32..e4db916e42424cba8ba007b7d055cc0f4328f478 100644
--- a/elections2021/parser.py
+++ b/elections2021/parser.py
@@ -8,6 +8,8 @@ import bleach
 import bs4
 from django.utils.text import slugify
 
+from shared.utils import strip_all_html_tags
+
 from .constants import BENEFITS
 
 KNOWN_KEYS = [
@@ -202,10 +204,6 @@ def parse_program_html(fp):
     }
 
 
-def strip_html(value):
-    return bleach.clean(value, tags=[], attributes={}, styles=[], strip=True)
-
-
 def strip_div(value):
     return value.replace("<div>", "").replace("</div>", "")
 
@@ -233,7 +231,7 @@ def clean_point(point):
             raise ValueError(f"Unknown key: {old_key}")
 
         if key in ["nadpis"]:
-            out[key] = strip_html(val)
+            out[key] = strip_all_html_tags(val)
         else:
             out[key] = replace_tags(val)
 
@@ -259,7 +257,7 @@ def prepare_faq(value):
 
 
 def prepare_horizon(value):
-    raw = strip_html(value)
+    raw = strip_all_html_tags(value)
     m = re.match(r"^(\d+)\s(\w+)$", raw)
     if m:
         return None, m.group(1), m.group(2)
diff --git a/shared/models.py b/shared/models.py
index 230ee10fdc3c0a5c9e34e96a8b924cf0fb09cba2..9e50faed7cff3deff886b6c0a41cd5beed696c7a 100644
--- a/shared/models.py
+++ b/shared/models.py
@@ -8,24 +8,11 @@ from wagtail.admin.edit_handlers import (
     PublishingPanel,
     StreamFieldPanel,
 )
-from wagtail.core.blocks import RichTextBlock
 from wagtail.core.fields import StreamField
 from wagtail.core.models import Page
 from wagtail.images.edit_handlers import ImageChooserPanel
 
-from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
-from shared.blocks import (
-    DEFAULT_CONTENT_BLOCKS,
-    CardBlock,
-    FigureBlock,
-    GalleryBlock,
-    MenuItemBlock,
-    MenuParentBlock,
-    ThreeColumnBlock,
-    TwoColumnBlock,
-    YouTubeVideoBlock,
-)
-from shared.const import RICH_TEXT_DEFAULT_FEATURES
+from shared.blocks import DEFAULT_CONTENT_BLOCKS, MenuItemBlock, MenuParentBlock
 
 logger = logging.getLogger(__name__)
 
@@ -100,13 +87,13 @@ class ArticleMixin(models.Model):
         return self.get_parent()
 
     def get_meta_image(self):
+        if hasattr(self, "search_image") and self.search_image:
+            return self.search_image
         return self.image
 
     def get_meta_description(self):
-        if self.search_description:
+        if hasattr(self, "search_description") and self.search_description:
             return self.search_description
-        if len(self.perex) > 150:
-            return str(self.perex)[:150] + "..."
         return self.perex
 
 
diff --git a/shared/utils.py b/shared/utils.py
index 44972b09c018ca0983ea64172fd9c4894955a354..ad56f316399b23e542bccd4b121c4b6a825370a3 100644
--- a/shared/utils.py
+++ b/shared/utils.py
@@ -1,6 +1,7 @@
 import json
 import logging
 
+import bleach
 import requests
 from django.conf import settings
 from django.utils.translation import gettext_lazy
@@ -63,3 +64,24 @@ def subscribe_to_newsletter(email, news_id, source):
             "Failed to subscribe!",
             extra={"payload": payload, "response": response.text},
         )
+
+
+def strip_all_html_tags(value: str):
+    """Drop all HTML tags from given value.
+
+    :param value: string to sanitize
+    """
+    return bleach.clean(
+        value, tags=[], attributes=[], styles=[], strip=True, strip_comments=True
+    )
+
+
+def trim_to_length(value: str, max_length: int = 150):
+    """Trim value to max length shall it exceed it.
+
+    :param value: input string
+    :param max_length: max allowed length
+    """
+    if len(value) > max_length:
+        return value[:max_length] + "..."
+    return value