Export oMLX Chats as Markdown (Bookmarklet)
Sometimes you want to keep an archival copy of a local oMLX Chat conversation. The oMLX web app doesn’t have a built-in export feature, so I made one.
This bookmarklet grabs the live data from an oMLX chat page, formats each turn with "# User" and "# Assistant" headers separated by horizontal rules, then triggers a browser download as chat-[currentChatId].md.
Automatic Installation
For most browsers, you can simply drag the link below directly onto your bookmarks bar:
Manual Installation
Alternatively, here’s the bookmarklet code, ready for manual installation:
!function(){const e=document.querySelector("[x-html]").closest("[x-data]"),t=Alpine.$data(e);!function(e,t){const n=new Blob([e],{type:"text/markdown;charset=utf-8"}),o=URL.createObjectURL(n),c=document.createElement("a");c.href=o,c.download=t,document.body.appendChild(c),c.click(),document.body.removeChild(c),URL.revokeObjectURL(o)}(function(e){const t=e.messages||e.chatHistory?.[0]?.messages||[];return t.length?t.filter((e=>e.role&&e.content)).map((e=>`# ${e.role.charAt(0).toUpperCase()+e.role.slice(1)}\n\n${e.content.replace(/^\s+|\s+$/g,"")}`)).join("\n\n---\n\n"):"# Chat\nNo messages found."}(t),`chat-${t.currentChatId}.md`)}();
Instructions
- On any page, right-click your browser’s bookmarks / favorites bar and choose Add Bookmark (or Add Page).
- In the Name field, type anything memorable — I use
Export oMLX Chat. - In the URL field, paste the entire minified code block above exactly as-is.
- Hit Save (or Add).
Usage
Using the bookmarklet is a two-step process:
- Open your chat in oMLX. Open the oMLX web app and load the specific chat conversation you want to export.
- Click the bookmark. With that chat tab active, click the
Export oMLX Chatbookmark you installed. The file will download immediately aschat-<id>.md.
What it does under the hood
The script works by:
- Finding the active Alpine.js component on the page (oMLX stores its state there).
- Extracting the messages array from that component’s data.
- Formatting each message as
# Useror# Assistantwith horizontal rules between turns. - Generating a blob and triggering a programmatic download via a temporary anchor element.
Here’s the readable source if you’d like to audit it:
(function () {
/**
* Downloads the currently viewed oMLX chat as a Markdown file.
* Labels each turn with H1 headers ("# User", "# Assistant").
*/
function chatToMarkdown(chatData) {
const messages =
chatData.messages || chatData.chatHistory?.[0]?.messages || [];
if (!messages.length) return "# Chat\nNo messages found.";
const formattedBlocks = messages
.filter((msg) => msg.role && msg.content)
.map((msg) => {
// Capitalize role for the H1 header (e.g., "user" -> "User")
const roleHeader = msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
// Trim only leading/trailing whitespace, preserve internal formatting/newlines
const cleanContent = msg.content.replace(/^\s+|\s+$/g, "");
return `# ${roleHeader}\n\n${cleanContent}`;
});
// Combine all blocks with a horizontal rule for readability
return formattedBlocks.join("\n\n---\n\n");
}
/**
* Triggers a browser download for the given text content.
*/
function triggerDownload(content, filename) {
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
// Programmatically click and clean up
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Grab the data from the oMLX Alpine instance
const root = document.querySelector("[x-html]").closest("[x-data]");
const componentData = Alpine.$data(root);
// Call the formatting function
const markdownOutput = chatToMarkdown(componentData);
// Take the output and download it as "chat-[id].md"
const filename = `chat-${componentData.currentChatId}.md`;
triggerDownload(markdownOutput, filename);
})();
The exported file is plain Markdown, so you can open it in any text editor or Markdown viewer. Hope it’s helpful!
Disclosure: This code and this blog post was written partly by me and partly by Qwen-3.6. I manually audited all code for safety to the best of my professional ability.