# `MishkaCmsMigration.Targets.BlogPost`
[🔗](https://github.com/mishka-group/mishka_cms_migration/blob/v0.0.1/lib/mishka_cms_migration/targets/blog_post.ex#L6)

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.

## Links to other posts

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"`).

# `error_return`

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

# `okey_return`

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

# `address`

```elixir
@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`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `config`

```elixir
@spec config() :: keyword()
```

# `config`

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

# `enable`

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

# `get`

```elixir
@spec get() :: keyword()
```

# `health_check`

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

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

# `on_dependency_error`

```elixir
@spec on_dependency_error(term()) :: term()
```

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

# `register`

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

# `restart`

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

# `start`

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

# `start_link`

# `stop`

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

# `unregister`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
