Devlog Social Rendering
This note documents the current design for turning devlog markdown entries into social posts, the problems it fixes, the trade-offs we accepted, and the next steps if this area needs to become more capable.
Problem
The canonical devlog source lives under docs/dev/journal/*.md.
That format is correct for the docs site, but it is not correct as a direct transport format for chat/social targets:
- image markdown such as
is valid in the docs, but chat transports need native media payloads rather than raw markdown syntax - relative links are meaningful relative to the source markdown file, but social targets need absolute public URLs
- the public user-facing destination is the generated devlog index page, not the per-entry markdown page
- different targets can require meaningfully different payload shapes even when they share the same source content
This means the docs markdown should remain the canonical source, but publishing must render it into a target-safe representation instead of reusing the raw source verbatim.
Current Design
Current files:
docs/scripts/publish-devlog.mjsdocs/scripts/devlog-social-renderer.mjsdocs/scripts/test-devlog-social-renderer.mjsdocs/scripts/generate-devlog-index.mjs
The current flow is:
publish-devlog.mjsreads each devlog entry fromdocs/dev/journal/*.md.- Entry links published to users point at the actual VitePress heading anchor form, currently
/dev/devlog#_<entry-id>, not at/dev/devlog/<entry-id>. generate-devlog-index.mjskeeps the public devlog page as the user-facing destination for entries.devlog-social-renderer.mjsconverts the entry body into channel-safe output.- Discord formatting and Matrix formatting are split at the payload boundary.
- Discord message text suppresses a trailing-link embed with angle brackets and ships images as native webhook embeds.
- Matrix uploads referenced local image assets first, then sends one text event followed by one
m.imageevent per image.
Rendering Rules
The renderer keeps the source markdown as input and applies explicit rules for the markdown subset currently used in devlog entries.
Important behavior:
- markdown images are removed from the text body
- image URLs are resolved to absolute public URLs so channel-specific publishers can attach them as native media payloads
- markdown links become visible text plus absolute URL
- fenced code blocks are preserved
- simple lists are preserved
- blockquotes are preserved
- inline code spans are preserved
- final entry links use the public devlog heading anchor URL
- Discord wraps the final entry URL in angle brackets so Discord does not create a second preview embed from that link
- Matrix sends a single text event first, then image events, to keep room noise minimal while still using native media messages
Example source markdown:
Published Discord payload shape:
{
"content": "Devlog 2025-09-16\n\n<https://spaceui.org/dev/devlog#_2025-09-16>",
"embeds": [
{
"image": {
"url": "https://spaceui.org/dev/space-prototype-screenshot1.png"
}
}
]
}Published Matrix event flow:
- One
m.textm.room.messageevent with the rendered devlog body and public devlog link. - One
m.imagem.room.messageevent per image, referencing uploadedmxc://media.
Why This Design
This is still intentionally a middle-ground design.
Why we did not keep the old approach:
- the original publisher used raw markdown body text in social payloads
- a quick regex rewrite was enough to prove the direction, but it was not robust enough to be the long-term implementation
- trying to force image markdown into social posts produced the wrong user experience and still depended on transport-specific behavior
Why we did not jump straight to a full markdown stack:
- the docs toolchain in this repo does not currently expose a simple direct markdown parser dependency for this script
- pulling in a full parser just for this workflow would add dependency and maintenance work that was not justified by the current content complexity
Why the current solution is acceptable:
- it cleanly separates canonical source content from publish-target rendering
- it fixes the real user-facing failures we saw in production
- it is tested against fragments and real entries
- it is easy to extend further per channel without mutating the canonical markdown
Trade-offs
This is better than regex rewriting, but it is still not a full CommonMark implementation.
Accepted trade-offs:
- the parser is purpose-built for the markdown subset we currently use in devlog entries
- unusual nested markdown constructs may still render imperfectly
- inline HTML is not treated as a first-class structured format
- Matrix media publishing currently supports local file-backed image markdown, not arbitrary remote image URLs
- the current heading-anchor path depends on VitePress's heading id behavior for date-based headings
This is deliberate. The goal was to make the publisher correct for current content and clean enough to extend, without overbuilding.
Unsupported Construct Policy
Unsupported or partially supported constructs should follow explicit rules instead of accidental parser behavior.
Current policy:
- images: omit them from rendered text; publish them only through channel-native media payloads when the channel implementation supports that
- nested or unusual markdown combinations: flatten to the nearest plain-text rendering when possible
- inline HTML: treat as plain text, not as structured markup
- reference-style links, tables, and other unsupported markdown forms: leave them as plain text unless a specific renderer rule is added
- remote image URLs in Matrix publishing: fail loudly instead of guessing at download/upload behavior
- constructs that would corrupt the published message shape or destination URL resolution: fail loudly in the publisher rather than silently inventing output
Current Test Coverage
docs/scripts/test-devlog-social-renderer.mjs now covers:
- relative asset URL resolution
- markdown image omission from text bodies
- Discord image embed extraction
- markdown link rendering
- fenced code blocks
- simple lists
- blockquotes
- inline code spans
- Discord full-message formatting with the public devlog anchor URL wrapped in angle brackets
- Matrix full-message formatting with the plain public devlog anchor URL
- fixture-style coverage using complete sample entries from
docs/dev/journal/ - extracted image metadata needed for Matrix upload
Run with:
node --test docs/scripts/test-devlog-social-renderer.mjsFuture Improvements
If this system needs to become more capable, the next steps should be:
Add richer Matrix image metadata.
- width and height
- thumbnails where worthwhile
- encrypted media if needed by the deployment
Consider an intermediate structured representation if channel divergence grows.
- blocks
- inline spans
- resolved URLs
- image/link/code/list/quote nodes
Consider adopting a real markdown AST parser once complexity justifies it.
- this becomes worthwhile if entries start using more advanced markdown or if several output targets diverge meaningfully
Guidance
When changing the devlog publisher:
- keep
docs/dev/journal/*.mdas the canonical authoring format - do not special-case Discord or Matrix by mutating the source markdown
- keep public links pointed at the generated devlog heading anchors
- extend the social renderer and its tests together
- prefer explicit rendering rules over silent magic
