The picture a link to a page shows where it is shared — X, Telegram, WhatsApp, LinkedIn, Slack, Discord — drawn from what the page is: 1200×630 JPEG, the size every one of them shows large.
MishkaCmsCore.Runtime.Seo asks for one (image/2) for a public page whose head has no image of
its own to show: nothing stored in its og:image. A plugin on MishkaCmsCore.Runtime.Seo.event/0
fills what the card shows of its record through card, and its image is the card's picture.
What a card shows
card is a map; every key but title may be left out:
title— the page's title, up to three lines, smaller as it grows, then cut with…description— all of it, in as many lines as fit under the title: set smaller, down to 21 pixels, before the title steps down a size; one that fits at no size is cut at the end of a sentence, else at a word, with…(shorten/1)eyebrow— a short label above the title: a post's category, "Team", "Author"site,host— the site's name, in the page's language, and its addresslogo— thesite.logosetting, on a white tile; the site's first letter without onepeople—[%{name: name, image: path}]: round photos, the names beside them; up to fourbyline,meta— a line under the names: a team, a date, a reading timepicture— a picture behind the card, darkened under the text
A picture is a file of the site's own (site_file/2): a /uploads/<site_id>/media/… path of
its media (MishkaCmsCore.Runtime.Media.storage_paths/2), or a /… path under the host's static
folder. An http(s):// address is never fetched. A photo is read only when it is a JPEG, PNG,
WebP or GIF (picture_type/1); the logo may be an SVG too. One it cannot read is left out, and
the card is drawn without it.
Text written right to left — Persian, Arabic, Hebrew — puts everything on the right. The fonts
are in priv/share_card: Plus Jakarta Sans for Latin text and Vazirmatn for Arabic script, both
under the SIL Open Font License 1.1 (priv/share_card/OFL.txt).
When it is drawn
Never in a request. image/2 answers from ETS (MishkaCmsCore.Runtime.States.AssetsState), and
the first time a card is asked for it is queued: this module is the Oban worker, on
:share_cards (MishkaCmsCore.Runtime.ShareCardQueue, as many at once as the Share-card workers
setting says), one job per card. Until it is drawn, the page shows its own image, or none.
A card is named by what it shows (key/1): a page whose title changes asks for a new card, and the
old one stays where it is. It is stored at /uploads/<site_id>/share/<key>.jpg, served as the
site's other files are.
Ready before the first visit
refresh/3 looks at a page's or a record's card in the background — the plugins on the
after-save events call it — and queues its drawing when it is not drawn yet, so it is there for
the first visitor. A save that leaves what the card shows alone finds it drawn and draws nothing.
refresh(site_id, target, force: true) draws it again over the old file: the Regenerate button.
Every card of a site
redraw_all/2 looks at every card of a site in the background, so none waits for its first
visitor: after what every card of the site shows changes — its address, its name or that name's
translation, its logo — and from the Redraw buttons of the settings page and of an extension. It
queues one planner job (MishkaCmsCore.Runtime.Workers.ShareCardPlanWorker), which asks the
producers for the cards they own on the site through the targets_event/0 mishka_installer
event (targets/2) and queues a look at each, in batches. The core answers with its pages
(MishkaCmsCore.Runtime.PageShareCardTargets); an extension or a site answers with its own.
Old cards
A card drawn for data that has changed since stays on disk until the next look at every card of
the site: each look marks the files its pages show (keep/2), and once all have run the planner's
MishkaCmsCore.Runtime.Workers.ShareCardSweepWorker deletes the files none of them marked. A page
that asks again for a card swept away has it drawn again.
Drawn by libvips in a separate process (MishkaCmsCore.Isolated), so a file that crashes it ends
that process, never the server. That process runs at a lower priority (nice 10) and libvips in
it on one thread, so a long redraw leaves the processor to the pages being served.
Summary
Types
What a card shows; see the module doc.
What a refresh is for: one page, by its id, or the pages that show records of a kind
(MishkaCmsCore.Runtime.Seo.record_of/1), with the captures that name one record —
{:record, "blog:post", %{"slug" => "hello", "lang" => "en"}}.
Functions
Each card target's pages show now: %{title:, url:, drawn_at:}, drawn_at the Unix time its
file was written, nil until it is drawn. A page kept out of an index, or storing an og:image
of its own, shows none.
The folder the share cards of the site site_id are written to.
The card card names, for the site site_id: %{url:, width:, height:, type:} once it is
drawn, nil until then — the first ask queues it.
Marks the card file url names as shown now — /uploads/<site_id>/share/<name>.jpg, or an
address whose path is that — so the sweep that follows a look at every card of the site
(MishkaCmsCore.Runtime.Workers.ShareCardSweepWorker) leaves it. A producer whose own job finds
its card drawn calls it; look/3 calls it for the core's cards and a plugin's. A map with a
url, as image/2 answers, is read for it. A file that is not there, a path elsewhere and
anything else are left alone.
The name of the card that shows card: the same for the same card, another when anything on it
changes.
Where card's text goes, measured as draw/1 sets it, in a separate process like the drawing and
without drawing it: {:ok, %{title_size:, description_size:, description:}} — the sizes in pixels
and the description as it is shown, which is the whole of it unless no size fits it. Never called
in a request.
Looks at the cards target's pages show now, as ShareCardRefreshWorker does in the background:
a card not drawn yet is queued (image/2), and with force: true every card is drawn again over
its old file (redraw/2). Each card file the pages show — the core's, and one a plugin on
MishkaCmsCore.Runtime.Seo.event/0 drew and put in the head's image — is kept (keep/2).
The type of the picture file at path, read off its first bytes, never its name:
"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml" or
"image/x-icon". nil for any other file, and for one that cannot be read.
The producers that answer targets_event/0 on the site site_id — or on any site, for nil —
sorted: ["mishka_blog", "mishka_cms_core"]. Asks for their names alone; nothing is read.
Puts producer's targets in an answer to targets_event/0; fun is called for them only when
the answer asks every producer or this one.
Queues card to be drawn again, over the file drawn before; :ok for a card with no title.
Looks at every share card of the site site_id in the background — of every active site for
nil — so a card whose data changed is drawn before anybody asks for it: one planner job a site
on :share_cards, which collects the cards through targets_event/0 and queues a look at each.
A site that already has a planner waiting with the same options keeps that one.
Queues a look, in the background, at the card target shows now, and its drawing when it is not
drawn yet. Saves within two seconds of each other are one look.
text one step shorter, for a text that has no room: without its last sentence while it has more
than one, else without its last word, the rest ending in …. nil once one word is left.
The file on disk a picture's address names, when it is a file of the site site_id's own: a
/uploads/<site_id>/media/… path of its media (MishkaCmsCore.Runtime.Media.storage_paths/2),
or a /… path under the host's static folder outside uploads. nil for a file that is not
there, a path of another site or climbing out, an http(s):// address — never fetched — and
anything else.
The settings a core card shows on every page of a site: its name and its logo.
What each page that shows target puts on its share card, as Seo.head/2 builds it for a visit
to it: the same card, with the same name. The pages are read as the system, as the sitemap reads
them — a job has no visitor.
The picture at path drawn in a square of each of sizes, as PNGs: fitted whole and centered
on transparency, never stretched or cut. Drawn in a separate process, as a card is:
{:ok, %{size => png}}, else {:error, reason}.
The headers Plug.Static adds to a file it serves from the uploads folder
The event the names of the card files a sweep deleted are announced on:
"runtime:share_card:swept", with %{site_id:, removed: ["<name>.jpg", …]}. A producer that remembers
its cards as drawn forgets those names here; the core forgets its own.
Every share card of the site site_id, by producer, as the plugins on targets_event/0 answer:
{:ok, %{"mishka_cms_core" => [{:page, id}, …], …}}.
The event every share card of a site is collected through: "runtime:share_card:targets".
Where the card key of the site site_id is served.
Types
What a card shows; see the module doc.
What a refresh is for: one page, by its id, or the pages that show records of a kind
(MishkaCmsCore.Runtime.Seo.record_of/1), with the captures that name one record —
{:record, "blog:post", %{"slug" => "hello", "lang" => "en"}}.
Functions
@spec cards(String.t(), target()) :: [ %{title: String.t(), url: String.t(), drawn_at: integer() | nil} ]
Each card target's pages show now: %{title:, url:, drawn_at:}, drawn_at the Unix time its
file was written, nil until it is drawn. A page kept out of an index, or storing an og:image
of its own, shows none.
The folder the share cards of the site site_id are written to.
The card card names, for the site site_id: %{url:, width:, height:, type:} once it is
drawn, nil until then — the first ask queues it.
nil for a card with no title, and for a site with no id.
Marks the card file url names as shown now — /uploads/<site_id>/share/<name>.jpg, or an
address whose path is that — so the sweep that follows a look at every card of the site
(MishkaCmsCore.Runtime.Workers.ShareCardSweepWorker) leaves it. A producer whose own job finds
its card drawn calls it; look/3 calls it for the core's cards and a plugin's. A map with a
url, as image/2 answers, is read for it. A file that is not there, a path elsewhere and
anything else are left alone.
The name of the card that shows card: the same for the same card, another when anything on it
changes.
Where card's text goes, measured as draw/1 sets it, in a separate process like the drawing and
without drawing it: {:ok, %{title_size:, description_size:, description:}} — the sizes in pixels
and the description as it is shown, which is the whole of it unless no size fits it. Never called
in a request.
Looks at the cards target's pages show now, as ShareCardRefreshWorker does in the background:
a card not drawn yet is queued (image/2), and with force: true every card is drawn again over
its old file (redraw/2). Each card file the pages show — the core's, and one a plugin on
MishkaCmsCore.Runtime.Seo.event/0 drew and put in the head's image — is kept (keep/2).
The type of the picture file at path, read off its first bytes, never its name:
"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml" or
"image/x-icon". nil for any other file, and for one that cannot be read.
The producers that answer targets_event/0 on the site site_id — or on any site, for nil —
sorted: ["mishka_blog", "mishka_cms_core"]. Asks for their names alone; nothing is read.
@spec put_targets(map(), String.t(), (-> Enumerable.t())) :: map()
Puts producer's targets in an answer to targets_event/0; fun is called for them only when
the answer asks every producer or this one.
Queues card to be drawn again, over the file drawn before; :ok for a card with no title.
Looks at every share card of the site site_id in the background — of every active site for
nil — so a card whose data changed is drawn before anybody asks for it: one planner job a site
on :share_cards, which collects the cards through targets_event/0 and queues a look at each.
A site that already has a planner waiting with the same options keeps that one.
Options:
:force—truedraws every card again over its old file; by default only a card whose data changed is drawn:producer— the one producer to ask,"mishka_blog"; every one by default:schedule_in— seconds to wait first, so the saves of a burst are one look; none by default
{:ok, site_ids}: the sites a planner is queued for.
Queues a look, in the background, at the card target shows now, and its drawing when it is not
drawn yet. Saves within two seconds of each other are one look.
force: true looks now and draws the card again even when it is drawn, over the old file.
text one step shorter, for a text that has no room: without its last sentence while it has more
than one, else without its last word, the rest ending in …. nil once one word is left.
iex> MishkaCmsCore.Runtime.ShareCard.shorten("First one. Second one. Third one.")
"First one. Second one."
iex> MishkaCmsCore.Runtime.ShareCard.shorten("A single long sentence, with commas.")
"A single long sentence, with…"
The file on disk a picture's address names, when it is a file of the site site_id's own: a
/uploads/<site_id>/media/… path of its media (MishkaCmsCore.Runtime.Media.storage_paths/2),
or a /… path under the host's static folder outside uploads. nil for a file that is not
there, a path of another site or climbing out, an http(s):// address — never fetched — and
anything else.
site_file("/uploads/<site_id>/media/brand/logo.svg", site_id) #=> "<static>/uploads/<site_id>/media/brand/logo.svg"
@spec site_settings() :: [String.t()]
The settings a core card shows on every page of a site: its name and its logo.
What each page that shows target puts on its share card, as Seo.head/2 builds it for a visit
to it: the same card, with the same name. The pages are read as the system, as the sitemap reads
them — a job has no visitor.
redraw: true tells the plugins on MishkaCmsCore.Runtime.Seo.event/0 the card is to be drawn
again (redraw in MishkaCmsCore.Runtime.Seo.given/2): one that draws a card of its own draws it
over its old file.
@spec squares(Path.t(), [pos_integer()]) :: {:ok, %{required(pos_integer()) => binary()}} | {:error, term()}
The picture at path drawn in a square of each of sizes, as PNGs: fitted whole and centered
on transparency, never stretched or cut. Drawn in a separate process, as a card is:
{:ok, %{size => png}}, else {:error, reason}.
path is a file on disk picture_type/1 names a PNG, JPEG, WebP, GIF or SVG — a site's
home-screen icon (MishkaCmsCore.Runtime.SiteIcon).
squares("/srv/uploads/<site_id>/media/icon.svg", [180, 512])
#=> {:ok, %{180 => <<137, 80, 78, 71, …>>, 512 => <<137, 80, 78, 71, …>>}}
@spec static_headers(Plug.Conn.t()) :: %{optional(String.t()) => String.t()}
The headers Plug.Static adds to a file it serves from the uploads folder:
a card is named by what it shows (
key/1), and a site's icon (/uploads/<site_id>/icons/<hash>-<size>.png,MishkaCmsCore.Runtime.SiteIcon) by the picture it is drawn from, so any cache may keep either a year;a file a visitor uploaded to a page (
/uploads/<site_id>/forms/<file>) is saved rather than shown, never sniffed into another type, and sandboxed — seeMishkaCmsCore.Runtime.PageUpload.static_headers/0;a media library file (
/uploads/<site_id>/media/…) is never sniffed into another type, and an.svgamong them is sandboxed, so a script in it does not run when its address is opened;any other file gets none.
plug Plug.Static, at: "/", only: ~w(uploads), headers: {ShareCard, :static_headers, []}
@spec swept_event() :: String.t()
The event the names of the card files a sweep deleted are announced on:
"runtime:share_card:swept", with %{site_id:, removed: ["<name>.jpg", …]}. A producer that remembers
its cards as drawn forgets those names here; the core forgets its own.
@spec targets(String.t(), keyword()) :: {:ok, %{required(String.t()) => list()}} | {:error, :cannot_run}
Every share card of the site site_id, by producer, as the plugins on targets_event/0 answer:
{:ok, %{"mishka_cms_core" => [{:page, id}, …], …}}.
Options: :force, handed to the producers, and :producer, to ask that one alone.
{:error, :cannot_run} while the event cannot run: one of its plugins is started but not loaded.
@spec targets_event() :: String.t()
The event every share card of a site is collected through: "runtime:share_card:targets".
It is called with %{site_id: id, force: boolean, producer: name | nil, targets: %{}}, and each
plugin answers with its own put in targets under its name (put_targets/3) — the name of its
OTP application, %{"mishka_blog" => [target, …]}. A target is either
- a
target/0,{:page, id}or{:record, kind, captures}, whichrefresh/3looks at, or - an
Oban.Jobchangeset of the producer's own worker on:share_cards, for a card the core does not draw: built withforceit draws the card again over the old file, else only when it is not drawn yet.
An answer names targets only — ids and captures, read in pages or streamed — and draws nothing.
With producer set, only that producer answers; with names_only: true each puts its name and
no targets (producers/1).
Where the card key of the site site_id is served.