Versioned Docs in Phoenix LiveView with Mishka Document

A practical guide to versions, clones, pages and the two-stage review of Mishka Document, with the Elixir code that ran against the released library.

Versioned Docs in Phoenix LiveView with Mishka Document

Mishka Document stores documentation as records: a document has versions, a version has a tree of pages, and a page has notes. Readers see a page only after two reviews approve it, and you ship a new release of your docs by cloning a version. This guide builds a small versioned handbook from Elixir and walks it through a review.

Time left until the offer ends
114
D
23
H
56
M
08
S
Claim your seat · $250One payment, no subscription. Questions first? Contact us.

Mishka CMS stays free. A founding seat pays for the time we spend on it and on its extensions, and it gives you lifetime access, priority support and every paid extension we release.

Every step below ran against Mishka Document 0.0.3-alpha.2, the latest release on Hex, and the output you see is real. The behavior and the admin screens come from the Mishka Document docs, which this post links as it goes.

You need a Mishka CMS site with Documentation installed, and about fifteen minutes. See Set up documents on a site if you have not done that yet. By the end you have a handbook with two versions and one reviewed change.

Newsletter

Mishka CMS releases. Once a month.

New Mishka CMS releases, Elixir and Phoenix patterns, and case studies from the team. No spam, unsubscribe anytime.

✓ once a month✓ no spam
6version statuses
2reviews on every page
4checks before a reader sees a page
1transaction per clone

The short answer

Create a document, add a version, write pages, and clone the version when the next release starts. A page reaches readers when its document is Active, its version is Alpha, Beta, Rc or Stable, and both of its reviews are Approved.

1 · DocumentCreate itA name, a slug and an address
2 · VersionAdd 1.0.0Stable and marked latest
3 · PagesWrite a page and a noteBoth reviews Approved
4 · CloneCopy into 1.1.0One transaction
5 · ReviewDraft and approveTwo reviews, then it applies
Diagram of a Mishka Document document with two versions, a tree of pages with a front page, a group and a page, a note, and a draft waiting for review

One document, its versions, its tree of pages, a note and a draft.

The admin calls a document a workspace and a page a section, and so does the code. The How documents are organized page lists every part, including tags, issues and the event log.

Step 1: Create a document

A master writes with the master_create action and names the site. A site account uses create with a tenant instead. I ran the steps in the extension's own test setup, with a master user and a site from its factories. In your host app, master is a real master account and site is yours.

elixir
alias MishkaDocument.{Note, Section, SectionDraft, Workspace, WorkspaceVersion}

# master: a user with no site, such as one holding "*". site: the site that owns the document.
opts = [actor: master]

document =
  Workspace
  |> Ash.Changeset.for_create(
    :master_create,
    %{
      name: "Handbook",
      slug: "handbook",
      path: "/handbook/docs",
      status: :active,
      language: "en",
      site_id: site.id
    },
    opts
  )
  |> Ash.create!()

The address is checked and stored in its normal form, and the site pages that answer at it are written for you. In the admin the same step is the Documents screen. See Create a document.

Step 2: Add a version

A version is one release of the documents. Give it a semantic version number and a status, and mark it latest if readers should land on it.

elixir
version =
  WorkspaceVersion
  |> Ash.Changeset.for_create(
    :master_create,
    %{
      workspace_id: document.id,
      site_id: site.id,
      version_number: "1.0.0",
      status: :stable,
      is_latest_for_version: true,
      published_at: DateTime.utc_now()
    },
    opts
  )
  |> Ash.create!()

A Stable version needs a published_at that is not in the future. Marking a version as latest unmarks the document's other versions.

StatusReaders see the version
DraftNo
Alpha, Beta, RcYes
StableYes, with a published date that is not in the future
ArchivedNo

Step 3: Write a page and a note

A page is a title, a slug and a text, filed under a parent. A note is a titled block drawn after the page. Set both reviews to :approved here, or readers do not see the page.

elixir
page =
  Section
  |> Ash.Changeset.for_create(
    :master_create,
    %{
      workspace_version_id: version.id,
      site_id: site.id,
      language: "en",
      title: "Welcome",
      slug: "welcome",
      content: "<p>Start here.</p>",
      order_position: 0,
      content_review_status: :approved,
      technical_review_status: :approved
    },
    opts
  )
  |> Ash.create!()

Note
|> Ash.Changeset.for_create(
  :master_create,
  %{
    section_id: page.id,
    site_id: site.id,
    title: "Install",
    content: "<p>Add the dependency.</p>",
    order_position: 0,
    content_review_status: :approved,
    technical_review_status: :approved
  },
  opts
)
|> Ash.create!()

Give a page a parent with parent_section_id. Mark a version's front page with front_page: true and a heading over other pages with group_heading: true. A group and the front page leave their slug out of the address, so the address matches what a reader sees. The Write pages and notes guide shows the admin route.

Step 4: Clone the version for the next release

A clone copies every page, note and tag of a version into a new draft version of the same document, in one transaction. If anything fails, nothing is written.

elixir
next =
  WorkspaceVersion
  |> Ash.Changeset.for_create(
    :master_clone,
    %{version_number: "1.1.0", source_version_id: version.id},
    opts
  )
  |> Ash.create!()

{next.status, next.is_latest_for_version, next.__metadata__.cloned}
#=> {:draft, false, %{from: "1.0.0", tags: 0, pages: 1, notes: 1}}

The clone records its source and is neither latest nor visible, because its status is Draft. Drafts of pages and notes are not copied. Readers keep reading 1.0.0 while you edit 1.1.0.

Animation in six steps: 1.0.0 is stable and read by readers, 1.1.0 is cloned as a draft, a page draft waits for two reviews, the content review is approved, the technical review is approved and the draft applies itself, then 1.1.0 becomes stable and latest

From a stable 1.0.0 to a reviewed 1.1.0, in six steps.

Deleting a source deletes the versions cloned from it. To keep a clone when its source goes, use Make independent in the admin. The Add, clone and publish versions guide covers it.

Step 5: Propose a change and review it

Editing an approved page in place changes what readers see the moment you save. A draft is the other route: it holds a proposed page and waits for a Content Review and a Technical Review. When both are Approved it writes itself into the page.

First find the copy of the page in 1.1.0.

elixir
copy =
  Section
  |> Ash.Query.filter(workspace_version_id == ^next.id)
  |> Ash.read!(actor: master, action: :master_read, page: [limit: 50])
  |> Map.get(:results)
  |> hd()

Then add the draft. A field you leave out keeps the page's value, and applying the draft writes only the fields it changes.

elixir
draft =
  SectionDraft
  |> Ash.Changeset.for_create(
    :master_create,
    %{
      workspace_version_id: next.id,
      site_id: site.id,
      source_section_id: copy.id,
      title: "Welcome to the handbook",
      slug: "welcome",
      language: "en",
      change_version: "1.1.0",
      version_increment_type: :minor
    },
    opts
  )
  |> Ash.create!()

{draft.content_review_status, draft.technical_review_status, draft.applied_at}
#=> {:pending, :pending, nil}

Approve the two reviews one at a time. The page changes only after the second.

elixir
review = [actor: master, tenant: site.id]

draft
|> Ash.Changeset.for_update(:update, %{content_review_status: :approved}, review)
|> Ash.update!()
# the page still reads "Welcome"

draft
|> Ash.Changeset.for_update(:update, %{technical_review_status: :approved}, review)
|> Ash.update!()

Ash.get!(Section, copy.id, actor: master, action: :master_read).title
#=> "Welcome to the handbook"
The output of the walkthrough: the document, the version, the page, the clone with one page and one note copied, the pending draft, and the page title that changes only after both reviews

What the walkthrough printed. The title changes after the second review.

Who sees what

A reader sees a page only when every record above it allows it. Four checks decide: the document, the version, the page's two reviews and the permissions that lock a record.

Diagram of four checks a page must pass: the document is Active, the version is Alpha, Beta, Rc or Stable, both page reviews are Approved, and the reader holds any permission lock

Four checks, and a page that fails one is absent.

A document and a page can each carry a list of permission names. A page that names its own lock opens for the readers of that lock, even inside a document locked to other names. Review, status and who can read a page has a worked example.

How readers pick a version

A document opens on the version marked latest. With none marked, it opens on the newest published one. The latest version has no ?v= in its address, and the version switcher adds it for any other version.

The Mishka Chelekom docs on this site work that way. Open the Mishka Chelekom docs for the latest version and the Mishka Chelekom 0.0.9 docs for the older 0.0.9. Its 0.0.10 was created from 0.0.9, so each page keeps its history and the switcher leads from one to the other.

Next steps

Read the how-to guides for the admin side of each step, and Write documents from code for a script you can run twice. Details of the extension are on the Mishka Document page.

If you want the story behind it, read Mishka Document 0.0.1: Versioned Docs Engine for Phoenix LiveView. To read your documents from another app, see Headless or coupled.

Mishka CMS is free, and a small team builds it. If you want to help us work on it full time, a founding seat gives you lifetime access, priority support and every paid extension we release.

Time left until the offer ends
114
D
23
H
56
M
08
S
Claim your seat · $250One payment, no subscription. Questions first? Contact us.

You can also sponsor our open source work, or hire our team for a Phoenix LiveView project.

Newsletter

Mishka CMS releases. Once a month.

New Mishka CMS releases, Elixir and Phoenix patterns, and case studies from the team. No spam, unsubscribe anytime.

✓ once a month✓ no spam

Questions people ask about Mishka Document

A versioned documentation engine for Mishka CMS. A document holds versions, a version holds a tree of pages, and a page holds notes. Every record lives on a site, and a page reaches readers only after two reviews.

Add a version on the Workspace Versions screen, or clone an existing one. A clone copies every page, note and tag into a new draft version in one transaction and records its source. Readers see it once you change its status.

Alpha, Beta, Rc and Stable versions. Draft and Archived versions stay hidden, and a Stable version needs a published date that is not in the future. A document opens on the version marked latest, and ?v= picks another.

Every page and note has a Content Review and a Technical Review, each Pending, Approved, Rejected or Needs revision. Readers see a page only when both are Approved. A draft of a change applies itself when both are Approved.

Yes. Documents are Ash resources, so a script or an importer can create them with the master_create actions, as the Mishka Chelekom docs are written. Use master_clone to copy a version into the next release.

Yes. One site can hold many documents, each with several releases and languages. Translations of a document or a page share a language group.

Thank you

Mishka Document stands on Elixir, Phoenix LiveView and Ash Framework. Thank you to everyone who builds them, and to you for reading this far.

Contributors Mishka Group Core
Shahryar Tavakkoli
LEAD
Shahryar Tavakkoli @shahryar-tavakkoli
Elixir · Phoenix · LiveView

Shahryar Tavakkoli writes about Elixir and Phoenix LiveView: Mishka Chelekom releases, GuardedStruct validation, static sites, service workers and SEO.

Comments

Sign in to comment

No comments yet.