{"id":920,"date":"2025-04-30T13:09:07","date_gmt":"2025-04-30T11:09:07","guid":{"rendered":"https:\/\/www.syntera.ch\/blog\/?p=920"},"modified":"2025-04-30T13:09:07","modified_gmt":"2025-04-30T11:09:07","slug":"migrating-a-wordpress-blog-to-django-wagtail","status":"publish","type":"post","link":"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/","title":{"rendered":"Migrating a WordPress Blog to Django (Wagtail)"},"content":{"rendered":"\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-f56f613f wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\"><div class=\"wp-block-post-author has-medium-font-size\"><div class=\"wp-block-post-author__avatar\"><img alt='' src='https:\/\/secure.gravatar.com\/avatar\/4a88ceaa2d30e0c6f32856b2a56416e928a7b351e59466174227ac639abe522c?s=48&#038;d=mm&#038;r=g' srcset='https:\/\/secure.gravatar.com\/avatar\/4a88ceaa2d30e0c6f32856b2a56416e928a7b351e59466174227ac639abe522c?s=96&#038;d=mm&#038;r=g 2x' class='avatar avatar-48 photo' height='48' width='48' \/><\/div><div class=\"wp-block-post-author__content\"><p class=\"wp-block-post-author__byline\">LEAD DATA SCIENCE<\/p><p class=\"wp-block-post-author__name\">Kail Kuhn Schlicht<\/p><\/div><\/div><\/div>\n\n\n\n<div class=\"wp-block-column is-vertically-aligned-top is-layout-flow wp-block-column-is-layout-flow\">\n<ul class=\"wp-block-outermost-social-sharing alignright has-small-icon-size has-icon-color is-style-logos-only is-content-justification-left is-layout-flex wp-container-outermost-social-sharing-is-layout-fa5e4718 wp-block-outermost-social-sharing-is-layout-flex\"><li style=\"color: #1a4548\" class=\"outermost-social-sharing-link outermost-social-sharing-link-linkedin has-primary-color wp-block-outermost-social-sharing-link\">\n\t<a href=\"https:\/\/www.linkedin.com\/shareArticle?mini=true&#038;url=https%3A%2F%2Fwww.syntera.ch%2Fblog%2F2025%2F04%2F30%2Fmigrating-a-wordpress-blog-to-django-wagtail%2F&#038;title=Migrating%20a%20WordPress%20Blog%20to%20Django%20%28Wagtail%29\" aria-label=\"Share on LinkedIn\" rel=\"noopener nofollow\" target=\"_blank\" class=\"wp-block-outermost-social-sharing-link-anchor\">\n\t\t<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M19.7,3H4.3C3.582,3,3,3.582,3,4.3v15.4C3,20.418,3.582,21,4.3,21h15.4c0.718,0,1.3-0.582,1.3-1.3V4.3 C21,3.582,20.418,3,19.7,3z M8.339,18.338H5.667v-8.59h2.672V18.338z M7.004,8.574c-0.857,0-1.549-0.694-1.549-1.548 c0-0.855,0.691-1.548,1.549-1.548c0.854,0,1.547,0.694,1.547,1.548C8.551,7.881,7.858,8.574,7.004,8.574z M18.339,18.338h-2.669 v-4.177c0-0.996-0.017-2.278-1.387-2.278c-1.389,0-1.601,1.086-1.601,2.206v4.249h-2.667v-8.59h2.559v1.174h0.037 c0.356-0.675,1.227-1.387,2.526-1.387c2.703,0,3.203,1.779,3.203,4.092V18.338z\"><\/path><\/svg>\t\t<span class=\"wp-block-outermost-social-sharing-link-label screen-reader-text\">\n\t\t\tShare on LinkedIn\t\t<\/span>\n\t<\/a>\n<\/li>\n\n\n<li style=\"color: #1a4548\" class=\"outermost-social-sharing-link outermost-social-sharing-link-mail has-primary-color wp-block-outermost-social-sharing-link\">\n\t<a href=\"mailto:?subject=Migrating%20a%20WordPress%20Blog%20to%20Django%20%28Wagtail%29&#038;body=Migrating%20a%20WordPress%20Blog%20to%20Django%20%28Wagtail%29%20&mdash;%20https%3A%2F%2Fwww.syntera.ch%2Fblog%2F2025%2F04%2F30%2Fmigrating-a-wordpress-blog-to-django-wagtail%2F\" aria-label=\"Email this Page\"  class=\"wp-block-outermost-social-sharing-link-anchor\">\n\t\t<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M20,4H4C2.895,4,2,4.895,2,6v12c0,1.105,0.895,2,2,2h16c1.105,0,2-0.895,2-2V6C22,4.895,21.105,4,20,4z M20,8.236l-8,4.882 L4,8.236V6h16V8.236z\"><\/path><\/svg>\t\t<span class=\"wp-block-outermost-social-sharing-link-label screen-reader-text\">\n\t\t\tEmail this Page\t\t<\/span>\n\t<\/a>\n<\/li>\n\n\n<li style=\"color: #1a4548\" class=\"outermost-social-sharing-link outermost-social-sharing-link-whatsapp has-primary-color wp-block-outermost-social-sharing-link\">\n\t<a href=\"https:\/\/api.whatsapp.com\/send?text=Migrating%20a%20WordPress%20Blog%20to%20Django%20%28Wagtail%29%20&mdash;%20https%3A%2F%2Fwww.syntera.ch%2Fblog%2F2025%2F04%2F30%2Fmigrating-a-wordpress-blog-to-django-wagtail%2F\" aria-label=\"Share on WhatsApp\" rel=\"noopener nofollow\" target=\"_blank\" class=\"wp-block-outermost-social-sharing-link-anchor\">\n\t\t<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M 12.011719 2 C 6.5057187 2 2.0234844 6.478375 2.0214844 11.984375 C 2.0204844 13.744375 2.4814687 15.462563 3.3554688 16.976562 L 2 22 L 7.2324219 20.763672 C 8.6914219 21.559672 10.333859 21.977516 12.005859 21.978516 L 12.009766 21.978516 C 17.514766 21.978516 21.995047 17.499141 21.998047 11.994141 C 22.000047 9.3251406 20.962172 6.8157344 19.076172 4.9277344 C 17.190172 3.0407344 14.683719 2.001 12.011719 2 z M 12.009766 4 C 14.145766 4.001 16.153109 4.8337969 17.662109 6.3417969 C 19.171109 7.8517969 20.000047 9.8581875 19.998047 11.992188 C 19.996047 16.396187 16.413812 19.978516 12.007812 19.978516 C 10.674812 19.977516 9.3544062 19.642812 8.1914062 19.007812 L 7.5175781 18.640625 L 6.7734375 18.816406 L 4.8046875 19.28125 L 5.2851562 17.496094 L 5.5019531 16.695312 L 5.0878906 15.976562 C 4.3898906 14.768562 4.0204844 13.387375 4.0214844 11.984375 C 4.0234844 7.582375 7.6067656 4 12.009766 4 z M 8.4765625 7.375 C 8.3095625 7.375 8.0395469 7.4375 7.8105469 7.6875 C 7.5815469 7.9365 6.9355469 8.5395781 6.9355469 9.7675781 C 6.9355469 10.995578 7.8300781 12.182609 7.9550781 12.349609 C 8.0790781 12.515609 9.68175 15.115234 12.21875 16.115234 C 14.32675 16.946234 14.754891 16.782234 15.212891 16.740234 C 15.670891 16.699234 16.690438 16.137687 16.898438 15.554688 C 17.106437 14.971687 17.106922 14.470187 17.044922 14.367188 C 16.982922 14.263188 16.816406 14.201172 16.566406 14.076172 C 16.317406 13.951172 15.090328 13.348625 14.861328 13.265625 C 14.632328 13.182625 14.464828 13.140625 14.298828 13.390625 C 14.132828 13.640625 13.655766 14.201187 13.509766 14.367188 C 13.363766 14.534188 13.21875 14.556641 12.96875 14.431641 C 12.71875 14.305641 11.914938 14.041406 10.960938 13.191406 C 10.218937 12.530406 9.7182656 11.714844 9.5722656 11.464844 C 9.4272656 11.215844 9.5585938 11.079078 9.6835938 10.955078 C 9.7955938 10.843078 9.9316406 10.663578 10.056641 10.517578 C 10.180641 10.371578 10.223641 10.267562 10.306641 10.101562 C 10.389641 9.9355625 10.347156 9.7890625 10.285156 9.6640625 C 10.223156 9.5390625 9.737625 8.3065 9.515625 7.8125 C 9.328625 7.3975 9.131125 7.3878594 8.953125 7.3808594 C 8.808125 7.3748594 8.6425625 7.375 8.4765625 7.375 z\"><\/path><\/svg>\t\t<span class=\"wp-block-outermost-social-sharing-link-label screen-reader-text\">\n\t\t\tShare on WhatsApp\t\t<\/span>\n\t<\/a>\n<\/li>\n<\/ul>\n<\/div>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Problem Statement<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Our company website has been running on PHP + WordPress for years, but it\u2019s time to leave behind plugin roulette, limited extensibility, and the burden of maintaining a non-Python backend.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We wanted a single&nbsp;<strong>Python<\/strong>&nbsp;codebase where we could:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Keep a first\u2011class CMS experience for non\u2011technical editors.<\/li>\n\n\n\n<li>Share one auth\/session layer across future Django apps that want to implement.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Django<\/strong>&nbsp;was the obvious framework home, but which CMS layer? We compared Mezzanine, Django\u2011CMS, plain Django admin plus custom forms, and&nbsp;<strong>Wagtail<\/strong>. Wagtail won thanks to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>StreamField<\/strong>&nbsp;\u2013 Gutenberg\u2011style flexibility with structured data.<\/li>\n\n\n\n<li><strong>Image &amp; media pipeline<\/strong>&nbsp;\u2013 renditions, focal\u2011point cropping, WebP out of the box.<\/li>\n\n\n\n<li><strong>Polished editor UI<\/strong>&nbsp;\u2013 non\u2011devs can hit&nbsp;<em>Save &amp; Publish<\/em>&nbsp;confidently.<\/li>\n\n\n\n<li><strong>Lightweight architecture<\/strong>&nbsp;\u2013 everything is just Django models, views, and templates; no plugin maze.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The rest of this post shows how we migrated, step\u2011by\u2011step, while retaining the parts WordPress we liked and unlocking Django\u2019s app\u2011building capabilities.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Why\u00a0<strong>Django\u00a0Wagtail<\/strong>\u00a0Instead of Plain Django<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Concern<\/th><th>Plain Django<\/th><th>Django&nbsp;<strong>Wagtail<\/strong><\/th><\/tr><tr><td><strong>Editor UI<\/strong><\/td><td>Build your own admin or rely on stock Django admin (not CMS\u2011friendly).<\/td><td>Polished CMS dashboard, StreamField blocks, image chooser, preview.<\/td><\/tr><tr><td><strong>Rich Content<\/strong><\/td><td>Custom models + custom forms.<\/td><td>StreamField = Gutenberg\u2011style flexibility with structured data.<\/td><\/tr><tr><td><strong>Media &amp; Images<\/strong><\/td><td>Write your own thumbnail \/ rendition logic.<\/td><td>Built\u2011in image renditions, focal\u2011point cropping, collections.<\/td><\/tr><tr><td><strong>SEO \/ Redirects<\/strong><\/td><td>Add 3rd\u2011party libs.<\/td><td>First\u2011party&nbsp;<code>wagtailseo<\/code>,&nbsp;<code>wagtailredirects<\/code>.<\/td><\/tr><tr><td><strong>Menus \/ Snippets<\/strong><\/td><td>Roll your own.<\/td><td><code>wagtailmenus<\/code>, snippets chooser panels.<\/td><\/tr><tr><td><strong>Upgrade cadence<\/strong><\/td><td>Django LTS only.<\/td><td>Django + Wagtail LTS; Wagtail\u2019s editor features evolve faster.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Bottom line:<\/strong> Wagtail adds the CMS layer so you don\u2019t reinvent page editing, yet you still write pure Django under the hood.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. What you need to do in Django:<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Here we will cover only the essential parts that are easy to get wrong. As the most important task, you will need to declare the models which you will use to store the data coming from WordPress.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># blog\/models.py\n\nfrom django.db import models\nfrom modelcluster.contrib.taggit import ClusterTaggableManager\nfrom modelcluster.fields import ParentalKey, ParentalManyToManyField\nfrom taggit.models import TaggedItemBase\nfrom wagtail.admin.panels import FieldPanel, InlinePanel\nfrom wagtail.fields import RichTextField\nfrom wagtail.models import Page\n\n# ----------------- Authors -----------------\nclass BlogAuthor(models.Model):\n    wp_id = models.PositiveIntegerField(unique=True)  # maps 1\u2011to\u20111 with wp_users.ID\n    name = models.CharField(max_length=255)\n    email = models.EmailField()\n\n    panels = &#91;FieldPanel(\"name\"), FieldPanel(\"email\")]\n\n    def __str__(self):\n        return self.name\n\n\n# ----------------- Categories -----------------\nclass BlogCategory(models.Model):\n    name = models.CharField(max_length=255)\n    slug = models.SlugField(unique=True)\n\n    panels = &#91;FieldPanel(\"name\"), FieldPanel(\"slug\")]\n\n    def __str__(self):\n        return self.name\n\n\n# ----------------- Tags -----------------\nclass BlogPageTag(TaggedItemBase):\n    content_object = ParentalKey(\n        \"blog.BlogPage\", related_name=\"tagged_items\", on_delete=models.CASCADE\n    )\n\n\n# ----------------- Blog index -----------------\nclass BlogIndexPage(Page):\n    intro = RichTextField(blank=True)\n\n    content_panels = Page.content_panels + &#91;FieldPanel(\"intro\")]\n\n    parent_page_types = &#91;\"home.HomePage\"]\n    subpage_types = &#91;\"blog.BlogPage\"]\n\n    class Meta:\n        verbose_name = \"Blog Index Page\"\n\n\n# ----------------- Individual post -----------------\nclass BlogPage(Page):\n    date = models.DateField(\"Post date\")\n    author = models.ForeignKey(\n        \"blog.BlogAuthor\", null=True, blank=True, on_delete=models.SET_NULL\n    )\n    cover_image = models.ForeignKey(\n        \"wagtailimages.Image\", null=True, blank=True, on_delete=models.SET_NULL, related_name=\"+\"\n    )\n    body = RichTextField(blank=True)\n    categories = ParentalManyToManyField(\"blog.BlogCategory\", blank=True)\n    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)\n\n    content_panels = Page.content_panels + &#91;\n        FieldPanel(\"date\"),\n        FieldPanel(\"author\"),\n        FieldPanel(\"cover_image\"),\n        FieldPanel(\"body\"),\n        FieldPanel(\"categories\"),\n        FieldPanel(\"tags\"),\n        InlinePanel(\"post_comments\", label=\"Comments\"),\n    ]\n\n    class Meta:\n        verbose_name = \"Blog Post\"\n\n\n# ----------------- Comments -----------------\nclass BlogComment(models.Model):\n    post = ParentalKey(\n        \"blog.BlogPage\", related_name=\"post_comments\", on_delete=models.CASCADE\n    )\n    author = models.CharField(max_length=255)\n    email = models.EmailField(blank=True)\n    content = models.TextField()\n    approved = models.BooleanField(default=False)\n    date = models.DateTimeField(auto_now_add=True)\n    parent = models.ForeignKey(\"self\", null=True, blank=True, on_delete=models.SET_NULL)\n\n    panels = &#91;\n        FieldPanel(\"author\"),\n        FieldPanel(\"email\"),\n        FieldPanel(\"content\"),\n        FieldPanel(\"approved\"),\n    ]\n\n    def __str__(self):\n        return f\"Comment by {self.author} on {self.post.title}\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why these choices?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code><strong>wp_id<\/strong><\/code>&nbsp;on&nbsp;<code>BlogAuthor<\/code>&nbsp;lets the XML import map authors flawlessly.<\/li>\n\n\n\n<li><code><strong>ParentalManyToManyField<\/strong><\/code>&nbsp;keeps category edits inline\u2014no admin hopping.<\/li>\n\n\n\n<li><code><strong>RichTextField<\/strong><\/code>&nbsp;for body keeps import simple; later we can convert to StreamField blocks if we need richer layouts.<\/li>\n\n\n\n<li><strong>Comments<\/strong>&nbsp;live as a child relation so editors can moderate without leaving the post.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">With models in place, we can now parse XML and hydrate these objects\u2014next section shows the command that does exactly that.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. XML Import Pain \u2014 Why Existing Libraries Fell Short<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Before reaching for&nbsp;<code>xmltodict<\/code>, we trial\u2011ran every library we could find:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>Library<\/td><td>Status in 2025<\/td><td>Deal\u2011Breaker<\/td><\/tr><tr><td><code><strong>wagtail-wordpress-import<\/strong><\/code><\/td><td>Alpha, opinionated<\/td><td>Tied to their demo models; mis\u2011mapped our categories<\/td><\/tr><tr><td><code><strong>wagtail-transfer<\/strong><\/code><\/td><td>Production\u2011ready\u2014but Wagtail\u202f\u2192\u202fWagtail<\/td><td>Not designed for WordPress<\/td><\/tr><tr><td><code><strong>django-import-export<\/strong><\/code><\/td><td>Generic fixtures<\/td><td>Knows nothing about WordPress schema<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">After three evenings of trial\u2011and\u2011error, the verdict was clear:&nbsp;<strong>roll our own once, run it forever<\/strong>.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">Export your Data from WordPress<\/h5>\n\n\n\n<p class=\"wp-block-paragraph\">Simply go to your WordPress Dashboard and go to &#8220;Tools&#8221; and select &#8220;All content&#8221; and click on &#8220;Downliad Export File&#8221; as shown below.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"413\" src=\"https:\/\/www.syntera.ch\/blog\/wp-content\/uploads\/2025\/04\/Screenshot-2025-04-30-at-12.38.00-1024x413.png\" alt=\"\" class=\"wp-image-932\" srcset=\"https:\/\/www.syntera.ch\/blog\/wp-content\/uploads\/2025\/04\/Screenshot-2025-04-30-at-12.38.00-1024x413.png 1024w, https:\/\/www.syntera.ch\/blog\/wp-content\/uploads\/2025\/04\/Screenshot-2025-04-30-at-12.38.00-300x121.png 300w, https:\/\/www.syntera.ch\/blog\/wp-content\/uploads\/2025\/04\/Screenshot-2025-04-30-at-12.38.00-768x310.png 768w, https:\/\/www.syntera.ch\/blog\/wp-content\/uploads\/2025\/04\/Screenshot-2025-04-30-at-12.38.00.png 1055w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h5 class=\"wp-block-heading\">Our Custom Management Command<\/h5>\n\n\n\n<p class=\"wp-block-paragraph\">After wrestling with half\u2011maintained libraries, we wrote a&nbsp;<em>single<\/em>&nbsp;management command that:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Parses WordPress XML via&nbsp;<code>ElementTree<\/code>&nbsp;(faster, no type leaks).<\/li>\n\n\n\n<li>Cleans messy HTML with&nbsp;<strong>BeautifulSoup\u202f+\u202fBleach<\/strong>&nbsp;so Wagtail\u2019s rich\u2011text import never chokes.<\/li>\n\n\n\n<li>Imports authors, categories, tags, cover images, and nested comments in two passes (first create, then parent\u2011link).<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code># blog\/management\/commands\/import_wordpress.py\n\nimport html\nimport xml.etree.ElementTree as ET\nfrom datetime import datetime\nfrom email.utils import parsedate_to_datetime\n\nimport bleach\nfrom blog.models import BlogCategory, BlogComment, BlogIndexPage, BlogPage\nfrom bs4 import BeautifulSoup\nfrom django.core.management.base import BaseCommand\nfrom django.utils import timezone\nfrom django.utils.dateparse import parse_datetime\nfrom django.utils.text import slugify\n\nfrom wagtail.models import Page\n\n# --- Extra imports for author and cover image handling\nimport requests\nimport os\nfrom django.core.files.base import ContentFile\nfrom wagtail.images.models import Image\nfrom blog.models import BlogAuthor\n\n# Namespaces for WordPress XML\nns = {\n    \"content\": \"http:\/\/purl.org\/rss\/1.0\/modules\/content\/\",\n    \"dc\": \"http:\/\/purl.org\/dc\/elements\/1.1\/\",\n    \"wp\": \"http:\/\/wordpress.org\/export\/1.2\/\",\n}\n\n# ------------------------------------------------------------------\n# Clean up WordPress HTML so Wagtail's rich\u2011text converter doesn't\n# choke on orphan &lt;li&gt; tags or other malformed markup.\nALLOWED_TAGS = &#91;\n    \"p\",\n    \"br\",\n    \"strong\",\n    \"em\",\n    \"a\",\n    \"ul\",\n    \"ol\",\n    \"li\",\n    \"blockquote\",\n    \"h1\",\n    \"h2\",\n    \"h3\",\n    \"h4\",\n    \"h5\",\n    \"h6\",\n    \"img\",\n    \"pre\",\n    \"code\",\n    \"hr\",\n]\nALLOWED_ATTRS = {\"a\": &#91;\"href\", \"title\"], \"img\": &#91;\"src\", \"alt\"]}\n\n\ndef clean_wp_html(raw: str) -&gt; str:\n    soup = BeautifulSoup(raw or \"\", \"html.parser\")\n\n    # Wrap orphan &lt;li&gt; in &lt;ul&gt;\n    for li in soup.find_all(\"li\"):\n        if li.parent.name not in (\"ul\", \"ol\"):\n            wrapper = soup.new_tag(\"ul\")\n            li.wrap(wrapper)\n\n    # Normalise &lt;br&gt; tags to self\u2011closing &lt;br \/&gt; so Wagtail's converter\n    # doesn't confuse an implicit &lt;\/br&gt; close.\n    for br in soup.find_all(\"br\"):\n        br.attrs = {}  # strip any stray attributes\n\n    cleaned = bleach.clean(\n        str(soup),\n        tags=ALLOWED_TAGS,\n        attributes=ALLOWED_ATTRS,\n        strip=True,\n    )\n    # Replace any &lt;br&gt;&lt;\/br&gt; or &lt;br&gt;  pairs that slipped through with a self\u2011closing tag.\n    cleaned = cleaned.replace(\"&lt;br&gt;&lt;\/br&gt;\", \"&lt;br \/&gt;\").replace(\"&lt;br&gt;\", \"&lt;br \/&gt;\")\n    return cleaned\n\n\n# ------------------------------------------------------------------\n\n\nclass Command(BaseCommand):\n    help = \"Import WordPress blog posts and comments from XML export\"\n\n    def add_arguments(self, parser):\n        parser.add_argument(\"xml_file\", type=str, help=\"Path to WordPress XML file\")\n\n    def handle(self, *args, **kwargs):\n        xml_file = kwargs&#91;\"xml_file\"]\n        tree = ET.parse(xml_file)\n        root = tree.getroot()\n\n        # Preload WordPress authors\n        authors_map = {}\n        for author in root.findall(\".\/channel\/wp:author\", ns):\n            login = author.findtext(\"wp:author_login\", namespaces=ns)\n            authors_map&#91;login] = {\n                \"id\": author.findtext(\"wp:author_id\", namespaces=ns),\n                \"name\": author.findtext(\"wp:author_display_name\", namespaces=ns),\n                \"email\": author.findtext(\"wp:author_email\", namespaces=ns),\n            }\n\n        # Preload attachments for cover images\n        attachments_map = {}\n        for item in root.findall(\".\/channel\/item\"):\n            if item.findtext(\"wp:post_type\", namespaces=ns) == \"attachment\":\n                attachments_map&#91;item.findtext(\"wp:post_id\", namespaces=ns)] = item.findtext(\"wp:attachment_url\", namespaces=ns)\n\n        # Map post ID to cover URL via _thumbnail_id postmeta\n        thumbnail_map = {}\n        for item in root.findall(\".\/channel\/item\"):\n            if item.findtext(\"wp:post_type\", namespaces=ns) == \"post\":\n                post_id = item.findtext(\"wp:post_id\", namespaces=ns)\n                for pm in item.findall(\".\/wp:postmeta\", namespaces=ns):\n                    if pm.findtext(\"wp:meta_key\", namespaces=ns) == \"_thumbnail_id\":\n                        thumb_id = pm.findtext(\"wp:meta_value\", namespaces=ns)\n                        thumbnail_map&#91;post_id] = attachments_map.get(thumb_id)\n\n        # Helper: generate a unique slug (\u2264255\u202fchars) among the BlogIndexPage\u2019s children\n        def _generate_unique_slug(parent_page, title):\n            base_slug = slugify(title)&#91;:255] or \"post\"\n            slug = base_slug\n            suffix = 1\n            while parent_page.get_children().filter(slug=slug).exists():\n                slug = f\"{base_slug}-{suffix}\"\n                suffix += 1\n            return slug\n\n        # Get BlogIndexPage (must be created manually first)\n        try:\n            blog_index = BlogIndexPage.objects.first()\n            if not blog_index:\n                self.stderr.write(\n                    \"\u274c No BlogIndexPage found. Please create one in the Wagtail admin first.\"\n                )\n                return\n        except BlogIndexPage.DoesNotExist:\n            self.stderr.write(\"\u274c BlogIndexPage model not defined.\")\n            return\n\n        for item in root.findall(\".\/channel\/item\"):\n            post_type = item.find(\".\/wp:post_type\", ns)\n            if post_type is not None and post_type.text == \"post\":\n                # Capture this post\u2019s ID\n                wp_post_id = item.findtext(\"wp:post_id\", namespaces=ns)\n                title = item.findtext(\"title\")\n                raw_body = html.unescape(item.find(\"content:encoded\", ns).text or \"\")\n                content = clean_wp_html(raw_body)\n                pub_date = item.findtext(\"pubDate\")\n                # Parse publication date (WordPress uses RFC\u202f2822). Fallback to now().\n                try:\n                    parsed_date = parsedate_to_datetime(pub_date) if pub_date else None\n                except (TypeError, ValueError):\n                    parsed_date = None\n                if parsed_date is None:\n                    parsed_date = timezone.now()\n\n                # --- WordPress author mapping\n                login = item.findtext(\"dc:creator\", namespaces=ns)\n                author_data = authors_map.get(login)\n                if author_data:\n                    author_obj, _ = BlogAuthor.objects.get_or_create(\n                        wp_id=author_data&#91;\"id\"],\n                        defaults={\"name\": author_data&#91;\"name\"], \"email\": author_data&#91;\"email\"]},\n                    )\n                else:\n                    author_obj = None\n\n                categories = &#91;\n                    c.text\n                    for c in item.findall(\"category\")\n                    if c.get(\"domain\") == \"category\"\n                ]\n                tags = &#91;\n                    t.text\n                    for t in item.findall(\"category\")\n                    if t.get(\"domain\") == \"post_tag\"\n                ]\n\n                blog_page = BlogPage(\n                    title=title,\n                    slug=_generate_unique_slug(blog_index, title),\n                    author=author_obj,\n                    body=content,\n                    date=parsed_date.date(),\n                )\n\n                # Attach and save the blog page\n                blog_index.add_child(instance=blog_page)\n                blog_page.save()  # Ensure instance is saved first\n\n                # Import cover image if available\n                cover_url = thumbnail_map.get(wp_post_id)\n                if cover_url:\n                    try:\n                        resp = requests.get(cover_url)\n                        resp.raise_for_status()\n                        image_name = os.path.basename(cover_url)\n                        image_file = ContentFile(resp.content, name=image_name)\n                        wagtail_image = Image.objects.create(title=f\"Cover for {title}\", file=image_file)\n                        blog_page.cover_image = wagtail_image\n                        blog_page.save(update_fields=&#91;\"cover_image\"])\n                    except Exception as e:\n                        self.stderr.write(f\"Failed to import cover image for {title}: {e}\")\n\n                # Tags (ClusterTaggableManager handles creation)\n                if tags:\n                    blog_page.tags.add(*tags)\n\n                for cat in categories:\n                    category_obj, _ = BlogCategory.objects.get_or_create(\n                        slug=slugify(cat), defaults={\"name\": cat}\n                    )\n                    blog_page.categories.add(category_obj)\n\n                # Final save and publish\n                blog_page.save()\n                blog_page.save_revision().publish()\n\n                # \u2014\u2014\u2014 prepare to map WordPress comment IDs \u2192 BlogComment objects\n                comments_map = {}\n\n                # First pass: create comments without parents\n                for comment in item.findall(\".\/wp:comment\", ns):\n                    # Ignore pingbacks \/ trackbacks (WordPress marks them via &lt;wp:comment_type&gt;)\n                    ctype = comment.findtext(\n                        \"wp:comment_type\", default=\"\", namespaces=ns\n                    )\n                    if ctype and ctype.strip() not in (\"\", \"comment\"):\n                        continue\n\n                    wp_comment_id = comment.findtext(\"wp:comment_id\", namespaces=ns)\n                    parent_wpid = (\n                        comment.findtext(\"wp:comment_parent\", namespaces=ns) or None\n                    )\n\n                    comment_obj = BlogComment.objects.create(\n                        post=blog_page,\n                        author=comment.findtext(\n                            \"wp:comment_author\", default=\"\", namespaces=ns\n                        ),\n                        email=comment.findtext(\n                            \"wp:comment_author_email\", default=\"\", namespaces=ns\n                        ),\n                        date=timezone.make_aware(\n                            parse_datetime(\n                                comment.findtext(\"wp:comment_date\", namespaces=ns)\n                            )\n                            or datetime.now()\n                        ),\n                        content=html.unescape(\n                            comment.findtext(\n                                \"wp:comment_content\", default=\"\", namespaces=ns\n                            )\n                        ),\n                        approved=comment.findtext(\"wp:comment_approved\", namespaces=ns)\n                        == \"1\",\n                        parent=None,  # set later\n                    )\n                    comments_map&#91;wp_comment_id] = (comment_obj, parent_wpid)\n\n                # Second pass: hook up parent relationships now that all comments exist\n                for wp_comment_id, (comment_obj, parent_wpid) in comments_map.items():\n                    if parent_wpid and parent_wpid in comments_map:\n                        parent_obj, _ = comments_map&#91;parent_wpid]\n                        comment_obj.parent = parent_obj\n                        comment_obj.save(update_fields=&#91;\"parent\"])\n\n                self.stdout.write(\n                    f\"  \u21b3 Imported {len(comments_map)} comments, {len(tags)} tags, {len(categories)} categories for '{title}'\"\n                )\n\n                self.stdout.write(self.style.SUCCESS(f\"\u2705 Imported post: {title}\"))\n\n        self.stdout.write(\n            self.style.SUCCESS(\"All blog posts and comments imported successfully.\")\n        )\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You can run that script using: <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python manage.py import_wordpress \/path\/to\/wordpress.xml<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4. How can you make Wagtail have a WordPress-like experience?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Must-have:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>WordPress Feature<\/td><td>Wagtail Equivalent<\/td><td>Why You Probably Need It<\/td><\/tr><tr><td>Permalink structure<\/td><td><code>wagtailredirects<\/code>, custom&nbsp;<code>Route<\/code>&nbsp;mixins<\/td><td>Preserve SEO juice &amp; old backlinks.<\/td><\/tr><tr><td>Menus (Appearance \u2192 Menus)<\/td><td><code>wagtailmenus<\/code><\/td><td>Drag\u2011and\u2011drop nav builder for editors.<\/td><\/tr><tr><td>Yoast SEO<\/td><td><code>wagtailseo<\/code>,&nbsp;<code>wagtail-metadata<\/code><\/td><td>Title\/description previews, OpenGraph tags.<\/td><\/tr><tr><td>Widgets \/ Sidebars<\/td><td>Snippets + inclusion tags<\/td><td>Recent posts, categories list, etc.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Nice\u2011to\u2011haves:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>WordPress Feature<\/td><td>Wagtail Equivalent<\/td><td>Notes<\/td><\/tr><tr><td>Gutenberg Blocks<\/td><td>Custom&nbsp;<strong>StreamField<\/strong>&nbsp;blocks<\/td><td>Re\u2011create fancy layouts with icons &amp; help\u2011text.<\/td><\/tr><tr><td>Comments<\/td><td>Disqus embed or&nbsp;<code>wagtail-commenting<\/code><\/td><td>Offload spam filtering &amp; moderation.<\/td><\/tr><tr><td>Multilingual (WPML\/Polylang)<\/td><td><code>wagtail-localize<\/code><\/td><td>Locale\u2011aware URLs, translation workflow.<\/td><\/tr><tr><td>Forms (Contact Form\u202f7)<\/td><td><code>wagtailformblocks<\/code>&nbsp;or native&nbsp;<code>FormPage<\/code><\/td><td>Email hooks, Akismet spam protection.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">5. Conclusion &amp; Key Takeaways<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Migrating a mature WordPress site to\u00a0<strong>Django + Wagtail<\/strong>\u00a0isn\u2019t a weekend hobby project\u2014but it\u2019s far from the multi\u2011month project many teams would fear.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Unified Python stack<\/strong>&nbsp;\u2192 one deployment pipeline, one set of libraries, easier hiring (if needed).<\/li>\n\n\n\n<li><strong>Performance &amp; security gains<\/strong>&nbsp;\u2192 Core Web Vitals up, plugin exploits down.<\/li>\n\n\n\n<li><strong>First\u2011class editor UX<\/strong>&nbsp;\u2192 StreamField, image choosers, and drag\u2011and\u2011drop menus keep non\u2011devs happy.<\/li>\n\n\n\n<li><strong>Custom import pipeline<\/strong>&nbsp;\u2192 XML\u2011to\u2011Wagtail in minutes, not days, with comments, authors, and media intact.<\/li>\n\n\n\n<li><strong>Incremental parity roadmap<\/strong>&nbsp;\u2192 tackle must\u2011have packages first, add \u201cnice\u2011to\u2011haves\u201d when you\u2019re ready.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Problem Statement Our company website has been running on PHP + WordPress for years, but it\u2019s time to leave behind plugin roulette, limited extensibility, and the burden of maintaining a non-Python backend. We wanted a single&nbsp;Python&nbsp;codebase where we could: Django&nbsp;was the obvious framework home, but which CMS layer? We compared Mezzanine, Django\u2011CMS, plain Django admin [&hellip;]<\/p>\n","protected":false},"author":5,"featured_media":931,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[48],"tags":[52],"class_list":["post-920","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-django","tag-wagtail"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v21.8 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Migrating a Wordpress Blog to Django (Wagtail) - Syntera<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Migrating a Wordpress Blog to Django (Wagtail) - Syntera\" \/>\n<meta property=\"og:description\" content=\"Problem Statement Our company website has been running on PHP + WordPress for years, but it\u2019s time to leave behind plugin roulette, limited extensibility, and the burden of maintaining a non-Python backend. We wanted a single&nbsp;Python&nbsp;codebase where we could: Django&nbsp;was the obvious framework home, but which CMS layer? We compared Mezzanine, Django\u2011CMS, plain Django admin [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/\" \/>\n<meta property=\"og:site_name\" content=\"Syntera\" \/>\n<meta property=\"article:published_time\" content=\"2025-04-30T11:09:07+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.syntera.ch\/blog\/wp-content\/uploads\/2025\/04\/wagtail.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1024\" \/>\n\t<meta property=\"og:image:height\" content=\"683\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Kail Kuhn Schlicht\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Kail Kuhn Schlicht\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"4 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/\",\"url\":\"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/\",\"name\":\"Migrating a Wordpress Blog to Django (Wagtail) - Syntera\",\"isPartOf\":{\"@id\":\"https:\/\/www.syntera.ch\/blog\/#website\"},\"datePublished\":\"2025-04-30T11:09:07+00:00\",\"dateModified\":\"2025-04-30T11:09:07+00:00\",\"author\":{\"@id\":\"https:\/\/www.syntera.ch\/blog\/#\/schema\/person\/8048dfb9e4b2f044222a4baa11ee3e68\"},\"breadcrumb\":{\"@id\":\"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.syntera.ch\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Migrating a WordPress Blog to Django (Wagtail)\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.syntera.ch\/blog\/#website\",\"url\":\"https:\/\/www.syntera.ch\/blog\/\",\"name\":\"Syntera\",\"description\":\"translating data into business value.\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.syntera.ch\/blog\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.syntera.ch\/blog\/#\/schema\/person\/8048dfb9e4b2f044222a4baa11ee3e68\",\"name\":\"Kail Kuhn Schlicht\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.syntera.ch\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/4a88ceaa2d30e0c6f32856b2a56416e928a7b351e59466174227ac639abe522c?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/4a88ceaa2d30e0c6f32856b2a56416e928a7b351e59466174227ac639abe522c?s=96&d=mm&r=g\",\"caption\":\"Kail Kuhn Schlicht\"},\"sameAs\":[\"https:\/\/www.linkedin.com\/in\/kail-kuhn-schlicht-603584a3\/\"],\"url\":\"https:\/\/www.syntera.ch\/blog\/author\/kail\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Migrating a Wordpress Blog to Django (Wagtail) - Syntera","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/","og_locale":"en_US","og_type":"article","og_title":"Migrating a Wordpress Blog to Django (Wagtail) - Syntera","og_description":"Problem Statement Our company website has been running on PHP + WordPress for years, but it\u2019s time to leave behind plugin roulette, limited extensibility, and the burden of maintaining a non-Python backend. We wanted a single&nbsp;Python&nbsp;codebase where we could: Django&nbsp;was the obvious framework home, but which CMS layer? We compared Mezzanine, Django\u2011CMS, plain Django admin [&hellip;]","og_url":"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/","og_site_name":"Syntera","article_published_time":"2025-04-30T11:09:07+00:00","og_image":[{"url":"https:\/\/www.syntera.ch\/blog\/wp-content\/uploads\/2025\/04\/wagtail.png","width":1024,"height":683,"type":"image\/png"}],"author":"Kail Kuhn Schlicht","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Kail Kuhn Schlicht","Est. reading time":"4 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/","url":"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/","name":"Migrating a Wordpress Blog to Django (Wagtail) - Syntera","isPartOf":{"@id":"https:\/\/www.syntera.ch\/blog\/#website"},"datePublished":"2025-04-30T11:09:07+00:00","dateModified":"2025-04-30T11:09:07+00:00","author":{"@id":"https:\/\/www.syntera.ch\/blog\/#\/schema\/person\/8048dfb9e4b2f044222a4baa11ee3e68"},"breadcrumb":{"@id":"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.syntera.ch\/blog\/2025\/04\/30\/migrating-a-wordpress-blog-to-django-wagtail\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.syntera.ch\/blog\/"},{"@type":"ListItem","position":2,"name":"Migrating a WordPress Blog to Django (Wagtail)"}]},{"@type":"WebSite","@id":"https:\/\/www.syntera.ch\/blog\/#website","url":"https:\/\/www.syntera.ch\/blog\/","name":"Syntera","description":"translating data into business value.","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.syntera.ch\/blog\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/www.syntera.ch\/blog\/#\/schema\/person\/8048dfb9e4b2f044222a4baa11ee3e68","name":"Kail Kuhn Schlicht","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.syntera.ch\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/4a88ceaa2d30e0c6f32856b2a56416e928a7b351e59466174227ac639abe522c?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/4a88ceaa2d30e0c6f32856b2a56416e928a7b351e59466174227ac639abe522c?s=96&d=mm&r=g","caption":"Kail Kuhn Schlicht"},"sameAs":["https:\/\/www.linkedin.com\/in\/kail-kuhn-schlicht-603584a3\/"],"url":"https:\/\/www.syntera.ch\/blog\/author\/kail\/"}]}},"_links":{"self":[{"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/posts\/920","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/users\/5"}],"replies":[{"embeddable":true,"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/comments?post=920"}],"version-history":[{"count":11,"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/posts\/920\/revisions"}],"predecessor-version":[{"id":936,"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/posts\/920\/revisions\/936"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/media\/931"}],"wp:attachment":[{"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/media?parent=920"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/categories?post=920"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.syntera.ch\/blog\/wp-json\/wp\/v2\/tags?post=920"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}