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

The `"page"` items of a source written as the CMS's own pages (`MishkaCmsCore.Runtime.Page`),
through the `:create` and `:update` an admin's own page form runs, on the run's site and as the
admin who pressed Import: the admin needs the core's `admin:dashboard:runtime:page`, and the
core's own rules decide the rest, such as that a path is unique within a site and that a page's
template must compile.

An item's title is the page's title, its content pieces, converted, the page's template (below),
and the page is in the language the item names (a site that runs Polylang or WPML names one),
joined as one group with the pages 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. It is
drawn in the site's `site-main` layout (`MishkaCmsCore.Runtime.SiteLayout.layout_id/1`), between
the header and the footer every page of the site shares, as the pages an extension provisions are.
The core names the page after its title, as it does for a page typed in the form.

## Its path

The page's path is the path of its address at the source, without the host and the trailing `/`:
`http://old.example/about/team/` is `/about/team`, so a page under its parent keeps the nested
path and needs nothing from the parent's record. A page that has no address of its own yet, such
as a draft whose `"link"` is `/?page_id=30`, takes the one WordPress would give it, from the
`"permalink_template"` it answers (`MishkaCmsMigration.Sources.WordPress` keeps it in `extra`)
with the page's slug in it. The core serves only ASCII paths, so a segment that is not made of
letters, digits and `- _ .` (a slug written in another script, which WordPress keeps
percent-encoded) is decoded and transliterated the way the core slugs a name, `/وردپرس` being
`/wrdprs`, and the report says so (`"path_made_ascii"`). The old address is not redirected.

The source's front page is the page whose address is `/`, and its posts page the page at the
address of the posts: neither is told apart, since the address says it. A page whose address the
site already answers waits for an admin's decision (`"slug_taken"`,
`MishkaCmsMigration.MigrationItem`'s Decide…), also when the site's page is not live: the site
has a page at the same path, such as its home at `/`, or one whose path captures the language
answers the same public address, such as the site's `/:lang/about` for `/about` or the Blog's
`/:lang/blog` for `/blog`. Addresses are compared as the core's own check compares them
(`MishkaCmsCore.Runtime.PageEditions.urls/3` with the primary language unprefixed), so the page
the core would refuse to put live beside the site's is asked about first, never failed. The
site's pages that could answer are read in one query: those at the items' paths and addresses,
and those whose path captures the language. Nothing is ever written over a page an admin made.
Connect to the existing record makes the item that page, kept as it is, and the pages under it
are written as the walk writes any child. Import as new with another slug writes the item as a
page of its own, its path's last part followed by its source id, `/contact-29` for `/contact` and
`/home-2` for a front page whose slug is `home`, and the report says so (`"another_slug"`). A
page under one that waits is skipped, as the walk does for any item whose parent was not
imported, naming that page (`"parent_undecided: <parent>"`).

## Status

By the run's `pages` choice (`MishkaCmsMigration.MigrationRun`'s options):

  * `"publish"` — a page WordPress has published is live at its path (`active`); a draft, and any
    other, is a page that is not live;
  * `"draft"` (the default) — every page is a page that is not live, at the path it would have,
    so nothing goes live until an admin publishes it.

The core has no page pending review, private, behind a password or scheduled, so `"pending"`,
`"private"`, a page with a `"password"` (in its `extra`) and `"future"` are never live, and the
report says so for each (`note/2`: `"draft_from_pending"`, `"draft_from_private"`,
`"draft_from_password"`, `"draft_from_future"`), in the dry run too.

## Its template

A page is compiled as HEEx, in which `{...}` and `<%= ... %>` are Elixir the page runs, so the
content, which is only text and markup, is written so that it cannot be either:

  * it is read and written back with an HTML parser (`LazyHTML`), so its tags are balanced and
    the `<` of every text and attribute value is `&lt;`;
  * the `<%` that is left, in a script, a style or a comment, whose text is not escaped, is
    written `&lt;%`, so no EEx tag opens;
  * all of it sits inside one `<div phx-no-curly-interpolation>`, which makes HEEx read every `{`
    under it as text, however deep. A `{` in a paragraph, in a code block, or in the raw text of
    a block nobody converted is shown as it was.

The builder opens the page with that `div` as its root. An element WordPress had, with an
attribute name HEEx reads as code (`:if`, or one in braces), is refused by the core's compile
check with its own sentence, and the item fails with it. A deployment that turned hand-written
templates off (`config :mishka_cms_core, :page_code, :off`) refuses every page the same way.

## Its pictures and files

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 Media address of the file the
same run, or an earlier one, wrote for it (`MishkaCmsMigration.Targets.CmsMedia.relinked/3`), a
size WordPress made of a file too. The address of a file that was not written is kept as it is,
and so is every other link; an `srcset` candidate of such a file is named in the report
(`"srcset_file_missing"`).

## Its links to pages

Its `"pages"` name the links of its content that point at a page of the source, each by its
`href` as written and the address WordPress gives that page (the WordPress source finds them,
`MishkaCmsMigration.Sources.WordPress`). Each such link points at the path that page is written
at instead (`relinked/3`), however the link was written, and keeps its `?query` and `#fragment`:
`http://old.example/about/team/#staff` becomes `/about/team#staff`, and a link to a page an
admin decided to import as new beside the site's own (`"slug_taken"`) its other path,
`/contact-29`. It is the path the run writes the page at, so a page that links to one listed
after it points at it too. A page with no
address of its own yet, such as a draft, and any other link, such as a category's or a feed's,
is kept as it is. The same links in a post point at the same paths
(`MishkaCmsMigration.Targets.BlogPost`).

## Its SEO

Its `"seo"`, what Yoast SEO or Rank Math kept for it (the WordPress source reads it), is the
page's `seo_tags` (`MishkaCmsMigration.Targets.Helpers.SeoTags`): its title, description, robots,
share image, as the Media file it was imported as, and a canonical address on another site.

## 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 page 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 page waits for an admin's decision (`"player_not_allowed"`), once its path does not,
in the dry run too (`waiting/2`), also a page 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"`).

## Its scripts

A page 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) writes it as it is
(`"scripts_kept: <found>"`).

## Its Elementor layout

A page laid out with Elementor arrives as the page builder's own elements
(`MishkaCmsMigration.Sources.WordPress.Elementor`), and is written as any other page. 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`), once
its path does not, in the dry run too (`waiting/2`), also a page 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.

# `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()]}
```

# `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()
```

# `relinked`

```elixir
@spec relinked(String.t(), map() | nil, MishkaCmsMigration.Target.context()) ::
  String.t()
```

`html` with each link that `pages` names, an `href` as the source wrote it to the `"link"` and
`"id"` of the page it is (`MishkaCmsMigration.Sources.WordPress`'s `pages`), pointing at the path
the CMS serves that page at, with the link's own `?query` and `#fragment`: the path of its
address, in ASCII, followed by `-<id>` once an admin decided to import that page as new beside
the site's page at that path (`"slug_taken"`), as `attrs/2` writes it. A link whose page has no
address of its own, and every other link, is kept as it is. The admins' decisions are read from
the pages' items (`MishkaCmsMigration.MigrationItem`) with one query, as `context`'s admin.

# `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()
```

# `written_path`

```elixir
@spec written_path(String.t() | nil, map() | nil) :: String.t() | nil
```

The path the page whose address at the source is `link` is written at, as `attrs/2` writes it:
the path of its address, in ASCII, followed by `-<source id>` once an admin decided to import it
as new beside the site's page at that path (`"slug_taken"` in the `decisions` of `item`, the
page's `MishkaCmsMigration.MigrationItem`, or `nil` for a page no run has met). `nil` for an
address with no path of its own, such as a draft's `/?page_id=30`.

---

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