DEV Community

D A Blackburn
D A Blackburn

Posted on

Humans and Machines read differently, I think I have a fix?

The Setup

CSS exists because we figured out that a phone and a desktop should not get the same experience. Same content, different presentation. Media queries let us define what "reading" means for each context, and the browser figures out the rest.

We never applied that thinking to AI agents. We kept serving them the same cluttered HTML we serve humans: nav bars, footers, sidebars, cookie banners, and all of it. Then we wondered why LLMs hallucinate structure that was never really there.

I am building something to fix that. I am calling it Cascading Markdown Sheets.


What I Built

MagpieCSS is a CSS and JavaScript design system I extracted from a larger project MagpieStash. While building out the framework, I added a namespace to the main MagpieCSS object called cms. Its primary method is toMarkdown().

If the requesting agent identifies itself as an AI agent, instead of serving the rendered HTML, the framework converts the DOM subtree to clean Markdown and serves that instead. Same content, different presentation.

The "Cascading" part is intentional. Just as CSS defines how a page looks based on context, Cascading Markdown Sheets define how a page reads based on who is asking.

Here is the actual implementation from dist/magpie.js:

// MagpieCSS.cms — Cascading Markdown Sheets
// Recursively compiles a DOM subtree into clean, token-efficient Markdown.
// Strips layout chrome, nav, copy buttons, and theme controls.
// Preserves semantic structure: headings, tables, code blocks, lists, and trees.

toMarkdown: function(element, indentLevel = 0) {
    const cmsRef = this;
    const recurse = (el, level) => {
        if (!el) return '';

        // Strip layout chrome that agents do not need
        if (el.classList && (
            el.classList.contains('hide-print') ||
            el.classList.contains('copy-btn') ||
            el.classList.contains('code-block-copy') ||
            el.classList.contains('mobile-menu-toggle') ||
            el.classList.contains('dropdown-content') ||
            el.classList.contains('theme-btn')
        )) return '';

        const tagName = el.tagName ? el.tagName.toLowerCase() : '';
        if (tagName === 'script' || tagName === 'style') return '';

        // MagpieCSS component-aware serialization
        if (el.classList?.contains('code-block')) {
            const lang = el.querySelector('.code-block-lang')?.innerText.trim().toLowerCase() || '';
            const code = el.querySelector('code')?.innerText.trim() || '';
            return `\n\`\`\`${lang}\n${code}\n\`\`\`\n\n`;
        }

        // Standard semantic elements
        if (tagName === 'h1') return `\n# ${el.innerText.trim()}\n\n`;
        if (tagName === 'h2') return `\n## ${el.innerText.trim()}\n\n`;
        if (tagName === 'h3') return `\n### ${el.innerText.trim()}\n\n`;
        if (tagName === 'p')  return `${cmsRef._parseChildren(el, level)}\n\n`;

        // Table serialization to Markdown pipe syntax
        if (tagName === 'table') {
            const rows = Array.from(el.querySelectorAll('tr'));
            if (!rows.length) return '';
            const headers = Array.from(rows[0].querySelectorAll('th, td'));
            let md = '| ' + headers.map(h => h.innerText.trim()).join(' | ') + ' |\n';
            md    += '| ' + headers.map(() => '---').join(' | ') + ' |\n';
            for (let i = 1; i < rows.length; i++) {
                const cells = Array.from(rows[i].querySelectorAll('td'));
                md += '| ' + cells.map(c => c.innerText.trim()).join(' | ') + ' |\n';
            }
            return md + '\n';
        }

        // Recursive container crawl
        if (el.children?.length > 0) return cmsRef._parseChildren(el, level);
        return el.innerText?.trim() || '';
    };

    // Post-process: collapse whitespace runs outside of code fences
    let raw = recurse(element, indentLevel);
    if (indentLevel === 0) {
        const parts = raw.split('```

');
        for (let i = 0; i < parts.length; i += 2) {
            parts[i] = parts[i]
                .replace(/\n{3,}/g, '\n\n')
                .split('\n')
                .map(line => /^\s*[-*>]|\s*\d+\./.test(line) ? line : line.trim())
                .join('\n')
                .replace(/\n{3,}/g, '\n\n');
        }
        raw = parts.join('

```').trim() + '\n';
    }
    return raw;
}
Enter fullscreen mode Exit fullscreen mode

A few things worth calling out in the implementation:

The exclusion list at the top strips layout chrome before recursion even starts. Nav elements, theme toggles, copy buttons, and print-hidden elements are gone before a single line of content is processed.

The component-aware serialization handles MagpieCSS-specific patterns. Code blocks get proper fenced Markdown with the language preserved. Tables get serialized to pipe syntax. Collapsible tree nodes get flattened to nested bullet lists. The converter knows what the framework renders because it lives inside the same framework.

The post-processing pass collapses whitespace runs without touching code fences. It splits on backtick blocks first so it never corrupts a code sample while cleaning up the structural output around it.


Why This Is Different

There are a few ways people are handling AI-readable content right now:

Static files: Maintain index.html and index.md separately. Works until they drift, and they always drift.

Middleware stripping: Run HTML through a server-side tag stripper. You get something Markdown-shaped but the semantic hierarchy collapses.

llms.txt: A growing informal standard where sites publish a static machine-readable summary. A snapshot, not a live view.

Cloudflare recently shipped edge-layer content negotiation that detects the Accept: text/markdown header and converts HTML on the fly. It is the right direction. It is also infrastructure-level, not framework-level.

What is different here is that toMarkdown() lives inside the design system itself. It knows the components. It knows what a .code-block is, what a .card-container is, what .hide-print means. A generic HTML-to-Markdown converter does not. A framework-native converter does, because it was built alongside the components it is reading.


The Right Mental Model

We already have the vocabulary for this. We call it a viewport.

A mobile browser is a viewport. A print stylesheet is a viewport. An AI agent is a viewport. It has different constraints, different needs, and it deserves a purpose-built rendering path.

Treating AI readability as a layout concern rather than a crawling concern is the shift. The DOM is still the source of truth. The Markdown view is derived from it on demand. No separate files, no drift, no maintenance overhead.


Is Anyone Else Doing This?

At the infrastructure level, yes. At the framework level, almost no one. Most UI libraries are strictly presentational. They define how things look. The question of how things read, specifically how they read to a machine, is outside their scope.

I think that is the gap. If you are building for the web in 2026, AI agents are a real part of your audience. Serving them better HTML is not the answer. Serving them something other than HTML is.

The repo is at github.com/derekarronblackburn/magpieCss. The cms namespace is in dist/magpie.js. Curious what others are building in this space.

Top comments (0)