Publishing from Ulysses to my Astro blog

Updated 8 November 2024
·
nodejs
ulysses

Since beginning my blog in 2020, I’ve been using the Ulysses text editor to write all my posts. I publish my posts using a static site generator (SSG) called Astro. Since SSGs let you write and publish your posts in Markdown, this is a perfect fit for Ulysses, since its text editor works in Markdown as well.

However, exporting a sheet from Ulysses and getting it into a publishable Markdown file in my blog is not a one-click process. To try and make things smoother, for a long time now I’ve been using a script to automate this process for me instead. This is not Astro-specific, and could be done with any blogging platform that supports Markdown, whether it be Next.JS, Gatsby, Hugo and so on.

If you happen to be using Ghost or Wordpress, Ulysses actually has proper publishing integrations, so that would be worth checking out instead.

If Ulysses already exports to Markdown, why a script?

There’s just a small couple of fiddly things I like to automate. One example is that Markdown files deal with the concept of something called frontmatter. These are details that you store at the top of your post, like the title and the date it was published:

---
title: "How to export from Ulysses to Markdown"
date: 2021-09-25
tags: []
---
Here's the content of the post.
![](./image.png)

Ulysses doesn’t have a concept of this, so the exported Markdown file would look more like this:

# How to export from Ulysses to Markdown
 
Here's the content of the post. 
 
![](./image.png)

So my script handles restructuring the title into a frontmatter format, and adding a date for me.

I also use MDX, which is a slightly fancier version of Markdown, and this requires you to have .mdx file endings, so my script handles the file renaming step.

Finally, MDX lets you use React components in your Markdown, and so inside of my Ulysses sheet I have them in code blocks like this (to prevent Ulysses from complaining that I have typos in my sheet):

// e.g. I use this component to render a summary component about a hiking course
```
<HikingCourse 
    uploadUrl=""
    dateClimbed=""
    length=""
    time=""
>
Info about the hiking course goes here.
</HikingCourse>
```

But in my .mdx file itself, I want these codeblock backticks removed so that the component actually renders, so my script handles that for me as well.

My script for converting Ulysses to Markdown

Before running my script, I:

  1. Create a new folder for my post in my git repo e.g. src/content/programming/new-post-here
  2. Right click a sheet in Ulysses and choose the Export option with the Markdown format.
  3. Save that file to that newly created folder. If there’s any images, they’ll get saved as well.

Note: On my original 2020 version of this blog post, I was actually exporting my file as a .textbundle file. This made things a lot more complicated. I’m not sure why I was doing that, but it seems like just exporting as .md works fine for me today.

I then run the script, passing in the name of my new folder:

node ./scripts/ulysses.js new-post-here
scripts/ulysses.js
#!/usr/bin/env node
 
const { renameSync } = require('fs');
const { join } = require('path');
const glob = require('glob');
const replace = require('replace-in-file');
 
const ulysses = async () => {
    // When I run the script, I pass in the folder name of my new post
    // [1] if you run via package.json script, [2] if you run the script directly
    const fileName = process.argv[2];
 
    // Gets the file path of the new post
    // All my posts live underneath the src/content folder
    const file = glob.sync(
        join(process.cwd(), 'src', 'content', '**', fileName, 'index.md'),
    )[0];
 
    // Change from file from .md to .mdx
    const newFileName = `${file}x`;
    renameSync(file, newFileName)
 
    const headerRegex = /^(# .*)/g;
    const backTicksRegex = /```(\w+)?\n([\s\S]*?)\n```/g;
 
    const headerFn = (title) => `---
title: "${title.replace('# ', '').trim()}"
date: ${new Date().toISOString().substring(0, 10)}
---`;
    const backTicksFn = (_x, _y, arg) => arg;
 
    const options = {
        files: newFileName,
        from: [headerRegex, backTicksRegex],
        to: [headerFn, backTicksFn],
    };
 
    return replace(options);
};
 
ulysses();

If you run into any errors, you’ll probably need to install some of the missing libraries:

npm install -D fs path glob replace-in-file

From here, you can also customise the script however you like. The replace-in-file library that I’m using lets you pass in additional items into the array. So if you wanted change what apostrophes you were using, you could pass in something like:

const options = {
    files: newFileName,
    from: [headerRegex, backTicksRegex, /’/g],
    to: [headerFn, backTicksFn, "'"],
};

And this would swap out the for '.

If you prefer, you can also add the script to your package.json file:

"scripts": {
    "uly": "node -e 'require(\"./scripts/ulysses-hiking.js\").ulysses()'",
},

And then run it with npm run uly -- post-name-here.

Recent posts

Comments