How to serve a markdown to html python page — README, changelog, or docs — with zero dependencies using StayPresent's built-in renderer.
Turn Your README Into a Live Website with StayPresent's Markdown Renderer
Most bots already have a README.md or CHANGELOG.md sitting in the repo, fully written and maintained, but invisible to anyone who isn't looking at the source code. StayPresent's web.markdown() turns any markdown to html python file into a styled, GitHub-like web page — served directly from your bot's own HTTP server, with zero extra dependencies.
Table of Contents
- Why Serve Markdown Directly
- Basic Usage
- What the Renderer Supports
- Light, Dark, and Auto Theming
- Favicon, Title, and Description
- Security: Escaping and URL Filtering
- What It Doesn't Support
- Full Example
- Best Practices
- Common Mistakes
- FAQs
- Conclusion
Why Serve Markdown Directly
A CHANGELOG.md is genuinely useful to end users and other developers — but only if they can actually read it without cloning the repo. Since StayPresent's HTTP server already exists for keep-alive purposes, serving that same Markdown file as a proper web page costs nothing extra and requires no separate documentation site or static site generator.
Basic Usage
import staypresent
staypresent.web.markdown("CHANGELOG.md")
staypresent.run("bot.py")
Like html(), the file is read and re-rendered fresh on every request — editing CHANGELOG.md on disk is reflected the next time someone loads the page, with no restart required.
Signature:
staypresent.web.markdown(
file_path: str,
path: str = "/",
mode: str = "auto",
favicon: str = None,
title: str = None,
description: str = None,
) -> None
file_path must exist at call time — a typo raises FileNotFoundError immediately rather than only 404ing once a browser requests the page.
What the Renderer Supports
Everything is handled by StayPresent's own built-in renderer — no markdown package or any other dependency is required. It covers the constructs people actually use in real READMEs and docs:
- ATX and Setext headings (levels 1–6), each with a stable, GitHub-compatible anchor
id. - Bold, italic, bold-italic, and strikethrough.
- Inline code spans, including multi-backtick spans.
- Links, images, and bare autolinks — including URLs with a single level of balanced parentheses.
- Unordered and ordered lists, including nesting, multi-paragraph items, tight/loose rendering, custom start numbers, and GitHub-style task-list checkboxes (
- [ ]/- [x]). - Nested blockquotes.
- Fenced and indented code blocks, with language-tag syntax highlighting hooks.
- GitHub-flavored pipe tables, including per-column alignment.
- Horizontal rules and hard line breaks.
- Raw HTML passthrough for recognized block-level tags — which is what makes a centered logo/badge header at the top of a README render correctly.
- Backslash escapes for literal punctuation.
Nested inline content — a code span or bold text inside a link, or an image inside a link (the common "clickable badge" pattern) — is fully supported:
[](https://example.com)
renders as a proper clickable image link, not broken markup.
Light, Dark, and Auto Theming
mode controls how the rendered page handles color scheme:
staypresent.web.markdown("guide.md", mode="dark")
-
mode="auto"(default): the page includes<meta name="color-scheme" content="light dark">with no forced theme — the visitor's own OS/browser preference decides. -
mode="light"/mode="dark": forces that theme for every visitor via adata-themeattribute, regardless of their own system setting.
mode is case-insensitive and whitespace-tolerant (" Dark " works fine); anything outside "light"/"dark"/"auto"/None raises ValueError.
Favicon, Title, and Description
staypresent.web.markdown(
"docs/guide.md",
path="/docs",
favicon="favicon.png",
title="Project Docs",
description="Everything you need to get started.",
)
-
faviconaccepts either a direct URL (used as-is) or a relative path next tofile_path— resolved the same way neighboring static assets already are, and checked for existence immediately, just likefile_pathitself. -
titlesets both<title>and anog:titleOpen Graph tag, falling back to the Markdown file's own filename if omitted. -
descriptionadds a<meta name="description">tag plusog:description— useful for link-preview cards when the page gets shared in a chat app or on social media.
Security: Escaping and URL Filtering
Plain text and every recognized Markdown construct is HTML-escaped before rendering, so a .md file containing <script> tags or stray </>/& characters can't inject markup into the page — only a fixed list of recognized block-level raw-HTML tags is ever passed through unescaped.
Link and image destinations are additionally checked against a scheme blocklist, independent of the escaping above: javascript: and vbscript: are rejected outright for both links and images, file: is rejected for both, and data: is rejected for links specifically (while still permitted for images, since inline data: images are a common, inert pattern). A rejected URL simply falls back to plain, escaped text rather than becoming a working link.
Neighboring files are exposed the same way
html()exposes them. Everything infile_path's directory becomes servable as a static asset — not only the files actually referenced from the page. Keep the Markdown file's directory limited to files you're comfortable being publicly reachable; StayPresent logs a one-timeWARNINGthe first time a directory gets exposed this way.
What It Doesn't Support
This is a lightweight renderer, not a full CommonMark implementation. It intentionally does not support: footnotes, definition lists as distinct syntax, custom containers/admonitions, LaTeX/math, reference-style links ([text][ref] + a separate [ref]: url line), or blockquote lazy continuation. For any of these, pre-render your Markdown with a different tool and serve the resulting HTML with web.html() instead.
Full Example
import staypresent
staypresent.web.markdown(
"README.md",
path="/",
mode="auto",
title="My Bot",
description="A Telegram bot for X, Y, and Z.",
)
staypresent.web.markdown(
"CHANGELOG.md",
path="/changelog",
mode="dark",
)
staypresent.run("bot.py")
Best Practices
- Keep
.mdfiles intended for public serving in a directory that contains only files you're fine exposing — the whole directory becomes reachable, not just the file you calledmarkdown()on. - Use
mode="auto"unless you have a specific brand reason to force a theme — it respects the visitor's own preference with zero extra effort. - Set
title/descriptionfor any page you expect to get linked or shared, so previews render sensibly instead of falling back to a bare filename.
Common Mistakes
-
Expecting reference-style links (
[text][ref]) to work. They don't — only the inline[text](url)form is supported; rewrite reference-style links before serving. -
Placing sensitive files in the same directory as a publicly served
.mdfile. They become reachable via the same static-asset mechanism, regardless of whether anything links to them. -
Assuming a relative
faviconresolves against the current working directory. It resolves relative tofile_path's own directory, not the process's cwd.
Conclusion
Turning markdown to html in Python doesn't require pulling in a templating engine or a separate documentation build step. staypresent.web.markdown() renders your existing README.md or CHANGELOG.md into a clean, GitHub-styled page — complete with theming, favicon, and link-preview metadata — using nothing beyond what StayPresent already ships with.
pip install staypresent[prod]
Top comments (0)