Extensions and plugins
v0.0.1
Mishka CMS is a core plus extensions. Both change what the CMS does through plugins: modules that listen for a named event and may change what flows through it. This page explains how extensions and plugins fit together, so you can read an extension's code or write your own. Mishka Blog and Mishka Document are the two extensions that ship with the CMS.
Extensions
An extension is a package from Hex that the host application depends on. The host's mix.exs lists it, for example as {:mishka_blog, "~> 0.0.1"}, and the host's configuration names its web module under config :mishka_cms_core, extensions: [...]. The standard host lists MishkaCmsCoreWeb, MishkaBlogWeb and MishkaDocumentWeb.
An extension can add:
- Admin screens. Its
Routesmodule implementsMishkaCmsCore.ExtensionRouterand answers for:admin,:admin_standalone,:publicand:api. The host router reads those routes when it compiles. - Permission names, such as
admin:dashboard:blog:post. A site gets them when the extension is set up on it. - Components and sample pages. The components ship as a UI kit bundle, and each site gets its own copy of them and of the pages. The pages hang on the site's
site-mainlayout, between the header and the footer that every page of the site shares. See Draw an extension's pages in the site layout. - JSON:API routes for its resources.
- Sidebar entries, dashboard widgets, settings and more. Each one is a plugin.
The core names no extension. Removing an extension removes its sidebar entry, and nothing in the core changes.
Setting an extension up on a site
Having an extension's code in the host does not give a site its pages. A site gets them when it is set up, either from the Extensions card of the install wizard or from the extension's menu with its Set up button. The set-up writes, in this order, the permission names, the components, the pages and a snippet for the page editor. It writes only what the site lacks and never updates a row, so running it again keeps your edits.
The setting Active on this site switches an extension on or off for one site. Off hides its menu and stops its pages being served. Nothing is deleted.
Plugins
A plugin is a module that uses MishkaInstaller.Event.Hook for one event. When the CMS reaches that point, it calls Hook.call(event, payload). The payload passes through every plugin on the event, lowest priority first, and each plugin's call/1 returns it, changed or not, as {:reply, payload}. Returning {:reply, :halt, state} ends the chain with that state.
This plugin adds a link to the admin sidebar. Its event is sidebar:menus, and the payload holds the lists groups and items:
defmodule MyExtWeb.SidebarMenu do
use MishkaInstaller.Event.Hook, event: "sidebar:menus", initial: %{priority: 40}
use Gettext, backend: MishkaCmsCore.Gettext
alias MishkaCmsCore.Sidebar.Item
@items [
Item.new!(%{
key: "notes",
label: gettext_noop("Notes"),
icon: "hero-book-open",
path: "/admin/dashboard/notes",
module: MyExtWeb.Live.Admin.Notes.Index,
group: "plugins",
order: 10,
permissions: ["admin:dashboard:notes"]
})
]
@impl true
def call(%{items: items} = data), do: {:reply, %{data | items: items ++ @items}}
endThe extension's OTP application starts its plugins under a supervisor. A plugin missing from that list never registers.
def start(_type, _args) do
Supervisor.start_link([MyExtWeb.SidebarMenu], strategy: :one_for_one, name: MyExt.Supervisor)
endEach plugin is a process of its own. Its record, with its status and priority, is stored in the database and in Mnesia. The priority is a number from 0 to 100, and plugins with the same number run in order of their names. A plugin is in one of these statuses:
| Status | Meaning |
|---|---|
:registered | Stored, and not started yet. |
:started, :restarted | Running, and part of its event's chain. |
:stopped | An operator switched it off. It stays off across restarts. |
:held | A plugin it depends on is not running. It starts again when that one does. |
Events the CMS fires
These events are a sample. The core and the extensions name many more.
| Event | A plugin uses it to | Used by |
|---|---|---|
sidebar:menus | Add groups and links to the admin sidebar. | MishkaBlogWeb.SidebarMenu |
dashboard:widgets | Add cards to the dashboard. | MishkaBlog.Dashboard.Widgets.RecentPosts |
settings:register | Add settings to the Settings screen. | MishkaBlog.Dashboard.WidgetSettings |
installer:extension:install | Offer the extension in the wizard's Extensions card and install it on a site. | MishkaBlog.Plugins.Installer |
runtime:page:source | Read the rows a page shows once, and hand them to its head, its frame and its blocks. | MishkaBlog.Plugins.PageSource |
runtime:page:frame | Give the site layout a page's languages and the bar under the header. | MishkaBlog.Plugins.PageFrame |
runtime:page:seo | Give a page that shows records each record's own SEO head. | MishkaBlog.Plugins.PageSeo |
runtime:translations:used_keys | Name the words a plugin looks up, so Remove unused keeps them. | MishkaBlog.Plugins.TranslationKeys |
seo:sitemap:urls | Add addresses to a site's sitemap. | MishkaBlog.Plugins.SitemapUrls |
web:request:after_site_resolve | Change or halt a web request once its site is known. | Nothing built in listens. It is a seam for your plugins. |
Resource actions have events too. A resource that adds PluginBeforeHook to an action fires <app>:<resource>:before_<action> with the changeset, and a plugin may change it or add an error to refuse the action. Every action of a resource that uses the PluginHook notifier fires <app>:<resource>:after_<action> once it has committed, and a create or an update also fires after_save. Examples are blog:blog_post:after_publish and document:section:after_update.
Example: what Mishka Blog registers
Mishka Blog starts 24 plugins from its application module:
- five dashboard widgets;
- three plugins for publish notifications and their setting;
- one plugin for its settings tab, one for its sidebar entry, and one for the installer;
- three SEO plugins: sitemap addresses, the records its pages can show, and each record's head;
- six plugins for share cards, and one that names the components its posts draw so the site's stylesheet carries them;
- three plugins for the page frame: the rows a blog page shows, the bar and languages the site layout draws, and the words that bar looks up.
Mishka Document registers its plugins the same way, from its own application module. None of this needs a change to the core.