MishkaCmsMigration.Targets.BlogPost (Mishka CMS Migration v0.0.1)

Copy Markdown View Source

The "post" items of a source written as the Blog's posts (MishkaBlog.BlogPost), through the :create and :update an admin's own post form runs, on the run's site and as the admin who pressed Import: the admin needs the Blog's admin:dashboard:blog:post, and the Blog's own rules decide the rest, such as how a slug is written (its slugify), how its content is sanitized and its code blocks highlighted.

An item's title is the post's title, its "slug" the post's slug (its title for a draft WordPress left without one), its content pieces, converted, the post's content (MishkaCmsMigration.Source.Item.html/1), its "excerpt" the post's excerpt (an empty one is none), and "sticky" makes it featured. A post is in the language the item names (a site that runs Polylang or WPML names one), joined as one group with the posts that are its other editions, whatever order they are imported in, or, for an item that names none, in the language the site leads with (MishkaCmsMigration.Targets.Helpers.Editions). An item in a language the site does not publish in waits for an admin's decision, and so does one whose language a source that runs a language plugin does not say, in the dry run too.

Status and dates

Its "status" and "date_gmt" (UTC) decide the post's:

  • "publish" — published, published_at the date;
  • "future" — scheduled, scheduled_publish_at the date;
  • "draft", and any other — a draft, with neither.

The Blog has no post pending review, private or behind a password, so "pending", "private" and a post with a "password" (in its extra) are drafts, and the report says so for each (note/2: "draft_from_pending", "draft_from_private", "draft_from_password"), in the dry run too. When the post is created is when it is imported: the Blog's create sets it.

Its author

Its "author" is the blogger the same run, or an earlier one, wrote or connected for that user (MishkaCmsMigration.Targets.BlogBlogger), read from the item it was recorded as, and becomes the post's one author (its blogger_ids). An author with no blogger, one skipped for having no user of the site or never listed, leaves the post with none, and the import's report says so ("author_missing"); a dry run, which writes no blogger, does not.

Categories and tags

Its "categories" and "tags" are the Blog categories and tags the same run, or an earlier one, wrote for those ids (MishkaCmsMigration.Targets.BlogCollection, MishkaCmsMigration.Targets.BlogTag), read from the items they were recorded as; one that was never written is left out, and the post is still written. So is one an admin left out on the Items page (MishkaCmsMigration.MigrationItem's left_out): the post is written without it, and so without an author an admin left out. One whose record an admin archived in the CMS, which the Blog would refuse to link, holds the post ("waits_for: <kinds>") until the admin decides on it ("archived_in_cms", MishkaCmsMigration.Workers.Helpers.Decision): Restore it, and the post is written with it, or Leave it archived, and the post is written without it. One whose record an admin deleted for good holds it the same way ("deleted_in_cms"): Import it again, and the post is written with the new record, or Leave it out, and the post is written without it, as the last import that met it left it, "target_missing".

Its author, categories, tags, featured file and the files of its content are what it needs (needs/1): Import this item writes a post alone only once each of them has a CMS record, or was left out or skipped, and it waits otherwise, where a full run, which writes them first, writes it without the one that failed.

Its "links" name the links of its content that point at a post of the source, each by its href as written and the source's slug of the post it points at (the WordPress source finds them, MishkaCmsMigration.Sources.WordPress). Each such link points at that post's Blog address instead (address/3), its slug made as the Blog's create makes it (MishkaCmsMigration.Targets.Helpers.BySlug.slug/2) in the post's own language, and keeps its #fragment: http://old.example/2025/12/hello/#more becomes /blog/hello#more. It is the address the run writes the post at, so a post that links to one listed after it points at it too. Its "pages" name the links of its content that point at a page of the source, and each points at the path that page is written at instead, keeping its ?query and #fragment (MishkaCmsMigration.Targets.CmsPage.relinked/3): http://old.example/contact/ is /contact, or /contact-29 once an admin decided to import that page as new beside the site's own. Any other link is kept as it is: a category, a file, an outside site, a post or page the source no longer has, or a post an admin left out, found by the slug its item recorded (MishkaCmsMigration.MigrationItem's source_slug), which keeps its old address.

Its pictures and files

Its "featured_media" is the file the same run, or an earlier one, wrote to the CMS's Media for that id (MishkaCmsMigration.Targets.CmsMedia), and the file's address becomes the post's featured image (its featured_image_url). A post whose featured file was never written, such as an SVG or one that failed, is written with none, and the import's report says so ("featured_image_missing"); a dry run, which writes no file, does not.

Its "media" name the addresses of its content that are files of the source, and each src, href, video poster and srcset candidate of them points at the file's Media address instead (MishkaCmsMigration.Targets.CmsMedia.relinked/3), a size WordPress made of a file too: .../uploads/2026/11/hero-300x169.jpg is the address of the stored hero.jpg. The address of a file that was not written is kept as it is, and an srcset candidate of one is named in the report ("srcset_file_missing").

Its SEO

Its "seo", what Yoast SEO or Rank Math kept for it (the WordPress source reads it), is the post's seo_tags (MishkaCmsMigration.Targets.Helpers.SeoTags): its title, description, robots, share image, a canonical address on another site, and the social and Twitter title, description, image and card when they say something else than the page's own (see the helper). A share image that is the post's featured image is left out: the post's page then shows the core's share card, which draws the featured image with the post's title, where a stored og:image would replace the card. The WordPress REST API cannot tell Yoast's chosen image from its fallback to the featured image.

Its videos

A YouTube or Vimeo player its content frames (the WordPress source makes one of each embed) is allowed on the site before the post is written, in an import only, as the admin (MishkaCmsMigration.Targets.Helpers.Frames), and the report names them ("frames_allowed: <origins>"), in the dry run too. For an admin who may not change the site's settings the post waits for an admin's decision ("player_not_allowed"), in the dry run too (waiting/2), also a post an earlier import wrote: Wait for an admin keeps it for a run by one who may, and Import without the player writes it with that player left out ("player_left_out"). The Blog keeps an <iframe>'s address, title and size, and leaves out its other attributes, such as allowfullscreen.

Its scripts

A post whose content holds a <script> or an inline event handler (onclick, onerror, ...) waits for an admin's decision ("scripts: <found>", MishkaCmsMigration.Targets.Helpers.Scripts), in the dry run too: Remove them writes it without them ("scripts_removed: <found>"), and Keep them (I trust this content) hands it to the Blog as it is ("scripts_kept: <found>"), whose own sanitizer still decides what a post keeps.

Its Elementor layout

A post laid out with Elementor arrives as the Blog's HTML (MishkaCmsMigration.Sources.WordPress.Elementor). One with widgets the CMS has no equivalent for, such as a form, a slider or a map, waits for an admin's decision ("unsupported_widgets: <names>", MishkaCmsMigration.Targets.Helpers.Widgets), in the dry run too (waiting/2), also a post an earlier import wrote: Import with the fallback HTML for those parts writes it with what Elementor drew for them ("fallback_html: <names>", and "no_fallback: <names>" for those it drew nothing for), and Leave the page out leaves it out.

Defined only where Mishka Blog is loaded, and supervised by MishkaCmsMigration.Application only then; on a node without the Blog its items wait for it ("blog_missing").

Summary

Functions

Where the Blog serves a post with slug in language on the site site_id: the path of its post page (MishkaBlog.Provision.item_paths/0) as the site's own address (MishkaCmsCore.Runtime.Languages.url/3), /blog/<slug> in the language the site leaves unprefixed and /fa/blog/<slug> in Persian. nil for no slug, and for a language the site does not publish in.

Returns a specification to start this module under a supervisor.

Callback implementation for MishkaInstaller.Event.Hook.enable/0.

Callback implementation for MishkaInstaller.Event.Hook.get/0.

Optional. See MishkaInstaller.Event.Hook.health_check/0. Override to report health.

Callback implementation for MishkaInstaller.Event.Hook.register/0.

Callback implementation for MishkaInstaller.Event.Hook.restart/0.

Callback implementation for MishkaInstaller.Event.Hook.start/0.

Callback implementation for MishkaInstaller.Event.Hook.stop/0.

Types

error_return()

@type error_return() ::
  {:error, [%{action: atom(), field: atom(), message: String.t()}]}

okey_return()

@type okey_return() :: {:ok, struct() | map() | module() | [any()]}

Functions

address(slug, language, site_id)

@spec address(String.t() | nil, String.t(), String.t()) :: String.t() | nil

Where the Blog serves a post with slug in language on the site site_id: the path of its post page (MishkaBlog.Provision.item_paths/0) as the site's own address (MishkaCmsCore.Runtime.Languages.url/3), /blog/<slug> in the language the site leaves unprefixed and /fa/blog/<slug> in Persian. nil for no slug, and for a language the site does not publish in.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

config()

@spec config() :: keyword()

config(key)

@spec config(atom()) :: any()

enable()

@spec enable() :: okey_return() | error_return()

Callback implementation for MishkaInstaller.Event.Hook.enable/0.

get()

@spec get() :: keyword()

Callback implementation for MishkaInstaller.Event.Hook.get/0.

health_check()

@spec health_check() :: :ok | {:degraded, term()} | {:error, term()}

Optional. See MishkaInstaller.Event.Hook.health_check/0. Override to report health.

on_dependency_error(error)

@spec on_dependency_error(term()) :: term()

Optional. See MishkaInstaller.Event.Hook.on_dependency_error/1. Override to react.

register()

@spec register() :: okey_return() | error_return()

Callback implementation for MishkaInstaller.Event.Hook.register/0.

restart()

@spec restart() :: okey_return() | error_return()

Callback implementation for MishkaInstaller.Event.Hook.restart/0.

start()

@spec start() :: okey_return() | error_return()

Callback implementation for MishkaInstaller.Event.Hook.start/0.

start_link(args \\ [])

stop()

@spec stop() :: okey_return() | error_return()

Callback implementation for MishkaInstaller.Event.Hook.stop/0.

unregister()

@spec unregister() :: okey_return() | error_return()

Callback implementation for MishkaInstaller.Event.Hook.unregister/0.