Raus aus WordPress, rein ins Repository: @11ty/import in der Praxis

Ein Befehl holt einen kompletten WordPress-Blog als Markdown ins eigene Repository — Mastodon, Bluesky und YouTube gleich mit. Was @11ty/import 2.0.1 wirklich leistet, an sechs Quellen nachgemessen, samt der Stelle, an der es kippt.

von ·

Im Artikel über sechs Jahre 11ty habe ich gestern einen Satz geschrieben, der zu glatt war: „Im Juni 2020 habe ich den Schnitt gemacht." Das klingt nach einem Nachmittag. Es war keiner.

Was in dem Satz fehlt, ist die Arbeit dahinter. Inhalte aus einem CMS herauszubekommen, ist der unromantische Teil jeder Migration — und der, an dem die meisten Umzüge scheitern, lange bevor jemand die erste Zeile Template schreibt. Ich habe damals exportiert, konvertiert, nachgebessert und Bilder von Hand einsortiert. Es gab kein Werkzeug, das mir das abgenommen hätte.

Heute gibt es eins, und es kommt aus dem 11ty-Projekt selbst: @11ty/import. Für mich kam es vier Jahre zu spät. Für dich kommt es vielleicht genau richtig — vor sechs Tagen, am 17. August 2026, ist Version 2.0.1 erschienen.

Ich habe es nicht gelesen, sondern laufen lassen: alle sechs Quellen, echte Ziele, gemessene Ergebnisse. Drei davon liefern sauberes Markdown. Zwei liefern etwas, das nach Markdown aussieht und keines ist. Beides steht in diesem Artikel, mit den Befehlen zum Nachmachen.

Was das Werkzeug ist

Die eigene Beschreibung ist knapp:

„A small utility (and CLI) to import content files from various content sources. Requires Node 20.19 or newer."

Mehr ist es tatsächlich nicht, und das ist die Stärke. Es gibt nichts zu installieren, nichts zu konfigurieren, keine Konfigurationsdatei, keinen Eleventy-Kontext. Ein Aufruf, zwei Argumente:

npx @11ty/import [type] [target]

type ist die Quellenart, target das, was geholt werden soll — eine URL, ein Benutzername oder eine Kanal-ID. Wohin geschrieben wird, entscheidet erst --output. Sechs Quellen sind eingebaut, und die Beispiele stammen unverändert aus der Projekt-README:

npx @11ty/import wordpress https://blog.fontawesome.com
npx @11ty/import atom https://www.11ty.dev/blog/feed.xml
npx @11ty/import rss https://fosstodon.org/users/eleventy.rss
npx @11ty/import fediverse eleventy@fosstodon.org
npx @11ty/import bluesky @11ty.dev
npx @11ty/import youtubeuser UCskGTioqrMBcw8pd14_334A

Was dabei herauskommt, sind Dateien in deinem Verzeichnis: Markdown mit YAML-Frontmatter, daneben die heruntergeladenen Bilder. Keine Datenbank, kein Zwischenformat, kein Dienst. Das Projekt beschreibt vier Eigenschaften, die im Alltag zählen:

„Clean: Converts imported content to markdown files in your repository […] Standalone: downloads all referenced assets (images, videos, stylesheets, scripts, etc) in content and co-locates assets with the content […] Resumable: Can stop and resume a large import later, reusing a local cache […] Repeatable: avoids overwriting existing content files (unless you opt-in with --overwrite)."

Vor allem der letzte Punkt ist mehr wert, als er klingt: Der Import ist kein einmaliger Knall, sondern ein Befehl, den du wiederholen kannst. Was du nach dem ersten Durchlauf im Editor überarbeitet hast, bleibt beim zweiten unangetastet — es kommen nur neue Beiträge dazu. Damit ist der Import kein Stichtag mehr, sondern eine Brücke, über die du gehen kannst, solange du willst.

Das Paket ist MIT-lizenziert, stammt von Zach Leatherman, liegt unter 11ty/import (öffnet in neuem Tab) und hat heute 125 Sterne. Merk dir die Zahl — sie kommt im Abschnitt über die Kehrseite noch einmal vor.

Der WordPress-Fall

Das ist der Grund, warum es das Werkzeug gibt. Die README nennt ihn als erstes Anwendungsbeispiel, und das Begleitvideo des Projekts heißt „Start Your Escape from WordPress Using 11ty (in 3 minutes!)".

Also habe ich es gemacht. Zielobjekt: wordpress.org/news, der offizielle Nachrichtenblog von WordPress. Ein echter WordPress-Betrieb, öffentlich, groß genug, um ehrlich zu sein.

npx @11ty/import wordpress https://wordpress.org/news/ --within=45d --output=run

Das Ergebnis, unverändert aus meinem Terminal:

Wrote 11 documents and 100 assets (450 cleaned, unused) from WordPress (1 error) in 40.07 seconds (v2.0.1)

Elf Beiträge, hundert Bilder, 40 Sekunden, 106 MB auf der Platte. Die 450 „cleaned" sind der interessante Teil: WordPress liefert zu jedem Bild ein halbes Dutzend srcset-Größen, das Werkzeug lädt sie zunächst alle herunter und wirft danach weg, was in der Markdown-Fassung nicht mehr vorkommt. Der Kommentar in MarkdownToHtml.js sagt es trocken: „Removes unnecessarily downloaded and srcset assets that didn’t end up in the markdown simplification". Du bekommst also nicht die Bildvarianten von deren Theme, sondern die Originale — und darfst deine eigenen Größen daraus rechnen. Genau richtig.

Und so sieht eine importierte Datei aus, Kopf unverändert:

---
title: WordPress 7.0.4 Release
authors:
  - name: John Blackbourn
    url: https://johnblackbourn.com
    avatarUrl: >-
      https://secure.gravatar.com/avatar/51f55d…
date: 2026-08-12T12:45:53.000Z
metadata:
  categories:
    - Releases
    - Security
  tags:
    - minor-releases
    - releases
  uuid: 11ty/import::wordpress::https://wordpress.org/news/?p=21371
  type: wordpress
  url: https://wordpress.org/news/2026/08/wordpress-7-0-4-release/
tags:
  - releases
  - security
---

Darunter steht sauberes Markdown mit Überschriften, Listen, Links und relativen Bildpfaden. Kein <div>-Salat, keine Shortcode-Reste, keine Gutenberg-Kommentare. Bemerkenswert ist die Doppelung bei den Tags, und sie geht anders herum, als du vermutest: metadata.categories und metadata.tags halten die Originalbezeichnungen der WordPress-Kategorien und -Schlagwörter fest. Das Frontmatter-tags, das 11ty als Collection-Tag liest, entsteht ausschließlich aus den Kategorien — slugifiziert und kleingeschrieben. Die WordPress-Schlagwörter bleiben allein in metadata. Im Beispiel oben siehst du es direkt: Aus den Kategorien „Releases" und „Security" werden die Tags releases und security, während metadata.tags mit minor-releases gar nicht nach oben durchschlägt. Der Quelltext kommentiert die Stelle ausdrücklich mit „map WordPress categories for use in Eleventy tags (not WordPress metadata tags, which are different)". Wenn du deine Schlagwörter behalten willst, musst du sie selbst aus metadata nach oben ziehen.

Der Ordner spiegelt die Pfadstruktur der Quelle: run/news/2026/08/wordpress-7-0-4-release.md, daneben ein assets/-Verzeichnis pro Monat. Wer seine Permalinks behalten will, hat damit schon die halbe Arbeit erledigt.

Zwei Stolpersteine, die du nicht raten solltest

Der Schrägstrich am Ende ist Pflicht. Mein erster Versuch war https://wordpress.org/news ohne Slash, und der Abbruch war unfreundlich:

Error: Bad response for https://wordpress.org/newswp-json/wp/v2/posts/ (404): Not Found

Die URL wird ohne Trennzeichen zusammengeklebt. Ein Zeichen zu wenig, und der Fehler zeigt auf einen 404 statt auf die Ursache. Schreib die Blog-Adresse also immer mit abschließendem /.

Das Beispiel aus der README funktioniert nicht mehr. npx @11ty/import wordpress https://blog.fontawesome.com bricht heute mit 403: Forbidden ab. Ich habe gegengeprüft, ob das an meinem Aufruf liegt: Auch ein normaler Browser-Aufruf auf blog.fontawesome.com/wp-json/wp/v2/posts/ liefert 403. Font Awesome hat seine REST-Schnittstelle also dichtgemacht — ausgerechnet der Blog, den das eigene Beispiel importieren wollte. Das ist kein Fehler des Werkzeugs, aber eine gute Lehre: Ob ein Import überhaupt möglich ist, entscheidet die Zielseite, nicht du.

Warum die REST-Schnittstelle und nicht der XML-Export

WordPress hat einen eigenen Exportweg: Werkzeuge → Daten exportieren erzeugt eine XML-Datei im Format WXR („WordPress eXtended RSS"), die laut Dokumentation „posts, pages, custom post types, comments, custom fields, categories, tags, custom taxonomies, and users" enthält. Klingt vollständiger — ist es formal auch.

@11ty/import geht trotzdem den anderen Weg und fragt wp-json/wp/v2/posts ab. Der Grund ist praktisch: Im WXR steht der Beitragstext so, wie er in der Datenbank liegt — inklusive Shortcodes, Block-Kommentaren und allem, was Plugins hineingeschrieben haben. Die REST-Schnittstelle liefert content.rendered, also das, was der Besucher tatsächlich sieht. Für eine Konvertierung nach Markdown ist das die deutlich bessere Ausgangslage, weil die Shortcodes schon aufgelöst sind.

Der Preis: Du brauchst eine erreichbare Schnittstelle. Sie ist im Standard aktiv — WordPress registriert sie fest im Kern, und das Handbuch ist in dem Punkt ungewöhnlich deutlich:

„You should not disable the REST API; doing so will break WordPress Admin functionality that depends on the API being active."

Trotzdem tun es Sicherheits-Plugins und Server-Regeln regelmäßig, wie der Font-Awesome-Fall zeigt. Prüf das als Allererstes, bevor du einen Migrationstermin zusagst: Ruf https://deine-seite.tld/wp-json/wp/v2/posts?per_page=1 im Browser auf. Kommt JSON, kannst du loslegen. Kommt 401, 403 oder eine leere Seite, ist das dein eigentliches erstes Ticket.

Was nicht mitkommt

Hier bin ich dir eine klare Liste schuldig, weil die README keine gibt:

  • Seiten nicht. Die Quelle fragt ausschließlich posts ab. Dass Pages und eigene Post-Types fehlen, ist ein offener Wunsch im Projekt (Issue #17 (öffnet in neuem Tab), seit Oktober 2025).
  • Das Beitragsbild nur, wenn die Quelle es ausliefert. Anders als ich erwartet hatte, wird es durchaus geholt: Das Werkzeug liest jetpack_featured_media_url (ersatzweise og_image), lädt die Datei herunter und hinterlegt sie als metadata.media.featuredImage. Fehlt das Feld — auf jeder Installation ohne Jetpack —, bleibt das Beitragsbild liegen. Das zugehörige Issue (#11 (öffnet in neuem Tab)) ist trotzdem offen, weil der Beitragsauszug weiterhin fehlt.
  • Kommentare, Menüs, Widgets, Weiterleitungen nicht. Das Werkzeug importiert Inhalte, keine Website.
  • Entwürfe nur mit Zugangsdaten. Für Drafts brauchst du WORDPRESS_USERNAME und WORDPRESS_PASSWORD als Umgebungsvariablen; sie gehen als HTTP-Basic-Auth an die Schnittstelle. Nimm dafür ein Anwendungspasswort, nicht dein Adminpasswort.
  • Bei WordPress.com ist es intern eine andere Quelle. Gehostete Blogs laufen über einen eigenen Typ mit der public-api.wordpress.com-Schnittstelle, und dort steht im Quelltext unmissverständlich „DRAFTS NOT SUPPORTED". Umstellen musst du dafür nichts: Der wordpress-Typ erkennt eine .wordpress.com-Adresse selbst und schaltet stillschweigend um. Bequem — nur warnt dich dabei niemand, dass Entwürfe auf diesem Weg gar nicht erst versucht werden.

Sechs Quellen, gemessen

Jetzt der Teil, für den ich die Befehle wirklich habe laufen lassen. Alle Läufe mit Version 2.0.1 unter Node 24.19, Standardeinstellungen — nur bei WordPress habe ich mit --within=45d den Zeitraum begrenzt, damit ich einen fremden Server nicht ohne Not durchpflüge.

Quelle Ziel Ergebnis Sauber?
wordpress wordpress.org/news/ 11 .md, 100 Assets, 450 verworfen, 40 s ja
bluesky @11ty.dev 19 .md, 0 Assets, 1,3 s ja
youtubeuser UCskGTioqrMBcw8pd14_334A 15 .md, 0 Assets, 0,5 s ja
atom 11ty.dev/blog/feed.xml 10 .md, 0 Assets, 0,9 s nein
fediverse eleventy@fosstodon.org 20 .md, 2 Assets, 0,4 s nein
rss fosstodon.org/users/eleventy.rss 20 .html statt .md, 0,3 s nein

Die drei sauberen Fälle sind wirklich sauber. Bluesky liefert die Beiträge als schlichten Text mit ordentlichem Frontmatter, YouTube die Videobeschreibungen samt Kanalnamen — der bei 11ty inzwischen „Build Awesome (11ty)" lautet, was die Namensfrage aus dem letzten Artikel beiläufig bestätigt.

Die drei anderen Fälle brauchen einen eigenen Abschnitt.

Die Stelle, an der es kippt

Der Atom-Lauf meldet Erfolg. Zehn Dokumente, null Fehler, 868 Millisekunden. Erst wenn du eine der Dateien aufmachst, siehst du das hier — in Wirklichkeit eine einzige sehr lange Zeile, hier umbrochen und gekürzt:

\<p>As we harden our release practices in the wake of numerous recent
vulnerabilities in npm packages amongst high profile authors…\</p>

\<h2 id="dependency-watch">Dependency Watch\</h2>

Das ist kein Markdown. Das sind HTML-Tags, die als Text behandelt und dann noch escaped wurden. Ich habe nachgezählt: 10 von 10 Dateien aus dem Atom-Lauf betroffen, 20 von 20 aus dem Fediverse-Lauf, 0 von 11 bei WordPress. Der RSS-Lauf schreibt gar keine .md-Dateien, sondern .html — mit rohen &lt;p&gt;-Entities darin, also doppelt unbrauchbar.

Wichtig für die Einordnung: Das ist keine neue Regression. Ich habe denselben Feed mit der Vorgängerversion 1.0.24 importiert — identisches Ergebnis. Das Verhalten ist alt, es fällt nur niemandem auf, weil der Lauf „0 errors" meldet.

Die Ursache steht im Quelltext von Importer.js und ist in wenigen Zeilen erzählt — hier unverändert, samt der Einrückung des Originals:

if(Importer.isHtml(entry)) {
	let transformedHtml = content;
	if(!isWritingToMarkdown) {
		// decoding built-in with Markdown
		transformedHtml = entities.decodeHTML(content);
	}
	// …

Der Kommentar ist die Annahme, und die Annahme stimmt nicht. Ein Atom-Feed darf HTML auf zwei Arten transportieren: als type="xhtml" mit echtem Markup oder als type="html" entity-kodiert — &lt;p&gt; statt <p>. Praktisch alle Feeds nehmen die zweite Variante, der von 11ty auch. Beim HTML-Ausgabeweg wird das dekodiert. Beim Markdown-Ausgabeweg wird es übersprungen, weil der Konverter das angeblich selbst erledigt. Tut er nicht: Er sieht &lt;p&gt;, dekodiert die Entity zu einem literalen <, hält es für Text und escaped es zu \<. Deshalb bleiben auch die Bilder liegen — wo kein <img> erkannt wird, gibt es nichts herunterzuladen. Genau das erklärt die „0 Assets" in der Tabelle oben.

Der Umweg, der funktioniert, ist ein einziges Flag:

npx @11ty/import atom https://www.11ty.dev/blog/feed.xml --format=html

Gemessen: 10 saubere HTML-Dateien mit korrektem Markup — und 25 Assets, die der Markdown-Weg nie gesehen hat. Wenn du aus einem Feed importierst, nimm also HTML und konvertiere selbst, statt dem Standardweg zu vertrauen.

Ein offenes Issue dazu habe ich im Projekt nicht gefunden. Wer das Werkzeug ernsthaft für Feeds nutzt, sollte eins aufmachen — mit dem Feed, dem Befehl und den zwei Zeilen Ausgabe ist es in fünf Minuten geschrieben, und der Fall ist reproduzierbar. Für Mastodon gibt es außerdem einen halben Ausweg: fediverse statt rss nimmt die richtige Schnittstelle, lädt Bilder korrekt herunter und ist auch sonst der bessere Weg — nur an der Escaping-Stelle scheitert es genauso.

Ich schreibe das nicht, um ein kleines Projekt vorzuführen. Ich schreibe es, weil eine Migration eine dieser Sachen ist, bei denen du das Ergebnis erst Monate später ansiehst. „0 errors" ist keine Qualitätsaussage. Öffne nach jedem Import drei zufällige Dateien und lies sie. Das ist die ganze Prüfung, und sie hätte hier in zehn Sekunden gegriffen.

Die Flags, die den Unterschied machen

npx @11ty/import --help zeigt zehn Optionen. Verarbeitet werden dreizehn. Die, die im Alltag zählen:

  • --dryrun — schreibt nichts, zeigt alles. Die 11ty-Dokumentation schreibt dazu „Try it out with --dryrun first to be safe!", und ausnahmsweise ist das kein Pflichtsatz, sondern der wichtigste Rat des ganzen Themas.
  • --output=verzeichnis — Standard ist ., also dein aktuelles Verzeichnis. Wer das übersieht, verteilt einen fremden Blog über sein Projektwurzelverzeichnis.
  • --within=45d — begrenzt auf kürzlich erstellte oder geänderte Beiträge. Bei WordPress wird daraus serverseitig ein after/modified_after-Parameter, das spart also echte Anfragen und nicht nur Nachbearbeitung. Ideal für den Probelauf.
  • --assetrefs= — vier Modi, und die Wahl hängt an deinem Projekt: relative (Standard, slug.md neben assets/bild.png), absolute (/assets/bild.png), colocate (slug/index.md mit den Bildern im selben Ordner) und disabled (gar keine Downloads). Für einen ersten Blick ist disabled Gold wert: Der Lauf dauert Sekunden statt Minuten, und du siehst trotzdem, ob der Text taugt.
  • --overwrite und --overwrite-allow=drafts — Ersteres hebelt den Schutz aus, Letzteres nur für Entwürfe. Die zweite Variante ist die klügere: Entwürfe dürfen sich ändern, veröffentlichte und von dir überarbeitete Beiträge nicht.
  • --cacheduration=24h — der lokale Zwischenspeicher, aus dem ein abgebrochener Lauf weitermacht. Bei einem großen Blog ist das der Unterschied zwischen „nochmal von vorn" und „weiter".
  • --format=markdown|html — siehe oben. Bei Feeds die Rettung.

Drei weitere Flags existieren, stehen aber nicht in der Hilfe: --within (immerhin in der README), --persist (experimentell, veröffentlicht neue Beiträge direkt auf GitHub und braucht ein GITHUB_TOKEN) und --preserve, das weder in der Hilfe noch in der README-Übersicht auftaucht. Es nimmt Klassennamen und schützt die so markierten Elemente vor der Markdown-Vereinfachung. Wenn dein alter Blog Callout-Boxen oder eingebettete Karten hatte, die als HTML überleben sollen, ist das genau das Werkzeug dafür — du musst nur wissen, dass es existiert.

Über den Tellerrand: jede Website als CMS

Bis hierher war der Artikel eine Migrationsgeschichte. Der eigentlich interessante Satz der README ist ein anderer:

„Make anything on the web into a CMS for your web site using Indieweb PESOS."

PESOS steht für „Publish Elsewhere, Syndicate (to your) Own Site" und ist eine Idee aus der IndieWeb-Bewegung: Du postest weiterhin dort, wo die Leute sind — und holst dir die Inhalte anschließend automatisch in dein eigenes Repository zurück. Nicht als Einbettung, die von einem fremden Dienst lebt, sondern als Datei, die dir gehört.

Der Unterschied ist keine Feinheit. Eine eingebettete Zeitleiste ist ein Skript von einem fremden Server: langsam, oft ein Datenschutzproblem, und am Tag, an dem der Dienst abschaltet oder die Bedingungen ändert, ist deine Seite lückenhaft. Die letzten Jahre haben mehrfach vorgeführt, wie schnell aus einer Plattform ein Risiko wird. Eine importierte Markdown-Datei ändert sich nie. Sie ist im Git, sie ist im Backup, sie wird mit ausgeliefert und mit indexiert.

Drei Anwendungen, die mir dabei einfallen und die alle mit einem Befehl anfangen:

# Eigene Beiträge aus dem Fediverse als Notizen-Bereich
npx @11ty/import fediverse deinname@deine-instanz.tld --output=src/content/notizen

# Releases eines GitHub-Projekts als eigener Changelog
npx @11ty/import atom https://github.com/11ty/eleventy/releases.atom --format=html

# Konferenzvideos als durchsuchbarer Index
npx @11ty/import youtubeuser DEINE_KANAL_ID --output=src/content/videos

Der mittlere Fall gefällt mir besonders, weil er zeigt, wie weit die Idee trägt: GitHub liefert die Releases jedes öffentlichen Projekts als Atom-Feed. Ein Cron-Job, ein Import, ein Build — und du hast eine gepflegte Changelog-Seite, die sich selbst nachträgt. In dieser Knowledge Base gibt es einen Changelog-Bereich, den ich bislang aus den README-Dateien meiner Projekte spiegle. Der Feed-Weg wäre die automatisierte Variante desselben Gedankens — und die Einschränkung von oben gilt: mit --format=html, nicht mit dem Standardweg.

Und weil das der Punkt ist, an dem ich enthusiastisch werde: Diese Bauform ist der Gegenentwurf zu allem, was das Web in den letzten fünfzehn Jahren mit Inhalten gemacht hat. Nicht „deine Beiträge liegen bei uns, und wir zeigen sie freundlicherweise an", sondern „deine Beiträge liegen bei dir, und wir dürfen sie auch anzeigen". Ein Werkzeug, das in einem einzigen Befehl den Rückweg öffnet, ist mehr als eine Bequemlichkeit. Es ist die Rückgängig-Taste, von der jede Plattform hofft, dass du sie nicht findest.

Die ehrliche Kehrseite

Ein Artikel, der ein Werkzeug feiert und keine Grenzen benennt, ist Werbung. Also:

Es ist ein Node-Programm, und damit gilt alles, was für Node gilt. npx @11ty/import holt sich beim ersten Aufruf 53 Pakete auf deinen Rechner und führt sie aus. Das ist derselbe Vektor, über den die npm-Vorfälle der letzten Jahre funktioniert haben, und er wird nicht harmloser, weil ein Werkzeug nützlich ist. Bei mir läuft deshalb auch dieser Aufruf über Socket Firewall — jeder Paket-Fetch wird vorher geprüft, und Install-Hooks sind projektweit aus.

Zur Ehrenrettung des Projekts: Die eigene Hausaufgabe ist gemacht. Die 2.0.1 wurde über npm Trusted Publishers veröffentlicht, mit SLSA-Provenance-Attestierung und ohne langlebige Zugangstoken. Das ist mehr Sorgfalt, als die meisten deutlich größeren Pakete aufbringen.

Es ist ein kleines Projekt. 125 Sterne, ein Maintainer, 14 offene Issues. Das ist keine Kritik, sondern eine Größenordnung, die du bei der Planung kennen solltest: Wenn du auf einen Fehler stößt, wirst du ihn wahrscheinlich selbst umschiffen müssen. Wie ordentlich das Projekt trotzdem geführt wird, zeigt eine hübsche Kleinigkeit: Version 2.0.0 gibt es auf npm gar nicht. Der automatische Veröffentlichungslauf ist gescheitert, der GitHub-Release trägt deshalb einen durchgestrichenen Titel und im Text nur den Satz „v2.0.0 failed an automated immutable publish, so please use Import v2.0.1." Das ist bereits das dritte Mal, dass eine Version an dieser Stelle hängen blieb. Mir ist ein Projekt, das solche Pannen offen dokumentiert, lieber als eines, in dem sie stillschweigend verschwinden.

Es kennt deinen pathPrefix nicht. Im Quelltext steht über der Schreibfunktion ein nacktes // TODO options.pathPrefix. Für diese Knowledge Base, die unter /db/ liegt, hieße das: --assetrefs=absolute erzeugt /assets/… ohne Präfix, und ich müsste hinterher suchen und ersetzen. Mit relative oder colocate stellt sich die Frage nicht — noch ein Grund, die Standardeinstellung nicht leichtfertig zu ändern.

Das Frontmatter ist generisch, dein Schema ist es nicht. Was ankommt, ist title, authors, date, metadata, tags. Was diese Seite hier braucht, ist zusätzlich translationKey, kind, description und Tags aus einem kontrollierten Vokabular. Diese Zuordnung nimmt dir kein Importer ab. Rechne mit einem kleinen Skript, das nach dem Import über die Dateien läuft — und schreib es, bevor du zweitausend Beiträge holst, nicht danach.

Und Fehler werden gezählt, nicht erklärt. Mein WordPress-Lauf endete mit „(1 error)". Welcher, steht nirgends. Bei elf Beiträgen finde ich das noch selbst. Bei elfhundert nicht.

Mein Rezept

Wenn ich morgen einen WordPress-Blog umziehen müsste, würde ich in dieser Reihenfolge vorgehen:

  1. Schnittstelle prüfen. wp-json/wp/v2/posts?per_page=1 im Browser aufrufen. Kein JSON, kein Import — dann ist das die erste Aufgabe, nicht der Umzug.
  2. Trockenlauf. --dryrun --within=30d, um zu sehen, was überhaupt gefunden wird und wie die Dateinamen aussehen werden.
  3. Kleiner echter Lauf ohne Bilder. --within=30d --assetrefs=disabled --output=/tmp/probe. Dauert Sekunden. Dann drei Dateien öffnen und lesen — Tabellen, Codeblöcke, Fußnoten, Zitate. Das ist die Stelle, an der du Escaping-Probleme siehst, solange sie noch nichts kosten.
  4. Erst dann der vollständige Lauf, mit Bildern, in ein leeres Verzeichnis, mit ausreichender --cacheduration.
  5. Danach das Zuordnungsskript für dein eigenes Frontmatter — und den Import ruhig weiterlaufen lassen. Er überschreibt nichts, was du angefasst hast.

Fazit

@11ty/import löst genau ein Problem, und es löst es gut: Es holt Inhalte aus fremden Systemen als Dateien zu dir. Für WordPress funktioniert das heute überzeugend, für Bluesky und YouTube ebenfalls, für Feeds nur mit dem Umweg über --format=html. Es importiert Beiträge, keine Websites — Seiten, Beitragsbilder, Menüs und Weiterleitungen bleiben deine Aufgabe.

Was mich daran wirklich überzeugt, ist die Haltung dahinter. Ein Projekt, das aktiv daran arbeitet, dass Inhalte hereinkommen, denkt nicht in Lock-in. Es passt zu dem Satz, den ich gestern schon zitiert habe — „We don't want to hold your content hostage with a custom format." Ein Werkzeug, das den Weg zu dir öffnet, ist von einem, das den Weg von dir weg offenhält, nur einen Gedanken entfernt.

Bleibt die Frage, die der Import nicht beantwortet, und sie ist die schwierigere: Wenn die Inhalte erst einmal als Markdown im Repository liegen — wer pflegt sie dann? Für mich ist die Antwort ein Editor und ein Commit. Für eine Redaktion, die nie ein Terminal geöffnet hat, ist sie es nicht. Das ist die Lücke, die ich gestern selbst als Grenze von 11ty benannt habe, und darum geht es im nächsten Artikel.

Weiterlesen