Scaffolding Setup

From the starting point of Developing Drafts, I have a setup where I can begin working on new writing ideas and additions to the website without having to predetermine their place in a hierarchy or container. This also means that there’s no difference from a writing and editing perspective between working on a series or a single item. Which is what I want.

What I don’t have yet is a way to turn those drafts into published web pages with unique URLs which fits in seamlessly with managing the existing content archive.

End to end sketch

If I sketch out the CLI actions and workflow for posting a draft, I want it to go something like the following:

Create action

$ maetl --help
maetl [ content-type ] [options]

 -f, --file         Filename to create or convert. If no file type is given, references the directory
 -d, --id          Identifier of the content type, referencing the directory
 -i, --import      Local path or URL to import into the given content type

 Content type subcommands:

   draft           Creates a new writing project in the local DRAFTS_PATH
   entry           Converts a draft or imports a path into a notebook entry on maetl.net
   collection      Converts a draft or imports a path into a new collection type on maetl.net

Examples

$ maetl draft -f new-ideas.md
$ maetl draft -d design-method-docs
$ maetl draft -d redesign -f burn-the-notebooks.md
$ maetl draft -i https://maetl.net/draft-template
$ maetl draft --import ~/Desktop/some_messy_hacked_up_artifact
$ maetl entry -d new-ideas
$ maetl collection -d redesign
$ maetl collection -i $DRAFTS_PATH/redesign

This would be far too much or too weird for a lot of people, but for me, is a fairly small and concise bit of command line scaffolding that will be easy to remember and repeat.

It’s not a lot of effort to build but the critical point is that by taking a punt on worrying about the archive problems, there are only two places in this script that would need to be updated if I prevaricate, waver, or change my mind on how notebooks, blogs, pages, etc are structured:

  • entry / entries
  • collection / collections

The entry and collection converters could be adapted to work with static site generators like Eleventy, Hugo, Jekyll etc, or customised to whatever odd system I want to put in place in future. So it provides a bit of slippage to encourage the fast production of writing and draft files without necessarily needing to mess around with YAML config, Markdown front matter, JSON, etc.

The path of least resistance

I already have a whole bunch of Ruby scripts set up within the maetl.net project so adding more to this is a minor detail, really. In the past I often used Rake tasks to manage this kind of thing, but more recently I have been using libraries like tty-toolkit and trying to keep everything as modular as possible in different files so it’s easier to rename or delete (trying new things out in self-contained files that are easy to delete later helps prevent legacy mess from accumulating).

The first step is to get the skeleton of the script going, a big chunk of which is copying over details from sketch above.

#!/usr/bin/env ruby
require "pathname"
require "tty-option"
require "fileutils"

module Maetl
  DRAFTS_PATH = Pathname.new(ENV['DRAFTS_PATH'])

  class Script
    include TTY::Option
    include FileUtils

    argument :content_type

    option :file do
      short "-f"
      long "--file path"
      desc "Filename to create or convert. If no file type is given, references the directory"
    end

    option :dir do
      short "-d"
      long "--dir path"
      desc "Identifier of the content type, referencing the directory"
    end

    option :import do
      short "-i"
      long "--import path"
      desc "Local path or URL to import into the given content type"
    end

    flag :help do
        short "-h"
        long  "--help"
        desc "Print usage"
      end

    def run
      parse

      if params[:help]
        print help
        exit
      else
        case params[:content_type].to_sym
        when :draft then create_draft
        when :entry then prepare_entry
        when :collection then prepare_collection
        else
          raise ArgumentError.new("Missing content type")
        end
      end
    end

    private

    def create_draft
      pp params.to_h
    end

    def prepare_entry
      pp params.to_h
    end

    def prepare_collection
      pp params.to_h
    end
  end
end

script = Maetl::Script.new
script.run

Now that the basic skeleton works, the very first thing I need to get started is to follow the patterns established in Developing Drafts for creating starter files for new bursts of writing.

I’ll start out by only supporting the single file use case:

option :file do
  convert :pathname
  short "-f"
  long "--file path"
  desc "Filename to create or convert. If no file type is given, references the directory"
end

def create_draft
  file = params[:file]
  new_draft_dir = DRAFTS_PATH.join(file.basename(file.extname))
  new_file_path = new_draft_dir.join(file.basename)
  mkdir(new_draft_dir)
  File.write(new_file_path, file.basename(file.extname).to_s.gsub("-", " "))
end

And to test that it all works:

$ maetl draft -f how-to-shutdown-the-suez-canal.md

All this does is create a new directory in drafts then write a text file into it with the text how to shutdown the suez canal.

No problems getting that working. It’s rough, with no error handling or checking but this isn’t difficult to handle later on.

Now, a bit of rewriting and clean up to properly support different combinations of -dir and --file flags:

def first_sentence(identity)
  identity.basename(identity.extname).to_s.gsub("-", " ")
end

def draft_opts
  if params[:file] && params[:dir]
    new_dir = DRAFTS_PATH.join(params[:dir].basename)
    return [new_dir, new_dir.join(params[:file].basename)]
  end

  if params[:dir]
    new_dir = DRAFTS_PATH.join(params[:dir].basename)
    return [new_dir, new_dir.join("#{params[:dir].basename}.txt")]
  end

  if params[:file]
    new_dir = DRAFTS_PATH.join(params[:file].basename(params[:file].extname))
    return [new_dir, new_dir.join(params[:file].basename)]
  end

  raise ArgumentError.new("Must provide --file or --dir option")
end

def create_draft
  new_dir, new_file = draft_opts
  mkdir(new_dir)
  File.write(new_file, first_sentence(new_file))
end

In future, I’d like to change this to generate a random path and randomised draft content if the flags aren’t provided and remove the errors being raised but this isn’t helpful right now.

The following tests will ensure all branches are working:

$ maetl draft -d suez -f big-ship-crash.txt
$ maetl draft -d supply-chain-shocks
$ maetl draft -f cargo-ship-memes.md

The main thing I’ve achieved here is set up this CLI script as a metaphorical vice or clamp to force myself to make a decision about how to prepare files and directories to feed into the site publishing structure. I’ve put myself into the position of having to do it now in code, as I still haven’t come to a conclusion about how to design URLs and navigation.

One final little tweak is to open the new draft in a text writing app. Here I’ve used WriteRoom but without the -a flag it would use the default assigned application for that file type.

def create_draft
  new_dir, new_file = draft_opts
  mkdir(new_dir)
  File.write(new_file, first_sentence(new_file))
  IO.popen("open -a WriteRoom #{new_file}")
end