A secure, self-hosted PHP Markdown viewer and browser that turns plain
.mdfiles into polished documentation pages with a searchable file browser, auto-generated table of contents, heading numbering, Mermaid diagrams, glossary tooltips, responsive reading controls, dark mode, a built-in self-updater, file upload, clipboard preview, and server-side configuration. No build step. No database. No framework.
MD.Viewer is built around md.php, a single PHP entry point that renders a matching Markdown file, or switches to a recursive Markdown file browser when no matching file exists. The project is designed for simple deployment: copy files to a PHP host, place your .md documents nearby, and open md.php in a browser.
The viewer focuses on self-hosted documentation without a toolchain. It supports readable typography, heading numbering, automatic table of contents generation, Mermaid rendering with auto-repair and English console diagnostics, shared code and quote copy controls, glossary tooltips, dark mode, mobile-friendly controls, and a Settings panel that stores viewer preferences locally.
updater.php is a companion self-update and maintenance engine. It checks GitHub raw files using ETag and SHA-256, creates versioned backups before replacement, restores previous versions from the Settings panel or directly via URL, manages an optional index.php hard link, handles .md file uploads, and supports direct browser-based updates and rollbacks.
Server-side behavior is controlled through a .md.ini configuration file placed next to md.php. This file hard-locks feature flags that cannot be overridden from the browser or Settings panel.
# Clone into your web root or any subdirectory
git clone https://github.com/paulmann/MD.Viewer.git /var/www/html/docs
# Create a Markdown file that matches the script name
echo "# Hello, World\n\nThis is my documentation." > /var/www/html/docs/md.md
# Open in a browser
# https://your-domain.com/docs/md.phpMinimal setup:
- Place
md.php(and theassets/folder) next tomd.md. - Open
md.phpin a browser. - The script automatically looks for a Markdown file with the same base name.
Optional:
- Add
updater.phpto enable in-app update checks, apply updates, backups, restore, file uploads, andindex.phplink management. - Set
ALLOW_UPDATE = truein.md.inito enable the update system (disabled by default).
The fastest way to install is to upload only updater.php to your server, then open it in a browser with ?update=true. It will:
- Create
.md.iniwith safe defaults automatically. - Download all tracked files (
md.php,assets/js/*,assets/css/*,README.md,LICENSE) from GitHub. - Create all required subdirectories (
assets/js/,assets/css/) with0755permissions if they do not exist. - Place itself in the final list and show a full result page.
Requirement: ALLOW_UPDATE = true must be set in .md.ini before running (or add it manually to the auto-created .md.ini and reload).
# Upload only updater.php, then:
curl -o updater.php https://raw.githubusercontent.com/paulmann/MD.Viewer/main/updater.php
# Edit .md.ini (auto-created on first hit) and set ALLOW_UPDATE = true
# Then open: https://your-domain.com/updater.php?update=true- PHP 8.3+.
- PHP extensions:
mbstringandcurl(required for updater and clipboard). - A web server such as Apache, Nginx, Caddy, or the PHP built-in server.
- Browser-side internet access for CDN assets (Mermaid, syntax highlighting, Tailwind) unless replaced with local copies.
git clone https://github.com/paulmann/MD.Viewer.git
cd MD.Viewer- Download the repository or release archive.
- Copy
md.php,updater.php,assets/, and optionallyREADME.mdinto your target directory. - Add your Markdown files in the same directory tree.
- Open
md.phpin a browser..md.iniis created automatically on first load.
- Upload only
updater.phpto your server directory. - Open
updater.phponce in a browser — it auto-creates.md.iniwithALLOW_UPDATE = false. - Edit
.md.inion the server: setALLOW_UPDATE = true. - Open
https://your-domain.com/updater.php?update=true. - The updater downloads all files, creates
assets/js/andassets/css/directories as needed, and shows a result page. - Open
md.php— your MD.Viewer is ready.
cd /path/to/MD.Viewer
php -S localhost:8080
# then open http://localhost:8080/md.phpserver {
listen 80;
server_name docs.example.com;
root /var/www/html/docs;
index index.php md.php;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}MD.Viewer/
├── md.php # Main viewer / browser script v2.8.2
├── updater.php # Self-updater, backup, upload v3.8.2
├── .md.ini # Server-side config (auto-created)
├── md.md # Default Markdown file for md.php
├── uploads.md/ # Created automatically for uploaded/saved files
├── md.backup/ # Created automatically when updates/restores run
│ ├── 2.8.2/
│ ├── 2.8.2-pre-restore/ # Auto-created before each rollback
│ └── .state/ # ETag + SHA-256 per-file cache
├── assets/
│ ├── css/
│ │ ├── md.css # v2.2.2
│ │ ├── settings.css # v2.8.1
│ │ └── tooltips.css # v2.4.5
│ └── js/
│ ├── md.js # v2.5.2
│ ├── settings.js # v2.8.3
│ ├── tooltips.js # v2.4.5
│ └── upload.js # v2.8.0
├── LICENSE # v1.0.0
└── README.md # v2.8.1
Key naming rule: md.php looks for md.md, docs.php looks for docs.md. Multiple independent viewer instances can share one directory tree without extra routing.
On first request, md.php (or updater.php) creates .md.ini in the same directory. This file hard-locks feature flags so they cannot be changed from the browser or Settings panel. Missing keys are automatically appended with safe defaults on every load.
; MD.Viewer server-side configuration
; Disable the "Upload .md" button in the file browser (recommended: true)
DISABLE_UPLOAD = true
; Disable the "Preview Clipboard" button in the file browser
DISABLE_CLIPBOARD = false
; Disable the "Save to File" button in clipboard preview
DISABLE_SAVE_CLIPBOARD_TO_FILE = true
; Allow updating files via updater.php?update=true or the Settings panel
ALLOW_UPDATE = false
; Allow restoring a backup via updater.php?restore=latest or ?restore=[version]
ALLOW_RESTORE = false
; Allow creating/removing the index.php hard link from the Settings panel
ALLOW_CREATE_INDEX_PHP_LINK = true| Key | Default | Effect |
|---|---|---|
DISABLE_UPLOAD |
true |
Hides and disables the Upload button |
DISABLE_CLIPBOARD |
false |
Hides and disables the Clipboard Preview button |
DISABLE_SAVE_CLIPBOARD_TO_FILE |
true |
Hides Save to File in clipboard preview |
ALLOW_UPDATE |
false |
Enables update system (Settings panel + ?update=true) |
ALLOW_RESTORE |
false |
Enables Backup & Restore section and ?restore= URL mode |
ALLOW_CREATE_INDEX_PHP_LINK |
true |
Enables Index File section in Settings panel |
All keys are auto-appended (with their default values) to existing .md.ini files on next load — upgrading never requires manual .md.ini edits.
When a matching Markdown file exists, MD.Viewer renders it as a styled document page. It derives the page title from the first top-level heading, builds a description from the first paragraph, renders a table of contents, and applies local viewer preferences.
When no matching Markdown file exists, MD.Viewer switches to a recursive Markdown browser. The browser scans the current directory tree, lists Markdown files in a searchable table, and opens the selected file with ?file=relative/path.md.
The file browser includes a Clipboard Preview button. Paste any Markdown text into the textarea and click Preview — the content is rendered in viewer mode. If DISABLE_SAVE_CLIPBOARD_TO_FILE = false, a Save to File bar appears with a filename input and Save button that writes to uploads.md/.
| Parameter | Meaning | Example |
|---|---|---|
?file=path/to/doc.md |
Render a specific Markdown file | md.php?file=docs/api.md |
- Headings, paragraphs, emphasis, strong text, inline code, blockquotes, horizontal rules, fenced code blocks.
- Mermaid auto-repair converts PlantUML-style class notes, literal
\n, unclosed<br>, PlantUMLreturn, and unquoted parentheses; diagnostics are English-only. - Ordered and unordered lists, including nested lists.
- Tables with styled output.
- Task lists, footnotes, reference-style links.
- Images and standard links.
- Emoji shortcode support.
- Universal inline patterns for polished document formatting.
- Auto-generated table of contents.
- Optional automatic heading numbering.
- Title splitting by colon for cleaner hero titles.
- Searchable file browser mode.
- Syntax-highlighted fenced code blocks with copy button.
- Mermaid diagrams.
- Superscript/subscript support.
Inline glossary tooltips via assets/js/tooltips.js and assets/css/tooltips.css. Terms show a tooltip on hover; the engine resolves variant spellings to the same definition. Includes touch support, 2-second grace period, and smart viewport-aware positioning.
Every toggle in the Features section of the Settings panel shows a detailed hover tooltip explaining what the feature does and how it affects rendering. Tooltips use the same .g-tooltip bubble and positioning engine as Glossary Tooltips — same visual style, same dark-mode support, same touch/keyboard behavior. No additional CSS is required.
- Dark mode.
- Adjustable page width, font size, and line height.
- Header toolbar visibility settings (desktop/mobile independently).
- Persistent preferences stored in browser cookies.
If DISABLE_UPLOAD = false in .md.ini, the file browser shows an Upload .md button. Uploaded files are saved to uploads.md/. Upload protection is three-layered:
- UI button hidden/disabled when
DISABLE_UPLOAD = true. - JS click handler early-returns if
MDV_CONFIG.disableUploadis true. - PHP
updater.php: reads.md.iniindependently, returns403if disabled.
Paste Markdown text and render it instantly without saving a file. If DISABLE_SAVE_CLIPBOARD_TO_FILE = false, a Save to File bar appears with filename input, Save button, and inline status feedback. Source text is stored in sessionStorage when preview opens.
The Settings panel (opened from the header gear icon) includes:
- Viewer preference toggles (theme, width, font, line height, TOC, numbering).
- Header toolbar visibility controls per device type.
- PHP-side feature toggles with descriptive hover tooltips (applied on next page load via cookies).
- Update check and apply controls (visible only when
ALLOW_UPDATE = true). - Backup list and restore controls (visible only when
ALLOW_RESTORE = true). index.phphard-link management (visible only whenALLOW_CREATE_INDEX_PHP_LINK = true).
updater.php provides three ways to update and manage files.
Opening updater.php without any parameters shows an information page with:
- Present / missing status for every tracked file with version numbers.
- Each filename links to the raw file on GitHub (opens in new tab).
.md.inistatus: shows a warning ifALLOW_UPDATE = falsewith edit instructions.- Action buttons (Check & Apply Updates, ↺ Force Reinstall All, ⟲ Restore Latest Backup) — appear only when the corresponding flag is enabled in
.md.ini.
- Check for updates across all tracked files, with per-file version display.
- Apply updates — backs up current files, downloads new versions.
- Force reinstall all files regardless of version (bypasses ETag/SHA-256 cache).
- View and restore backup versions (visible only when
ALLOW_RESTORE = true).
Requires ALLOW_UPDATE = true in .md.ini.
https://your-domain.com/updater.php?update=true
https://your-domain.com/updater.php?update=true&force=true ← force reinstall
Two-phase bootstrap:
- Phase 1 — updates
updater.phpitself first. If the file changed, redirects to?_phase=2(with&force=truepreserved if set) so the new updater handles the rest. - Phase 2 — updates all remaining tracked files with version delta display.
&force=true flag:
- Skips ETag and SHA-256 cache entirely — always downloads each file from GitHub.
- Useful after a failed partial update or when local files are corrupted.
- Propagated automatically from phase 1 to phase 2 via redirect URL.
- An amber ↺ Force reinstall all button appears in the result page footer.
Directory auto-creation:
The updater creates any missing subdirectories (assets/js/, assets/css/) with 0755 permissions before writing files. This makes ?update=true work from a one-file install.
Result page:
- Per-file status badges:
updated/force-updated/created/current/error - Version delta:
2.7.0 → 2.8.2 - Each filename is a clickable link to the raw file on GitHub (opens in new tab)
- Footer: ↺ Force reinstall all button + ← Back button
Requires ALLOW_RESTORE = true in .md.ini.
https://your-domain.com/updater.php?restore=latest
https://your-domain.com/updater.php?restore=2.7.0
| URL | Behavior |
|---|---|
?restore=latest |
Scans md.backup/, picks the highest semver version automatically |
?restore=X.Y.Z |
Restores the exact named backup version |
Rollback process:
- Validates
ALLOW_RESTORE = true— shows error page if false. ?restore=latestscansmd.backup/, sorts by semver, picks the newest.- Validates the version string (alphanumeric, dots, dashes only).
- Backs up current files to
md.backup/[version]-pre-restore/before overwriting — rollback of a rollback is always possible. - Invalidates per-file ETag cache (
.state/) so next update check does a full fetch. - Outputs HTML result page with
restored/skipped/errorbadges, version delta, and clickable file links.
All files managed by the updater (checked, downloaded, backed up):
md.php v2.9.3
updater.php v3.8.2
assets/js/md.js v2.5.2
assets/js/settings.js v2.8.7
assets/js/tooltips.js v2.4.5
assets/js/upload.js v2.8.0
assets/css/md.css v2.2.2
assets/css/settings.css v2.8.1
assets/css/tooltips.css v2.4.5
README.md v2.9.5
LICENSE v1.0.0
Before each file replacement (update or force reinstall), the current local file is copied to md.backup/[version]/. The Settings panel lists backup versions, dates, and file counts (visible only when ALLOW_RESTORE = true). Restoring first backs up the current version (as [version]-pre-restore), then writes the selected files back. README.md and LICENSE are downloaded and updated but not backed up (they are docs, not code).
All security-sensitive actions are protected at three independent levels: server .md.ini flag, PHP API guard, and client-side UI suppression.
| Feature | .md.ini flag |
PHP guard | UI suppression |
|---|---|---|---|
| Update (apply/check) | ALLOW_UPDATE |
requireAllowUpdate() |
Hidden when false |
| Restore backup | ALLOW_RESTORE |
requireAllowRestore() |
Hidden when false |
| List backups | ALLOW_RESTORE |
requireAllowRestore() |
Not called when false |
| Create index link | ALLOW_CREATE_INDEX_PHP_LINK |
requireAllowIndexLink() |
Hidden when false |
| Remove index link | ALLOW_CREATE_INDEX_PHP_LINK |
requireAllowIndexLink() |
Hidden when false |
| Index status | ALLOW_CREATE_INDEX_PHP_LINK |
Returns {disabled:true} |
Not called when false |
| File upload | DISABLE_UPLOAD |
403 if true | Button hidden |
| Save clipboard | DISABLE_SAVE_CLIPBOARD_TO_FILE |
403 if true | Button hidden |
Additional protections:
.md.iniis never served to the browser — it is read server-side only.- Path traversal is prevented in all file-serving, upload, and save operations.
- Upload / Save to File validate filenames server-side (
.mdextension only, no slashes, no null bytes). - CORS guard in
updater.phprejects cross-origin requests. - All mutating actions require
POST(except direct URL modes which require their respectiveALLOW_*flags). - Version strings in
?restore=are validated against[a-zA-Z0-9.\-]only.
Released under the MIT License. See LICENSE.