plym Docs
    plym.io

    Introduction

    plym is an open source CMS built for a world where agents are first-class citizens. It helps you build blazing-fast blogs, optimised SEO and discoverability by LLMs like ChatGPT.

    Updated Aug 04, 2026

    On this page

    The problem

    Agents read the web on your readers' behalf, and they read the same HTML you built for browsers. On plym.io the article Introducing plym is 52,734 bytes of HTML and 4,189 bytes of markdown: the markup an agent downloads and then discards is 92% of the transfer. The workaround most teams reach for — a second markdown copy, maintained by hand — drifts from the source within a release or two.

    Core philosophy

    One source, two artifacts. You author markdown once. Both outputs are generated from it, so they cannot disagree.

    Render at publish time, not request time. A published page is a file on disk. Serving it is a file read, not a template render plus three queries.

    Highly configurable Templates, colors, fonts, caching, and SEO behavior come from config.yaml and a template directory. Changing how the site looks does not mean forking the application.

    The publish pipeline

    When a post moves to published, or when you call POST /api/posts/{id}/refresh, the render pipeline writes two files under storage/.generated/.

    File Contents
    {category}/{slug}.html Markdown rendered to HTML, template applied, CSS bundle and Prism JS inlined, JSON-LD (BlogPosting, plus FAQPage when the post has FAQs), Open Graph and article meta tags, canonical URL.
    {category}/{slug}.md The post's markdown source, unmodified.

    Each file is written to a temporary path and moved into place, so a reader never sees a half-written page. Unpublishing or deleting a post removes both files.

    Content negotiation

    Caddy serves the markdown twin when text/markdown is the first media type in the request's Accept header. Every other request gets the HTML.

    curl -H 'Accept: text/markdown' https://plym.io/blog/introducing-plym
    Today we're releasing plym as open source. It's a CMS built from the ground up for
    two audiences that used to be one: people reading your site in a browser, and the
    growing number of agents reading it on their behalf.
    
    ## Why another CMS?
    curl -H 'Accept: text/html' https://plym.io/blog/introducing-plym
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Introducing plym — plym</title>

    Both responses carry Vary: Accept, so a shared cache keys the two variants separately.

    What plym does not do

    plym does not detect AI user agents. A request from ClaudeBot/1.0 carrying a browser Accept header receives HTML. Markdown delivery is opt-in by header: an agent that wants markdown asks for it. This is by design. We previously served markdown content based on the user-agent. We had a list of 48 user agents who would see markdown content, always. In fact you can still do this by adding a simple snippet in the Caddy config. This topic has attracted a lot of opinions recently. However, we decided to NOT do it because of two reasons:

    1. A bot might intend to view the actual design and layout of the page. For example, a search engine crawler that rewards you for having good web fundamentals.

    2. Content negotiation is gaining popularity and many AI agents are now requesting markdown content natively.

    Further limits:

    • Accept: text/html,text/markdown returns HTML. text/markdown has to come first in the list.

    • Only published posts have rendered files. Drafts and archived posts fall through to the application, which returns HTML or 404.

    Benchmarks

    Time to first byte, measured on a self-hosted install — Raspberry Pi 5, Docker, Caddy over loopback — 10 requests per row, median reported.

    Request TTFB
    Published page, HTML 0.8 ms
    Published page, markdown 1.0 ms
    Blog index, rendered by the API and cached in RAM 6.2 ms

    Transfer size, three articles on plym.io, uncompressed:

    Article HTML Markdown Reduction
    Introducing plym 52,734 B 4,189 B 92%
    Creating templates for plym 66,241 B 11,678 B 82%
    How AI search actually works 60,359 B 10,714 B 82%

    A rendered page carries roughly 48 KB of inlined CSS and Prism JS. That is what buys the single-request paint: the HTML references no external stylesheet and no external script. Self-hosted fonts, images, and the favicon are the only follow-up requests, and all three are served with Cache-Control: public, max-age=31536000, immutable.

    Architecture

    Three containers, orchestrated by Docker Compose.

    Service Image Role
    caddy caddy:2-alpine Front door. Compresses with zstd/gzip, serves .generated/ and static assets off disk, proxies everything dynamic to the API.
    api plymio/plym:latest FastAPI application: authoring API, render pipeline, auth, migrations, scheduled backups.
    db postgres:16-alpine Posts, users, media metadata, tags, categories.

    A fourth service, mcp, ships in the same image and stays off until you run plym enable mcp. It exposes the authoring API as an MCP server so agents can create and update posts.

    On each boot the API applies pending SQL migrations, ensures the superuser exists, reconciles .generated/ against the database to delete orphaned files, then builds runtime assets: self-hosted webfonts, Prism for your configured languages, and a minified CSS bundle. Asset steps fail soft — a font that will not download logs a warning and serving continues.

    How plym compares

    Renders on Database Publishing one post
    WordPress, Ghost Every request, behind a cache Yes Cache invalidation
    Hugo, Jekyll Build No Rebuild and redeploy the whole site
    Headless CMS Your frontend, per request Yes Whatever your frontend does
    plym Publish Yes Re-renders that one post

    plym keeps what static generators lack — a running API, an admin portal, per-post rendering — and keeps what request-time systems lack: pages that are already files by the time a reader asks for one.

    SEO output

    Three paths are served on every install with no configuration.

    Path Contents
    /blog/sitemap.xml Every published post with lastmod, read from the database in pages of 1,000 rows.
    /blog/robots.txt The Disallow list from config.yaml, plus a Sitemap: line. Set robots.serve: false to stop serving it.
    /blog/llms.txt Site name, description, and every published post as a markdown link with its excerpt.

    Per page you get a canonical URL, overridable per post, Open Graph and article meta tags, and JSON-LD.

    Licensing and hosting

    plym is MIT licensed and self-hostable: one curl command brings up the stack on any host with Docker. plym Cloud is the managed option, with a 99.99% SLA and 300+ edge locations. Both run the same application and the same rendering pipeline.