# Mishka Blog v0.0.1 - Table of Contents > A multi-author blog engine for Mishka CMS, built with Elixir and Phoenix LiveView. ## Pages - [README](readme.md) - [License](license.md) ## Modules - [MishkaBlog](MishkaBlog.md): The blog's Ash domain: posts, collections, tags, bloggers and teams with their followers, comments, ratings, bookmarks and views, and auto posting, and the JSON:API routes that serve them to a master, to a site's admin and to a visitor. - [MishkaBlog.Application](MishkaBlog.Application.md): The blog's OTP application. It supervises the task supervisor that highlights a post's code blocks and every mishka_installer plugin the extension ships (`plugins/0`): the dashboard widgets, the notification producers, the settings, sidebar, install, SEO, share card, stylesheet and page plugins. A plugin registers on the event it listens on once it is supervised here. - [MishkaBlog.AutoPosting](MishkaBlog.AutoPosting.md): Manages auto-posting configurations in the MishkaBlog plugin with full versioning support. - [MishkaBlog.AutoPosting.Version](MishkaBlog.AutoPosting.Version.md): - [MishkaBlog.BlogPost](MishkaBlog.BlogPost.md): Manages blog posts in the MishkaBlog plugin with full versioning and translation support. - [MishkaBlog.BlogPost.Version](MishkaBlog.BlogPost.Version.md): - [MishkaBlog.Blogger](MishkaBlog.Blogger.md): Manages blogger profiles in the MishkaBlog plugin with full versioning support. - [MishkaBlog.Blogger.Version](MishkaBlog.Blogger.Version.md): - [MishkaBlog.BloggerFollower](MishkaBlog.BloggerFollower.md): Manages user follows for bloggers. - [MishkaBlog.Collection](MishkaBlog.Collection.md): Manages blog post collections in the MishkaBlog plugin with full versioning support. - [MishkaBlog.Collection.Version](MishkaBlog.Collection.Version.md): - [MishkaBlog.Comment](MishkaBlog.Comment.md): Manages blog post comments in the MishkaBlog plugin with full versioning support. - [MishkaBlog.CommentVote](MishkaBlog.CommentVote.md): Manages user votes (upvote/downvote) on comments. - [MishkaBlog.Dashboard.Analytics](MishkaBlog.Dashboard.Analytics.md): Every number on the blog analytics page (`/admin/dashboard/blog/analytics`), read from the database. - [MishkaBlog.Dashboard.Overview](MishkaBlog.Dashboard.Overview.md): Every number on the blog admin overview page (`/admin/dashboard/blog`), read from the database. - [MishkaBlog.Dashboard.Scope](MishkaBlog.Dashboard.Scope.md): Every read of the blog dashboard — `MishkaBlog.Dashboard.Overview`, `MishkaBlog.Dashboard.Analytics` and the widgets under `MishkaBlog.Dashboard.Widgets` — as the user viewing it, through the resource's own policies. - [MishkaBlog.Dashboard.WidgetSettings](MishkaBlog.Dashboard.WidgetSettings.md): Registers every mishka_blog dashboard widget's own settings with the Settings page through the `settings:register` mishka_installer hook — so "Posts to show" is documented under the **Blog** tab (`category: "blog"`, group **Dashboard widgets**) alongside the rest of the blog's settings, instead of hiding inside the dashboard's per-user layout where nothing else could see it. - [MishkaBlog.Dashboard.Widgets.Bloggers](MishkaBlog.Dashboard.Widgets.Bloggers.md): Dashboard widget (a `dashboard:widgets` plugin): the top bloggers leaderboard — a podium of the three most-published profiles plus the runners-up, each with their post tally and publish role (`Blogger` + the `post_count` aggregate), read as the viewer through `MishkaBlog.Dashboard.Scope`: a master ranks bloggers across every site, a site's admin only their own site's, and a viewer without `admin:dashboard:blog:post` none. - [MishkaBlog.Dashboard.Widgets.CommentsModeration](MishkaBlog.Dashboard.Widgets.CommentsModeration.md): Dashboard widget (a `dashboard:widgets` plugin): comments awaiting moderation — a pending count plus the latest few with an excerpt of their content (`Comment`), read as the viewer through `MishkaBlog.Dashboard.Scope`: a master moderates across every site, a site's admin only their own site's, and a viewer without `admin:dashboard:blog:comment` none. Each author is named as the comments page names them (`MishkaBlogWeb.Templates.CommentCard.author/1`), from what the viewer may read of the account. - [MishkaBlog.Dashboard.Widgets.DraftsScheduled](MishkaBlog.Dashboard.Widgets.DraftsScheduled.md): Dashboard widget (a `dashboard:widgets` plugin): draft vs scheduled post counts plus the latest unpublished posts — drafts with their age, scheduled posts with their publish time (`BlogPost`), read as the viewer through `MishkaBlog.Dashboard.Scope`: a master sees every site's pipeline, a site's admin only their own site's, and a viewer without `admin:dashboard:blog:post` none. - [MishkaBlog.Dashboard.Widgets.RecentPosts](MishkaBlog.Dashboard.Widgets.RecentPosts.md): Dashboard widget (a `dashboard:widgets` plugin): the latest blog posts with their status, read as the viewer through `MishkaBlog.Dashboard.Scope`: a master sees every site's posts, a site's admin only their own site's, and a viewer without `admin:dashboard:blog:post` none. - [MishkaBlog.Dashboard.Widgets.TopPosts](MishkaBlog.Dashboard.Widgets.TopPosts.md): Dashboard widget (a `dashboard:widgets` plugin): the most-viewed published posts with their view tally (`BlogPost` + the `view_count` aggregate), read as the viewer through `MishkaBlog.Dashboard.Scope`: a master ranks posts across every site, a site's admin only their own site's, and a viewer without `admin:dashboard:blog:post` none. - [MishkaBlog.Dev.Generator](MishkaBlog.Dev.Generator.md): Generator for MishkaBlog resources. - Test functions (no prefix): simple, deterministic — used by factory and tests. - Dev functions (dev_ prefix): realistic, rich data — used by dev seeds. - [MishkaBlog.Dev.Seeds](MishkaBlog.Dev.Seeds.md): Dev and stress seeds for MishkaBlog resources. - [MishkaBlog.Kit](MishkaBlog.Kit.md): Where this extension's UI kit bundle is, and how to install it. - [MishkaBlog.Notifiers.NotifySettings](MishkaBlog.Notifiers.NotifySettings.md): Registers mishka_blog's notification toggles with the Settings page through the `settings:register` mishka_installer hook. Each toggle is a boolean (default on) under the **Blog** tab (`category: "blog"`) in a **Notifications** group (`group:`), with `scope: :both` — a global default (shown in the master's Global view) that any site may override. Every mishka_blog setting lives under the Blog tab; `group` separates concerns within it (Notifications, and e.g. AI later). Delivery honours it: the engine gates every notification on `Settings.get(Notify.setting_key(type), true, site_id)` (site → global → default) before fanning it out. - [MishkaBlog.Notifiers.PostMasterPublished](MishkaBlog.Notifiers.PostMasterPublished.md): The master half of `MishkaBlog.Notifiers.PostPublished`. A master publishes through `:master_publish`, which fires `blog:blog_post:after_master_publish` — not the tenant action's event — so without a plugin here a post a master published would tell nobody. It delivers exactly what a tenant publish delivers, to the same audience. Registered in `MishkaBlog.Application`. - [MishkaBlog.Notifiers.PostPublished](MishkaBlog.Notifiers.PostPublished.md): A plain mishka_installer plugin: when a tenant publishes a blog post (`blog:blog_post:after_publish`), notify the tenant's blog admins AND master admins — in one `Notify.deliver/3` call — except the publisher themselves (the notifier's `meta.actor`, passed as `except:`), so no one is told about their own post. - [MishkaBlog.PageSource](MishkaBlog.PageSource.md): The rows a page of the blog is made of, read once for the request and handed to everything that draws them: the head (`MishkaBlog.Plugins.PageSeo`), the frame (`MishkaBlog.Plugins.PageFrame`) and the page's blocks. - [MishkaBlog.Plugins.BloggerShareCard](MishkaBlog.Plugins.BloggerShareCard.md): A plain mishka_installer plugin: after a blogger is saved (`blog:blogger:after_save` — created, updated or published, a master's twin alike), its share card is looked at in the background and drawn when what it shows changed (`MishkaCmsCore.Runtime.ShareCard.refresh/3`), so it is ready before the first visitor. Only while it is not archived (`MishkaBlog.ShareCards.target/1`). Registered in `MishkaBlog.Application`. - [MishkaBlog.Plugins.CollectionShareCard](MishkaBlog.Plugins.CollectionShareCard.md): A plain mishka_installer plugin: after a category is saved (`blog:collection:after_save` — created, updated or published, a master's twin alike), its share card is looked at in the background and drawn when what it shows changed (`MishkaCmsCore.Runtime.ShareCard.refresh/3`), so it is ready before the first visitor. Only while it is active (`MishkaBlog.ShareCards.target/1`). Registered in `MishkaBlog.Application`. - [MishkaBlog.Plugins.ContentComponents](MishkaBlog.Plugins.ContentComponents.md): Tells a site's stylesheet which components its posts draw, which icons they name and which classes they write, through the `runtime:compile:content_components` hook `MishkaCmsCore.Runtime.Compilers.TailwindCompiler.content_components/1` fires while it builds a site's stylesheet: the names the site's published posts list in `content_components`, the icon classes they list in `content_icons` and the classes they list in `content_classes`, which `MishkaBlog.Resources.Changes.ContentFacts` stores when a post is written. A template that draws a post's body — the kit's `blog-post-detail` — then reaches those components and no others (`MishkaCmsCore.Runtime.Compilers.CssClosure`), the stylesheet carries a rule for each icon, and it is built with each class. - [MishkaBlog.Plugins.Installer](MishkaBlog.Plugins.Installer.md): Installs the blog onto one site, when asked. - [MishkaBlog.Plugins.PageFrame](MishkaBlog.Plugins.PageFrame.md): What the blog tells the site's layout about each of its pages, through the `runtime:page:frame` mishka_installer hook `MishkaCmsCore.Runtime.SiteLayout` fires in `handle_params/3`: the bar under the header, and the languages the record a page shows is written in. The layout draws both, so no page of the blog draws a bar or a language switcher of its own. - [MishkaBlog.Plugins.PageRecords](MishkaBlog.Plugins.PageRecords.md): Names the kinds of record the blog's pages show — a post, a collection, a tag, a blogger, a team — on the `runtime:page:records` mishka_installer hook, for a page's "This page shows each" choice (see `MishkaCmsCore.Runtime.Seo.record_types/1`), each with a few of the site's own to preview a page with. Each is labelled as the blog's admin names it, where its Permissions and SEO Tags are set. `MishkaBlog.Plugins.PageSeo` answers for the same kinds. - [MishkaBlog.Plugins.PageSeo](MishkaBlog.Plugins.PageSeo.md): Gives each post, category, tag, blogger and team its own head on the blog page that serves it, through the `runtime:page:seo` mishka_installer hook `MishkaCmsWeb.RuntimeLive` fires in `handle_params/3` (see `MishkaCmsCore.Runtime.Seo` for the map it is handed and what the head builds from it). - [MishkaBlog.Plugins.PageSource](MishkaBlog.Plugins.PageSource.md): Reads the rows a page of the blog shows, once, through the `runtime:page:source` mishka_installer hook `MishkaCmsWeb.RuntimeLive` fires in `handle_params/3` before the frame and the head (`MishkaCmsCore.Runtime.PageSource` is the contract: the map it is handed and where what it reads goes). - [MishkaBlog.Plugins.PostShareCard](MishkaBlog.Plugins.PostShareCard.md): A plain mishka_installer plugin: after a post is saved (`blog:blog_post:after_save` — created, updated or published, a master's twin alike), its share card is looked at in the background and drawn when what it shows changed (`MishkaCmsCore.Runtime.ShareCard.refresh/3`), so it is ready before the first visitor. Only while it is published (`MishkaBlog.ShareCards.target/1`). Registered in `MishkaBlog.Application`. - [MishkaBlog.Plugins.ShareCardTargets](MishkaBlog.Plugins.ShareCardTargets.md): A plain mishka_installer plugin: answers `MishkaCmsCore.Runtime.ShareCard.targets_event/0` with the blog's share cards on the site asked, under `"mishka_blog"` (`MishkaBlog.ShareCards.targets/1`), so a redraw of every card of a site — and the blog's own Redraw button in its check-and-repair modal — reaches its posts, categories, tags, bloggers and teams. Registered in `MishkaBlog.Application`. - [MishkaBlog.Plugins.SitemapUrls](MishkaBlog.Plugins.SitemapUrls.md): Adds the blog's posts, categories, bloggers, teams and the tags that ask to be indexed to a site's `/sitemap.xml`, through the `seo:sitemap:urls` mishka_installer hook `MishkaCmsCore.Runtime.Compilers.SitemapCompiler` fires when it builds a site's sitemap. A post, a category, a tag, a blogger or a team written rebuilds its site's sitemap, because those resources list `MishkaCmsCore.Runtime.Resources.Notifiers.SitemapRefresh`. - [MishkaBlog.Plugins.TagShareCard](MishkaBlog.Plugins.TagShareCard.md): A plain mishka_installer plugin: after a tag is saved (`blog:tag:after_save` — created, updated or published, a master's twin alike), its share card is looked at in the background and drawn when what it shows changed (`MishkaCmsCore.Runtime.ShareCard.refresh/3`), so it is ready before the first visitor. Only while it is not archived (`MishkaBlog.ShareCards.target/1`). Registered in `MishkaBlog.Application`. - [MishkaBlog.Plugins.TeamShareCard](MishkaBlog.Plugins.TeamShareCard.md): A plain mishka_installer plugin: after a team is saved (`blog:team:after_save` — created, updated or published, a master's twin alike), its share card is looked at in the background and drawn when what it shows changed (`MishkaCmsCore.Runtime.ShareCard.refresh/3`), so it is ready before the first visitor. Only while it is active (`MishkaBlog.ShareCards.target/1`). Registered in `MishkaBlog.Application`. - [MishkaBlog.Plugins.TranslationKeys](MishkaBlog.Plugins.TranslationKeys.md): Names the words the blog's bar looks up (`MishkaBlog.Plugins.PageFrame.words/0`) on the `runtime:translations:used_keys` mishka_installer hook `MishkaCmsCore.Runtime.Translations` fires when it works out which keys a site still uses. - [MishkaBlog.PostBlogger](MishkaBlog.PostBlogger.md): Manages the many-to-many relationship between blog posts and bloggers with full version control. - [MishkaBlog.PostBookmark](MishkaBlog.PostBookmark.md): Manages user bookmarks for blog posts. - [MishkaBlog.PostCollection](MishkaBlog.PostCollection.md): Manages the many-to-many relationship between blog posts and collections with full version control. - [MishkaBlog.PostRating](MishkaBlog.PostRating.md): Manages user ratings for blog posts. - [MishkaBlog.PostTag](MishkaBlog.PostTag.md): Manages the many-to-many relationship between blog posts and tags with full version control. - [MishkaBlog.PostView](MishkaBlog.PostView.md): Tracks unique page views for blog posts using upsert. - [MishkaBlog.Provision](MishkaBlog.Provision.md): What a site needs before anybody can look at its blog: this extension's live components, nine sample pages built out of them, and the sidebar saved as a snippet an author can drop anywhere. - [MishkaBlog.Repo](MishkaBlog.Repo.md) - [MishkaBlog.Resources.Actions.PreviewVersion](MishkaBlog.Resources.Actions.PreviewVersion.md): Previews a record as it was in one of its own versions, without restoring it: the generic action takes the record's id and a `:version_id` argument and returns the record with the version's values over it. - [MishkaBlog.Resources.Calculations.EffectivePermissions](MishkaBlog.Resources.Calculations.EffectivePermissions.md): The permissions a post or a category opens with on its blog page, in its sitemap entry and wherever else a reader is let in, and those a comment is reached with - [MishkaBlog.Resources.Changes.ContentFacts](MishkaBlog.Resources.Changes.ContentFacts.md): What a post's `content` says about it, worked out once, when the content is written — reading a post works out neither - [MishkaBlog.Resources.Changes.HighlightCode](MishkaBlog.Resources.Changes.HighlightCode.md): Highlights the code blocks of a post's `content` with Makeup, once, when the post is written. - [MishkaBlog.Resources.Changes.PublishLifecycle](MishkaBlog.Resources.Changes.PublishLifecycle.md): Keeps a post's publish times in step with its `:status`. - [MishkaBlog.Resources.Changes.RestoreVersion](MishkaBlog.Resources.Changes.RestoreVersion.md): Restores a record to one of its own versions, named by the action's `:version_id` argument. - [MishkaBlog.Resources.Mixins.PaperTrailMixin](MishkaBlog.Resources.Mixins.PaperTrailMixin.md): The version resource a PaperTrail-tracked blog resource keeps its history in: the table, its foreign key to the record, and the actions and policies that read the history. - [MishkaBlog.Resources.Preparations.OpenToActor](MishkaBlog.Resources.Preparations.OpenToActor.md): Keeps a public read to the records its reader may open, by the rule the record's page keeps: a record whose lock names permissions opens only for a reader holding one of them (`AccessPermissions.match?/3`), and never for a visitor who is not signed in. A list leaves out, in its own query, every record the reader may not open, so its pages and its count are of what the reader sees; asking for one of them by itself finds nothing, as an id nothing is stored at does. - [MishkaBlog.Resources.Preparations.PreviewVersion](MishkaBlog.Resources.Preparations.PreviewVersion.md): Turns a read of one record into a preview of it as it was in one of its own versions, named by the read's `:version_id` argument. Nothing is saved. - [MishkaBlog.Resources.Preparations.ReaderLocks](MishkaBlog.Resources.Preparations.ReaderLocks.md): What a reader may open, as the filter of the read that fetches the records: the answer `MishkaCmsCore.Accounts.Checks.AccessPermissions` gives for a record's lock, worked out by the database in the statement that reads the rows, so that no lock is read beforehand. - [MishkaBlog.Resources.Preparations.SiteAskedOn](MishkaBlog.Resources.Preparations.SiteAskedOn.md): Keeps a public list to the site it is asked on: only the rows whose `:through` record belongs to the tenant. - [MishkaBlog.Resources.Validations.NotItsOwnParent](MishkaBlog.Resources.Validations.NotItsOwnParent.md): Refuses an update that makes a record its own parent: the attribute `:field` may not hold the record's own id. Any other record, or none, is let through. The refusal is `:message` on `:field`. - [MishkaBlog.Resources.Validations.OpenToActor](MishkaBlog.Resources.Validations.OpenToActor.md): Refuses a write onto a record its actor may not open — a comment, a rating or a bookmark on a locked post, a vote on a comment of one — with the message the action gives for a record that is not there, so the refusal tells nothing a reader may not know. Who may open it is what its page asks (`MishkaBlog.Resources.Preparations.OpenToActor.opens?/2`). - [MishkaBlog.ShareCards](MishkaBlog.ShareCards.md): Which share card (`MishkaCmsCore.Runtime.ShareCard`) a blog record shows: the kind of record its page shows (`MishkaBlog.Plugins.PageRecords`) and the captures that name it — what a page of that kind answers for (`MishkaBlog.Plugins.PageSeo`). - [MishkaBlog.Tag](MishkaBlog.Tag.md): Manages blog post tags in the MishkaBlog plugin with full versioning support. - [MishkaBlog.Tag.Version](MishkaBlog.Tag.Version.md): - [MishkaBlog.Team](MishkaBlog.Team.md): Manages team structures in the MishkaBlog plugin with full versioning support. - [MishkaBlog.Team.Version](MishkaBlog.Team.Version.md): - [MishkaBlog.TeamFollower](MishkaBlog.TeamFollower.md): Manages user follows for teams. - [MishkaBlog.TeamMember](MishkaBlog.TeamMember.md): Manages the many-to-many relationship between teams and bloggers with full version control. - [MishkaBlog.TeamPermission](MishkaBlog.TeamPermission.md): Manages the many-to-many relationship between teams and permissions. - [MishkaBlogWeb.AshJsonApiRouter](MishkaBlogWeb.AshJsonApiRouter.md): The router the blog's JSON:API is served through, with its OpenAPI document at `/open_api`. The domains it routes are set by the host application's config. - [MishkaBlogWeb.Live.Admin.Dashboard.Analytics.Index](MishkaBlogWeb.Live.Admin.Dashboard.Analytics.Index.md): The blog analytics page: five KPI tiles, a daily views chart, the most-read posts, where their readers came from and whether they were signed in — every figure read from the view log by `MishkaBlog.Dashboard.Analytics`. - [MishkaBlogWeb.Live.Admin.Dashboard.AutoPosting.Index](MishkaBlogWeb.Live.Admin.Dashboard.AutoPosting.Index.md): The auto posting list: a Gervaz table of the site's auto posting configurations. The page decodes the URL state and, once connected, forwards the PubSub broadcasts of its site (every site for a master) into the table component. - [MishkaBlogWeb.Live.Admin.Dashboard.Blogger.Index](MishkaBlogWeb.Live.Admin.Dashboard.Blogger.Index.md): The bloggers grid: a Gervaz table drawn as `MishkaBlogWeb.Templates.BloggerCard`, its create/edit form, and the version history of one blogger. - [MishkaBlogWeb.Live.Admin.Dashboard.Collection.Index](MishkaBlogWeb.Live.Admin.Dashboard.Collection.Index.md): The collections grid: a Gervaz table drawn as `MishkaBlogWeb.Templates.CollectionCard`, its create/edit form, a modal that edits one language edition of a collection, and the version history of one collection, which switches between its language editions. - [MishkaBlogWeb.Live.Admin.Dashboard.Comment.Index](MishkaBlogWeb.Live.Admin.Dashboard.Comment.Index.md): The comments list: a Gervaz table drawn as `MishkaBlogWeb.Templates.CommentCard`, with a create / edit / reply form. - [MishkaBlogWeb.Live.Admin.Dashboard.Home.Index](MishkaBlogWeb.Live.Admin.Dashboard.Home.Index.md): The blog admin overview: five KPI cards, eight section cards, an editorial calendar and three summary panels, every number read from the database by `MishkaBlog.Dashboard.Overview`. - [MishkaBlogWeb.Live.Admin.Dashboard.Post.Index](MishkaBlogWeb.Live.Admin.Dashboard.Post.Index.md): The blog posts list: a Gervaz table drawn as `MishkaBlogWeb.Templates.PostCard`, its create/edit form, and the version history of one post. - [MishkaBlogWeb.Live.Admin.Dashboard.Tag.Index](MishkaBlogWeb.Live.Admin.Dashboard.Tag.Index.md): The tags grid: a Gervaz table drawn as `MishkaBlogWeb.Templates.TagCard`, its create/edit form, and the version history of one tag. - [MishkaBlogWeb.Live.Admin.Dashboard.Team.Index](MishkaBlogWeb.Live.Admin.Dashboard.Team.Index.md): The teams grid: a Gervaz table drawn as `MishkaBlogWeb.Templates.TeamCard`, its create/edit form, and the version history of one team. - [MishkaBlogWeb.Live.Admin.Dashboard.VersionsModal](MishkaBlogWeb.Live.Admin.Dashboard.VersionsModal.md): The version history of a dashboard list page: a modal that lists one record's versions a page at a time, previews one of them, and restores one. The page keeps the template, its `:versions_modal` and the one-line event handlers; everything they do is here. - [MishkaBlogWeb.Routes](MishkaBlogWeb.Routes.md): Extension routes for MishkaBlog. - [MishkaBlogWeb.SidebarMenu](MishkaBlogWeb.SidebarMenu.md): The blog's admin pages and the one sidebar entry that links to them — a `"sidebar:menus"` plugin (`MishkaCmsCore.Sidebar`). - [MishkaBlogWeb.Templates.BloggerCard](MishkaBlogWeb.Templates.BloggerCard.md): The bloggers grid: one card per blogger, in as many columns as fit the screen. - [MishkaBlogWeb.Templates.CollectionCard](MishkaBlogWeb.Templates.CollectionCard.md): The collections grid: one card per collection, in as many columns as fit the screen. - [MishkaBlogWeb.Templates.CommentCard](MishkaBlogWeb.Templates.CommentCard.md): The comments list: one full-width row per comment, stacked. - [MishkaBlogWeb.Templates.EditionChips](MishkaBlogWeb.Templates.EditionChips.md): The language chips of a translated blog record: one per edition, the record's own first, each opening that edition. - [MishkaBlogWeb.Templates.PostCard](MishkaBlogWeb.Templates.PostCard.md): The blog posts list: one full-width row per post, stacked. - [MishkaBlogWeb.Templates.TagCard](MishkaBlogWeb.Templates.TagCard.md): The tags grid: one compact chip per tag, packed several to a row. - [MishkaBlogWeb.Templates.TeamCard](MishkaBlogWeb.Templates.TeamCard.md): The teams grid: one card per editorial team, in as many columns as fit the screen.