Using Obsidian and R for a Blogging Workflow

In a very real sense, the writer writes in order to teach himself; to understand himself, to satisfy himself; the publishing of his ideas, though it brings gratifications, is a curious anticlimax.
Alfred Kazin

I set up an Obsidian Vault for blogging (see this posting for a short introduction and this one for how I currently use it). I have lots and lots of ideas (around 480 or so given current count, yeah) and that’s the natural way to keep them until I have written the postings.

But the way from written posting to publishing it in WordPress sucks. My blog developed, well, evolutionary, so I have a few manual steps. I do not use or trust APIs, among others because I spot some (unfortunately not all) spelling and grammar mistakes when copying the text to WordPress. But the way from markdown file in Obsidian to WordPress could be a bit easier.

So I wrote an R Script that assists me a little. It takes the files in a folder of that Obsidian vault, replaces Obsidian’s image links with HTML links adapted to my blog (and in general markdown to HTML), then copies the used images to an external folder (in a filename suitable for the web) and saves the reformatted file there as well. The images are in specific directories, one for the teaser image (which is automatically resized by GraphicConverted) and one for the images in the posting itself (also resized for the blog). This way, the image files can be uploaded en block and the text files allow easy copy-and-paste. Files and images in the Obsidian Vault are also moved to a «used» directory. Oh, and the upload folder is also opened automatically.

Folders in the Vault for the Outbox and a folder for the used markdown and images. I delete the used markdowns/images manually once the postings are online (or back them up, not sure yet).

The current code is as follows (I have removed the path information, and as usual, no warranty, esp. considering there are still a few bugs in it, among others, using an image twice leads to problems, GraphicConverter creates jpgs but the image extension is not changed in order to reflect it, and sometimes the image names aren’t change correctly):

ocProcessPostings <- function() {
    # This function takes the postings from the outboxPath and processes them. End up in uploadPath with folders for the .txt files (3 Postings), with images being put into autoConvert folders for GraphicConverter to process them. Teaser image is the first image (only used once, scaled to 174 pixel), with all other images being text images (scaled up/down to 550 pixel). Scaling done via Graphic Converter.
    outboxPath = "~/ADDPATHINFO"
    mediaPath = "~/ADDPATHINFO"
    usedPath = "~/ADDPATHINFO"
    uploadPath = "~/ADDPATHINFO"
    outboxFiles <- list.files(outboxPath)
    for(postingNr in 1:length(outboxFiles)) {
        currentPostingName <- outboxFiles[[postingNr]]   
        currentPostingText <- read_lines(paste0(outboxPath, currentPostingName))
        sanitizeFilename <- function(nametochange) {
            nametochange <- stringi::stri_trans_general(nametochange, "Latin-ASCII")
            nametochange <- str_to_lower(nametochange)
            nametochange <- str_replace_all(nametochange, " ", "_")
        LookingForTeaserImage = TRUE
        quoteOn = FALSE
        outputLines = NULL
        boldOn = FALSE
        italicsOn = FALSE
        for(i in 1:length(currentPostingText)) {
            cL <- currentPostingText[[i]]
            # deal with images
            if(str_detect(cL, "!\\[\\[")) {
                imageCore <- str_replace(cL, "!\\[\\[", "")
                imageCore <- str_replace(imageCore, "\\]\\]", "")
                sanitizedCore <- sanitizeFilename(imageCore)
                if(LookingForTeaserImage) {
                    cL <- sanitizedCore
                } else {
                    cL <- paste0('<img class="ngg-singlepic ngg-center" alt="" src="', imageCore, '" />')
                if(LookingForTeaserImage) {
                    file.copy(paste0(mediaPath, imageCore), paste0(uploadPath, "autoConvert TeaserImages/teaser_", sanitizedCore))
                    LookingForTeaserImage = FALSE
                } else {
                    file.copy(paste0(mediaPath, imageCore), paste0(uploadPath, "autoConvert Text Images/", sanitizedCore))
                file.rename(paste0("~/Documents/Obsidian/Writings/0 Media/", imageCore), paste0("~/Documents/Obsidian/Writings/4 Used/used Images/", imageCore))
            # deal with quotes
            if(str_starts(cL, "> ")) {
                if(!quoteOn) {
                    quoteOn = TRUE
                    outputLines = c(outputLines, "<blockquote>")
                cL = str_replace(cL, "> ", "")
            } else {
                if(quoteOn) {
                    quoteOn = FALSE
                    outputLines = c(outputLines, "</blockquote>")
            # deal with markdown text
            if(str_detect(cL, "\\*\\*")) {
                while(str_detect(cL, "\\*\\*")) {
                    if(boldOn) { cL = str_replace(cL, "\\*\\*", "</b>"); boldOn = FALSE } else { cL = str_replace(cL, "\\*\\*", "<b>"); boldOn = TRUE }
            if(str_detect(cL, "\\*")) {
                while(str_detect(cL, "\\*")) {
                    if(italicsOn) { cL = str_replace(cL, "\\*", "</i>"); italicsOn = FALSE } else { cL = str_replace(cL, "\\*", "<i>"); italicsOn = TRUE }
            outputLines = c(outputLines, cL)
        write_lines(outputLines, paste0(uploadPath, "3 Posting/", str_replace(currentPostingName, ".md", ".txt")))
        file.rename(paste0(paste0(outboxPath, currentPostingName)), paste0("~/Documents/Obsidian/Writings/4 Used/used Markdown Files/", currentPostingName))
    system(paste0("open ", str_replace_all(uploadPath, " ", "\\\\ ")))

(And yeah, I am a psychologist, not a programmer. My code works-ish, but I am somewhere between a dilettante and a sorcerer, definitely not a wizard.)

I’m also likely going to rework the code (update: already have started to do so), to include dealing with headers (done), and verbatim text that is not processed (still to do). Plus squishing the bugs. But still, removed a lot of tedious steps on the first trial.

Given that this function is made available when I start RStudio, I can simply run the function and most of the tedious work is done by R.

I still have to deal with some special cases (like posting the code above, which I had to do externally), but so far, it works very well. Took me two to three hours on a Sunday morning (with one false start when trying to go via a tibble dataframe, instead of directly editing the lines), will save me much more than that.