Skip to content

sen-ltd/markdown-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

markdown-api

A tiny HTTP service that renders Markdown to HTML. PHP 8.2, Slim 4, league/commonmark. One endpoint, safe-by-default, GFM-capable, with heading extraction for table-of-contents generation.

Rendering Markdown is a need every CMS preview, doc site, forum, and AI chat UI eventually grows. Teams usually bolt a markdown library into each app that needs it. A ~100-line HTTP service is the clean separation.

Endpoints

  • POST /render — JSON in, JSON out (HTML + word count + headings)
  • POST /render/html — JSON in, raw HTML out (no envelope; drop-in for innerHTML)
  • GET /render?text=... — same thing for short inputs you can curl by hand
  • GET /health — liveness + version probe
  • GET / — a minimal textarea + live preview demo page

Full request/response schema: openapi.yaml.

Request shape

{
  "markdown": "# Hello\n\nThis is **bold**.",
  "flavor": "gfm",
  "safe": true
}
  • flavor: "gfm" (default) or "commonmark". GFM adds tables, task lists, strikethrough, and bare-URL autolinks on top of the CommonMark spec.
  • safe: true (default) escapes raw HTML inside the markdown source so that user input can't inject <script> tags. Set to false only if you trust the input — internal docs pipelines, trusted content authors, etc.

Response shape

{
  "html": "<h1>Hello</h1>\n<p>This is <strong>bold</strong>.</p>\n",
  "word_count": 4,
  "headings": [
    { "level": 1, "text": "Hello", "anchor": "hello" }
  ]
}

The heading list is extracted from the rendered HTML, not the source markdown, so setext headings and HTML blocks are handled consistently. Duplicate anchors are disambiguated with -1, -2, ... suffixes so you can feed the result straight into a table-of-contents renderer.

Run it

From source

composer install
composer serve
# → http://localhost:8000

With Docker

docker build -t markdown-api .
docker run --rm -p 8000:8000 markdown-api

Image is a multi-stage build on alpine:3.19 + PHP 8.2 copied from the official image and trimmed. Runs as a non-root user. Final image weighs under 120 MB.

Environment

var default meaning
MAX_MARKDOWN_LEN 100000 Max input length in bytes. 413 on overflow.

Try it

curl -sS -X POST http://localhost:8000/render \
  -H "Content-Type: application/json" \
  -d '{"markdown": "# Hello\n\nThis is **bold** and *italic*.\n\n- a\n- b\n- c"}'
# raw HTML for direct innerHTML insertion
curl -sS -X POST http://localhost:8000/render/html \
  -H "Content-Type: application/json" \
  -d '{"markdown": "- [ ] task\n- [x] done"}'
# safe mode (default) strips <script> tags
curl -sS -X POST http://localhost:8000/render \
  -H "Content-Type: application/json" \
  -d '{"markdown": "<script>alert(1)</script>"}'
# → {"html":"&lt;script&gt;alert(1)&lt;/script&gt;", ...}

Tests

composer install
vendor/bin/phpunit --no-coverage

Or inside the built image:

docker run --rm --entrypoint /app/vendor/bin/phpunit markdown-api \
  --no-coverage -c /app/phpunit.xml

29 PHPUnit tests cover the renderer, heading extractor, middleware, and every HTTP route (including safe/unsafe mode, empty input, oversize input, invalid flavor, GFM tables/tasks/strikethrough/autolinks, and duplicate-anchor disambiguation).

Design notes

  • Slim 4, not Laravel. Slim is the minimal PSR-7 glue that exists for microservices exactly like this one. No ORM, no queue, no service container to learn. Three files do the whole thing.
  • league/commonmark as the renderer. It is the CommonMark-compliant reference implementation in PHP, and its GFM extensions are first-class — not bolted on.
  • Safe mode on by default. If you forget to pass "safe": false you still get a safe-to-embed output. You have to opt out explicitly.
  • Heading extraction as a bonus endpoint payload. Computing the TOC is cheap if you already have the rendered HTML, and it's what most downstream consumers want next anyway.
  • PHP's built-in web server is the default runtime. For an internal service rendering a few requests per second behind a CMS preview route, there is no reason to drag in FPM, Apache, or nginx. Put a cache in front and forget about it.

License

MIT. Copyright (c) 2026 SEN 合同会社.

Links

About

A Markdown → HTML rendering HTTP service built on PHP 8.2 + Slim 4 + league/commonmark. POST markdown to one endpoint, get back HTML + word count + a flat heading list with anchor slugs.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors