Where this comes from
I wrote myself a content importer for WordPress – I drop in a file with ready HTML and the script turns it into a post: Polish and English at once, with SEO and categories set. Convenient, because I write the content elsewhere and importing is a single click.
Sounds simple. And it worked – until it started doing strange things. The post looked fine in the editor but was empty on the site. Polish characters turned into garbage. And once the English version overwrote the Polish one, leaving a single post instead of two.
Each of these three things is a separate trap – and each has a concrete cause. Below I go through them one by one, because if you ever push raw HTML into WordPress (via an importer, a script, the API), sooner or later you’ll hit one of them.
Trap 1: content shows in the editor but the front is empty
The most misleading symptom. You open the post in the dashboard – the content is all there. You visit the site – a white blank under the title. No text.
The cause: the block editor (Gutenberg) expects content wrapped in block comments, like <!-- wp:paragraph -->. When you drop in raw HTML without those comments, the editor treats it as “unexpected content” – it shows it in a recovery block, so you see it in the dashboard. But on the front, the_content() runs the content through the block parser, and with markerless HTML that parser can return nothing.
The fix: wrap the content in a single wp:html block. It’s Gutenberg’s official block for arbitrary HTML – it renders one to one, loses nothing.
// Before saving: if the content has no block markers at all,
// wrap it in a wp:html block - otherwise the front shows blank.
if ( strpos( $html, '<!-- wp:' ) === false ) {
$html = "<!-- wp:html -->\n" . $html . "\n<!-- /wp:html -->";
}
The strpos check matters: it verifies the content doesn’t already have blocks. That way I don’t double-wrap something that already has them – and I don’t break other post types that render differently.
Trap 2: garbage instead of accented characters
The content showed up, but instead of “treści” you see “treÅci”, instead of “Cześć” there’s “CzeÅÄ”. A classic.
This isn’t a WordPress problem – it’s a problem with the file you’re importing from. The file is saved in one encoding and read as another. Most often: the content is UTF-8, but something along the way reads it as Windows-1250 (or the other way round). The bytes don’t match, so accented characters break into those characteristic double-glyphs.
How to spot it: if you see “Å”, “Ä”, “Ô where accents should be – it’s almost certainly UTF-8 treated as Windows-1250. That’s the signature of this particular mistake.
How to fix it: make sure the source file is saved as UTF-8 (without BOM). In VS Code the encoding shows in the bottom-right corner – click, “Save with encoding”, UTF-8. On Windows it’s easy to slip up, because some editors default to a legacy code page.
A quick test of what the file actually is, without opening it in an editor:
file file-name.md # the result should contain "UTF-8 text" # if it shows "ISO-8859" or "Non-ISO extended-ASCII" - wrong encoding
Don’t try to fix the garbage by hand, character by character. It’s a losing game and something always slips through. Fix the encoding at the source and import the file again.
Trap 3: one language version overwrites the other
This one was the sneakiest. My importer makes two versions of a post at once – Polish and English – and links them through Polylang. The import looks like it succeeded. And on the site there’s one post, not two. The Polish content is gone, the English one stayed.
The cause was in how the importer checked whether a post already existed. It looked by address (slug), but without telling languages apart. The sequence of events:
- It creates the Polish post – fine, a new one appears.
- It moves on to English. It checks whether that address already exists. And it finds one – the Polish post, because the addresses were too similar.
- It decides “already exists, I’ll update it” – and overwrites the Polish post with English content.
Result: one post under one ID, with English content, and the Polish one lost.
The fix: when looking for an existing post, discard a hit that belongs to another language. If the found post is Polish and I’m importing English – that’s not the post, treat it as “doesn’t exist” and create a new one.
// After finding a post by slug: discard it if it's in another language.
// Without this, the EN import would overwrite the PL post (same, too-similar slug).
if ( $id && $lang && function_exists( 'pll_get_post_language' ) ) {
$found_lang = pll_get_post_language( $id );
if ( $found_lang && $found_lang !== $lang ) {
return 0; // another language = not this post, let a new one be created
}
}
On top of that, a second thing – this time not in the code but in the data: the addresses of both versions have to differ. “cattle-farm” in English and “hodowla-bydla” in Polish – different, that’s good. The same address for both languages is asking for trouble, no matter how clever the importer is.
What I learned
All three traps share a common thread: WordPress and Gutenberg have their assumptions about how content looks and where it comes from. When you push data in sideways – as raw HTML, from an external file, in two languages at once – those assumptions can bite you in a place you didn’t expect.
So when importing content I now always check three things before I call it done: whether the front shows the same as the editor, whether the accented characters are intact, and whether each language version is a separate post. Three glances, a minute of work – and they save an hour of wondering where the content went.
One more word on security
An importer like this runs in the admin panel, so its call is worth restricting to logged-in users with the right capability (current_user_can) and protecting the form with a nonce – in my case that’s check_admin_referer. If I write the content myself, from my own files, that’s enough. If the importer ever had to accept HTML from the outside (from someone else), one more thing would be needed: running the content through wp_kses_post to strip dangerous scripts. I deliberately skip that, because wp_kses also strips things my posts need (base64 images, backslashes in paths) – but that’s a decision for a trusted source of my own, not someone else’s.



