magebitcom / module-documentation

magebitcom/module-documentation

Module documentation viewer for Magento 2 admin

magento2-module Compatibility: 2.4.7-2.4.8 Code Quality: Warning Tests: N/A Security: Pass MIT

Are you the maintainer of magebitcom?

Packagento pulls magebitcom's Composer packages from the public registry so buyers can find them here.

Claim the namespace to take ownership, publish new releases directly, and start charging for premium versions.

Claim this namespace →

Magebit_Documentation

A Magento 2 admin documentation viewer. Any module can drop Markdown files into a folder, register
that folder in etc/documentation.xml, and the pages appear under Docs in the admin menu —
rendered, searchable, and permission-aware.

Requirements

  • PHP 8.1 or newer
  • Magento 2.4 (magento/framework ^103.0)

Installation

composer require magebitcom/module-documentation
bin/magento module:enable Magebit_Documentation
bin/magento setup:upgrade
bin/magento cache:flush

Open Docs in the admin menu. The menu item is top level, not nested under System.

Quick start

Put Markdown files in your module and point a documentation.xml at them.

app/code/Vendor/Module/
├── Docs/
│   ├── 1-getting-started.md
│   ├── 2-configuration.md
│   └── images/
│       └── flow.png
└── etc/
    └── documentation.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magebit_Documentation:etc/documentation.xsd">
    <module name="Vendor_Module" title="My Module" sortOrder="10">
        <documentation name="Guide" path="Docs" sortOrder="10"/>
    </module>
</config>

Then clear the right cache. After a documentation.xml change, run bin/magento cache:clean config
the merged configuration is stored in the config cache, so cleaning magebit_documentation alone will
not pick it up. After adding, renaming or removing a Markdown file,
bin/magento cache:clean magebit_documentation is enough. See Caching for the whole
picture.

documentation.xml

<module>

Attribute Required Description
name yes Module the sections belong to, e.g. Vendor_Module. Groups the sidebar.
title no Label in the sidebar. Falls back to the module name.
sortOrder no Position among modules, lower first. Default 100; ties break on title.
icon no View-asset path of an icon shown beside the module title in the documentation sidebar, e.g. Vendor_Module::images/icon.svg. Not the admin menu icon.

<documentation>

One folder of Markdown pages.

Attribute Required Description
name yes Section label in the sidebar. Also the key that merges declarations.
path yes Folder holding the Markdown files. See Paths.
acl no ACL resource an admin must hold to see the section.
sortOrder no Position among the module's sections, lower first. Default 100.

<changelog>

One Markdown file rather than a folder — for a CHANGELOG.md that already sits in the module root.

<module name="Vendor_Module" title="My Module">
    <documentation name="Guide" path="Docs"/>
    <changelog path="CHANGELOG.md"/>
</module>
Attribute Required Description
path yes Markdown file, relative to the module or written as Vendor_Other::CHANGELOG.md.
name no Section label. Default Changelog.
acl no ACL resource an admin must hold to see the section.
sortOrder no Position among the module's sections. Default 1000, so it lands last.

Paths

path is resolved against the module that declared it, or against another module when it is written
as Vendor_Other::Docs:

<documentation name="Guide" path="Docs"/>
<documentation name="API"   path="Docs/Api"/>
<documentation name="Notes" path="Vendor_Other::Docs"/>

A path may not leave the module it resolves against — ../OtherModule/Docs is refused, and so is any
.. that would climb out. Use the Vendor_Other:: form instead.

A section whose path does not resolve is dropped from the tree without an error, which is what
magebit:documentation:validate catches. A section whose path resolves but whose folder holds no
Markdown is dropped too; validate passes on that one, so check magebit:documentation:list — a
section missing from that table has an empty folder.

Adding sections to another module's group

A satellite module can file its own pages under the parent's heading by declaring the parent's name:

<!-- Vendor_SubModule/etc/documentation.xml -->
<module name="Vendor_Module">
    <documentation name="Sub Module" path="Vendor_SubModule::Docs" sortOrder="40"/>
</module>

Writing pages

File names

  • A numeric prefix sets the order and is stripped from the label: 1-getting-started.mdGetting Started.
  • - and _ become spaces, and each word is capitalised.
  • index.md and readme.md are the section or folder overview: they are labelled Overview and sort first.
  • Sub-folders become collapsible categories, named by the same rules. Nesting stops at 10 levels.
  • Files that are not .md are not pages. They can still be linked to as images.

Front matter

An optional YAML block at the very top of a page overrides the two things the file name decides:

---
title: Getting started with orders
order: 20
---

# Getting started
Key Type Effect
title string Replaces the label derived from the file name. Blank values are ignored.
order integer Replaces the numeric file-name prefix.

order must be an unquoted integer. order: "20" is a string, and a string is rejected: the page
keeps its file-name order and a warning naming the key and the file is written to the system log. Only
the first 8 KB of a file is read for front matter, so keep the block at the top.

Markdown

CommonMark with GitHub Flavored Markdown — tables, task lists, strikethrough, autolinks — plus heading
permalinks, footnotes and a table of contents. Raw HTML in a page is stripped, and unsafe link schemes
are refused. Links that leave the site open in a new tab and carry a small arrow.

Headings automatically fill the On this page panel beside the content, and the panel follows the
reader down the page.

GitHub-style callouts work as they do on GitHub — a blockquote opening with [!NOTE], [!TIP],
[!IMPORTANT], [!WARNING] or [!CAUTION] becomes a coloured box with a title:

> [!WARNING]
> Cleaning the wrong cache is the most common mistake.

Links between pages

Relative links ending in .md are rewritten to point at the other page:

See [configuration](2-configuration.md) and [the API](advanced/1-api.md).

A relative link without an extension is left exactly as written — [see](configuration) is not
turned into configuration.md. Add the extension.

Absolute URLs, anchors and mailto: links pass through untouched.

Images

Images are served by an admin controller, not by static content, so they do not need a deploy:

![Order flow](images/flow.png)

The path is resolved relative to the page and must stay inside the section folder — an image
outside it, or above it via .., returns a 404. Allowed types are png, jpg, jpeg, gif, svg
and webp, up to 8 MB. SVGs are served with a restrictive Content-Security-Policy, so an SVG that
pulls in external resources will not render them.

The module icon from documentation.xml is the other kind of image and follows the opposite rules:
it is an ordinary static view asset, resolved through getViewFileUrl(), so it lives under
view/adminhtml/web/ and does need bin/magento setup:static-content:deploy in production mode.
Images inside your pages need no deploy; the sidebar icon does.

Code blocks

Fenced blocks are highlighted in the browser by a bundled copy of highlight.js. The common languages
(php, javascript, json, xml, yaml, sql, bash, css, diff, ini, markdown and more) work out of the box.
dockerfile, nginx and twig ship as separate files and load on demand.

A language the bundle does not know is rendered as plain text and a warning is logged in the browser
console. To have an extra language ready before the first block that needs it, list it under Extra
Languages
in the configuration.

To show a fenced block inside a fenced block, wrap the outer one in four backticks.

Diagrams

A fenced block with the language mermaid is drawn in the browser by a bundled copy of
Mermaid — flowcharts, sequence diagrams, class diagrams, Gantt charts and
the rest. Nothing is fetched from a CDN, so it works under the admin Content-Security-Policy. Mermaid is
only loaded on pages that contain a diagram. A diagram Mermaid cannot read keeps its source visible with
a short message above it.

Clicking an image or a drawn diagram opens it full screen; Escape or another click closes it.

Access control

The whole viewer is gated by Magebit_Documentation::documentation ("View Documentation"), which sits
under System in System → Permissions → User Roles.

Individual sections can require their own resource:

<documentation name="Internal" path="Docs/Internal" acl="Vendor_Module::internal_docs"/>
<!-- Vendor_Module/etc/acl.xml -->
<resource id="Magento_Backend::admin">
    <resource id="Magento_Backend::system">
        <resource id="Vendor_Module::internal_docs" title="Internal Documentation"/>
    </resource>
</resource>

A section an admin may not see is removed from the tree, from search results and from the page and
image controllers. A module left with no visible section disappears entirely.

Name a resource that no acl.xml declares and Magento falls back to the role's blanket permission:
the section stays visible to a full-access administrator and vanishes for every restricted role. That
is hard to notice by hand, so magebit:documentation:validate checks it for you.

Configuration

Stores → Configuration → Magebit → Documentation

Field Path Description
Syntax Theme magebit_documentation/appearance/highlight_theme Colour theme for code blocks.
Extra Languages magebit_documentation/appearance/extra_languages Extra highlight.js bundles loaded on every page.
Enable Search magebit_documentation/search/enabled Shows the search box above the tree.

Commands

bin/magento magebit:documentation:list

Prints every registered section with its module, title, resolved absolute path and page count. It
reads the tree unfiltered, because the command line has no admin session and an ACL-filtered tree
would come back empty. A section that does not resolve is already gone from that tree, so it will not
be listed — which is what the second command is for.

bin/magento magebit:documentation:validate

Exits 0 when every configured path resolves and every acl attribute names a resource that
acl.xml actually declares. Otherwise it prints one line per problem and exits 1. It reads the
merged documentation.xml directly, so it still sees the sections the tree has dropped.

Neither fault announces itself in the browser — a bad path removes the section for everyone, and a
bad acl removes it only for restricted roles — so run this in CI.

Caching

Three separate things are cached, and they are not all cleaned by the same command.

What Where it lives Cleaned by
The merged documentation.xml the config cache type cache:clean config
The documentation tree the magebit_documentation cache type, tagged with the config cache cache:clean magebit_documentation or cache:clean config
The search index the magebit_documentation cache type, tagged with the config cache cache:clean magebit_documentation or cache:clean config

Page text is not cached at all — it is read from disk on every request, so an edit inside a page is
visible immediately. Search will keep matching the old text until the index is rebuilt.

So, in practice:

# changed a documentation.xml — this is the one that also covers everything else
bin/magento cache:clean config

# only added, renamed or removed a Markdown file
bin/magento cache:clean magebit_documentation

The magebit_documentation type is listed as Documentation under System → Cache Management,
where it can also be switched off.

Extending

The public contracts live in Magebit\Documentation\Api. Take a preference on one to replace an
implementation.

interface DocumentationTreeInterface
{
    /** @return array<string, ModuleDocsInterface> Keyed by module name, ordered by sort order */
    public function get(): array;

    public function getSection(string $moduleName, string $sectionName): ?SectionInterface;

    /** @return array{module: ModuleDocsInterface, section: SectionInterface, page: PageInterface}|null */
    public function getFirst(): ?array;
}

interface PageRepositoryInterface
{
    public function getContent(string $moduleName, string $sectionName, string $relativePath): ?string;

    public function getContentForSection(
        string $moduleName,
        SectionInterface $section,
        string $relativePath
    ): ?string;

    /** @return array<string, mixed> */
    public function getFrontMatter(string $moduleName, string $sectionName, string $relativePath): array;
}

interface SearchIndexInterface
{
    /** @return list<SearchHitInterface> Highest score first */
    public function search(string $query, int $limit = 20): array;
}

interface MarkdownRendererInterface
{
    /** @param array{module:string,section:string,path:string} $context */
    public function render(string $markdown, array $context): string;
}

interface SyntaxHighlighterInterface
{
    public function decorate(string $html): string;
}

interface PathResolverInterface
{
    public function resolveSectionRoot(string $contextModule, string $configuredPath): ?string;

    /** @param list<string> $allowedExtensions Lowercase, without the dot */
    public function resolveFile(string $sectionRoot, string $relativePath, array $allowedExtensions): ?string;

    /** @return array{path: string, fileName: string}|null */
    public function resolveChangelogFile(string $contextModule, string $configuredPath): ?array;
}

interface DirectoryScannerInterface
{
    public function scan(string $absoluteRoot): CategoryInterface;
}

DocumentationTreeInterface, PageRepositoryInterface::getContent() and SearchIndexInterface are
all filtered by the current admin's permissions. PageRepositoryInterface::getContentForSection() is
not — it takes an already-resolved section, and the caller owns the authorization check.

The data objects returned by these — ModuleDocsInterface, SectionInterface, CategoryInterface,
PageInterface, SearchHitInterface — are read-only value objects in Magebit\Documentation\Api\Data.

Turning a configured or requested path into a disk path happens only in Model\Path\Resolver and
Model\Scanner; reading the bytes of an already-resolved path, through the injected
Filesystem\Driver\File, also happens in Model\PageRepository, Controller\Adminhtml\Asset\Index
and Model\Config\Source\ShippedFiles.

Screenshots

Screenshot1 Screenshot2 Screenshot3

Troubleshooting

Nothing changed after editing documentation.xml. You cleaned the wrong cache. That file is
merged into the config cache: bin/magento cache:clean config.

A section does not appear. Run bin/magento magebit:documentation:validate. If it passes, the
folder resolved but held no .md files, or your role lacks the section's ACL resource.

Pages are stale after adding a file. bin/magento cache:clean magebit_documentation.

The sidebar icon does not load. Unlike page images, it is a static view asset:
bin/magento setup:static-content:deploy.

A front-matter order is ignored. It was quoted. Write order: 20, not order: "20".

An image is broken. It must sit inside the section folder and end in an allowed extension.

Releasing

Releases are cut by hand. Open the Actions tab, pick the Release workflow and run it on
master — it refuses to run on any other branch.

The version number comes from the commit messages since the last tag, which are
conventional commits:

Commits since the last tag Next version
A breaking change (feat!:, or a BREAKING CHANGE: footer) major, 1.2.32.0.0
A feat: minor, 1.2.31.3.0
Anything else that is listed, such as fix:, perf:, refactor:, docs: patch, 1.2.31.2.4
Only housekeeping (ci, chore, build, test, style) nothing — the run stops and says so

Two inputs:

  • bumpauto uses the table above. Pick patch, minor or major to force it instead.
  • dry_run — works out the version and the notes, prints both in the run summary and stops. Nothing
    is committed, tagged or published.

A real run then regenerates CHANGELOG.md with git-cliff,
commits it as chore(release): release vX.Y.Z, tags that commit vX.Y.Z, pushes both, and publishes
a GitHub release whose notes are the new section of the changelog.

If master only accepts changes through pull requests, the workflow needs a way past that rule for
its one changelog commit. Give it a deploy key:

  1. Create a key pair: ssh-keygen -t ed25519 -N "" -f release-key -C "release workflow".
  2. Add release-key.pub under Settings → Deploy keys with Allow write access ticked.
  3. Add the private key (release-key) as a repository secret named RELEASE_DEPLOY_KEY, then delete
    both files locally.
  4. In Settings → Rules, make sure Deploy keys is listed as a bypass actor for the rule set that
    protects master.

The push then goes over SSH as that deploy key. Without a protected branch the secret can stay unset
and the run's own token is used.

Entry formatting lives in cliff.toml.

License

MIT. See LICENSE.

Changelog

Every entry below is generated from this module's conventional commit messages.

1.1.0 - 2026-09-02

Features

  • Add callouts, footnotes, heading tracking, image zoom and print styles (e417917)
  • Draw mermaid fences as diagrams with a bundled mermaid.js (9a483d7)

Bug Fixes

  • Push the release with a deploy key and make the commit and tag one atomic push (77bb99f)

1.0.0 - 2026-08-31

Features

  • Add list and validate commands and rewrite the public documentation (d905eec)
  • Mask an open book svg into the menu icon and shorten the rail label to Docs (cfff9be)
  • Give the documentation menu item its own admin icon (9e02514)
  • Rewrite the admin ui with a single-pass tree, toc, keyboard search and copy buttons (eac8cb1)
  • Add the page, search and image controllers with their view models (a09c8dc)
  • Search document content and headings instead of file names (e2ab6ca)
  • Highlight code blocks with the bundled highlight.js and make the theme configurable (9cf9b83)
  • Rewrite markdown links and images in the AST instead of the output HTML (e5f84d6)
  • Read page content and front matter behind the ACL-filtered tree (493237d)
  • Let front matter override the title and order a page sorts by (088e700)
  • Build the documentation tree once and cache it before ACL filtering (bc4241c)
  • Flatten documentation.xml into typed module and section records (d5c37c8)
  • Scan documentation folders into an ordered category tree (d264bd0)
  • Resolve section paths against the target module and block traversal (f3bd3e1)
  • Add immutable data contracts for the documentation tree (3539e10)

Bug Fixes

  • Ignore configured highlight languages that no longer ship (9ef6a71)
  • Send validate failures to stderr and correct the cache, icon and cache-type documentation (defbdfa)
  • Widen the narrow-width content padding so the h1 permalink stays inside it (8470a00)
  • Announce search result selection and sidebar toggle target to screen readers (efa616b)
  • Keep the current page's tree ancestors open after a reload (3cfd460)
  • Scope the search highlight mark selector to the docs page (251af13)
  • Darken the muted text colour so it clears the 4.5:1 contrast floor (3da4171)
  • Swap the pale permalink mark for a hover-revealed # that no longer shifts headings (fddc8b9)
  • Build the page with the framework factory so the admin menu block exists (510ee00)
  • Sandbox served svg documents and cap the size of a served image (e5cbb6c)
  • Drop front matter and emphasis from indexed text and clean headings before collecting them (4b5a051)
  • Load each highlight language on its own request and keep admin comments translatable (34298b9)
  • Offer only light themes and fall back when the stored theme no longer ships (959670c)
  • Support anchored and encoded documentation links and contain every render failure (847ab94)
  • Warn the author when a front matter title or order cannot be used (fe9ffd9)
  • Read front matter behind a byte order mark and always close the file handle (39da34c)
  • Return the changelog file name from the resolver and cover the missing tree cases (a7ad127)
  • Give the documentation cache its own type and clear it with the config cache (988a182)
  • Make module metadata merging last-wins and read sections in document order (2b1a9be)
  • Strip only markdown suffixes, cap scan depth and tighten the scanner tests (bdc9c66)
  • Tighten the documentation path guard and cover its refusal branches (2fd26af)
  • Exclude vendor/docs from phpstan, fix icon/categories docs, cover title fallback (0dd5996)

Refactoring

  • Rename EnvironmentFactory to EnvironmentBuilder (2978a42)
  • Remove the unused languagePath setting from the highlight loader (eda23eb)
  • Drop the last phpcs:ignore from the directory scanner and give it a class docblock (b8bfce9)
  • Drop the dark theme pairing and ship a light only viewer (64c5f53)
  • Split shipped file names on the driver's slash instead of basename (faf4cb3)
  • Remove the old implementation and add module static-analysis gates (664a3c4)

Documentation

  • Correct the icon description and say where the file driver is used (1cefe96)

0.0.1 - 2026-02-10

Bug Fixes

Versions
Version Stability QA Status Compatibility Released
v1.1.0 stable Fail Magento 2.4.7-2.4.8 Details 2026-09-02 10:05:27
v1.0.0 stable Fail Magento 2.4.7-2.4.8 Details 2026-08-31 12:19:48
v0.0.1 stable Fail Magento 2.4.7-2.4.9 Details 2026-02-10 13:24:46

Requires 6

Package Constraint
ext-dom *
ext-mbstring *
league/commonmark ^2.10
magento/framework ^103.0
php ^8.1
symfony/yaml ^5.4 || ^6.0 || ^7.0

Compatibility

Each Magento release line is installed on its supported PHP versions, then the module is built (DI compilation + static-content deploy) and its unit and integration suites are run. The matrix shows the lines and PHP versions the module is confirmed to install and run on. Code-quality results further down (phpstan, phpcs, …) are reported separately and never affect compatibility.

Compatibility matrix (Magento × PHP)
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 Pass Pass
2.4.8 Pass Pass
2.4.9 Fail dependency conflict Fail dependency conflict

Code Quality

Advisory checks against the module's source. Static analysis runs once across the whole module; PHPStan re-runs per Magento + PHP version because resolvable symbols differ between releases. These NEVER affect the Compatibility badge. A phpcs finding can't make a module incompatible.

Static analysis

Coding standards (phpcs), mess detection (phpmd), copy-pasted code (cpd), PHP cross-version compatibility, composer.json validity. Each runs once for the whole module.

Static analysis results
Tool Status Findings Summary
PHPCS Pass 0
PHPMD Warning 59 59 rule violations (UnusedPrivateField:59)
Cpd Pass 0
Composer validate Pass 0

PHPStan

Type-checks the module's PHP against a real Magento install at the configured gate level. Re-runs per Magento and PHP version because resolvable symbols differ between releases.

PHPStan results by Magento and PHP version
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 Pass Pass
2.4.8 Pass Pass
2.4.9 Pass Pass

Tests

Unit and integration suites, run for each applicable Magento and PHP version. A test failure speaks to the module's behaviour, not its compatibility with a Magento line, so it is reported here separately and never reddens the compatibility matrix.

Unit tests

Unit tests results by Magento and PHP version
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 N/A N/A
2.4.8 N/A N/A
2.4.9 N/A N/A

Integration tests

Integration tests results by Magento and PHP version
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 N/A N/A
2.4.8 N/A N/A
2.4.9 N/A N/A

Security

Security checks run directly against the module: an audit of its declared dependencies for known vulnerabilities (composer audit) and a scan of its source for malware and web-shell signatures. Each runs once. A malware detection fails the version outright.

Security results
Tool Status Findings Summary
Composer audit Pass 0
Malware scan Pass 0
License
MIT
Authors

More from magebitcom

View vendor
Make it pay

Turn an existing module into recurring revenue.

If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.