Schedule Cobalt post for publication

I usually write in French, but as this can be hopefully helpful to the Cobalt community, I decided that I would translate this one into English. Using Cobalt now, one of the features I miss from WordPress is the ability to schedule a post and not bother to think about it any more. Many tools exist for a GitHub hosted website, but I can’t use them as I use Gitea on a private server. But being self-hosted allows me to hack some scripts in order to achieve this. Let’s see how it’s done!

Install the Scripts

All the scripts I’m going to present are in ~/bin and this folder is in the shell PATH.

Prepare the Work Folder

Let’s call user the user owning the folder that will contain our work folder:


We’re going to clone our blog source into this folder:

$ cd ~/www
$ git clone https://my_git/blog.git

Which gives us:


When Cobalt builds the website, the result is stored in the _site folder:


This folder should be fed into the web server configuration.

Create a git Hook

A hook is an action to be executed on some events. Here, we create a hook for when the server is done receiving a push:

Gitea is launched by supervisor and I’m not really familiar with it but it seems that it is launched by root so I chose to do as few as possible in the hook itself. Its only job is to run a script as user to get out from root user space.

Update the Work Folder

Once user is running the script, we first update our work folder by using

unset GIT_DIR
cd ~/www/blog/
git pull > ~/git.log 2> ~/err.log
git log -n 1 --format=%B | gawk -f ~/bin/process_git_command

The trick here is unset GIT_DIR. This is set by Gitea in the hook, and if we don’t get rid of it, git pull will fail. I don’t really understand the details here. 😅 Then the git log line fetches the last commit message and sends it to gawk for parsing.

Parse the Commit Message

Here is the awk script:

BEGIN { regex="^(cobalt_[a-z]+)( (\"([A-Za-z0-9:+ ]+)\" )?(.*))?(\n.*)?" }
{ if (match($0, regex, part)) {
  switch (part[1])
  case "cobalt_build":
    print "exec cobalt_build";
    system("exec ~/bin/ > ~/cobalt_build.log");
  case "cobalt_schedule":
    if (part[4] == "") {
      print "No date given";
    } else if (part[5] == "") {
      print "No file given";
    } else {
      print "exec cobalt_schedule";
      system("exec ~/bin/ \""part[4]"\" \""part[5]"\" \
                    2> ~/cobalt_schedule.log");
  case "cobalt_cancel":
    if (part[5] == "") {
      print "No name given";  
    } else {
      print "exec cobalt_cancel";
      system("exec ~/bin/ \""part[5]"\" \
                    > ~/cobalt_cancel.log");
    print "Unknown command";
  } else { print "No match"; }

This script will parse the first line from the commit message and will look for a defined command. The other lines are still available for some proper commit message.

The regex will store the matches into the part array, if any. Indexes 1, 4 and 5 can contain respectively the command name, the date and a filename without extension. The switch will try to match part[1] with one of the supported commands and if not, will print Unknown command.

Build the Website

The first command available is cobalt_build without any parameter. awk will launch the script:

cd ~/www/blog
cobalt build > ~/cobalt_build.log

Nothing fancy here.

Scheduling a Publication

This is why you’re reading this. 😉 cobalt_schedule needs 2 parameters: a date and a filename without .md. This command runs the script:

job="$(echo "~/bin/ \"$2\" && \
     git commit -a -m \"published $\" && \
     git push && ~/bin/" | at $1 |& grep job)"
echo "$job $2" >> ~/bin/jobs_list

First, note this is a batch file, not a sh file. I’ll explain why later. In this script, $1 is the date and $2 is the filename. As we use at command to schedule the task, the date has to be in a format supported by at. Here’s an example: cobalt_schedule "11:12 Feb 14" schedule-cobalt-post-for-publication

Many things happen here so let’s break them down. 🙂

First, we call with the filename as a parameter:

cd ~/www/blog
cobalt publish ./src/posts/$ >> ~/cobalt_publish.log \
2> ~/cobalt_publish_err.log

It modifies the file draft flag to false and fills the published_date field.

Now our git local repository is modified and we need to push this state back to upstream with two git commands. Be cautious to avoid putting cobalt_schedule in this commit message or you’ll end up with an infinite loop!

Then we call at with the wanted date, and we filter its output with grep to keep the job part only. I’m using |& to pipe the error output to grep because mysteriously, at is outputting to this channel and not to the standard output. That’s why I use bash here, as |& is not supported by sh.

All these commands are in $() to assign the result to job and then saved in a jobs_list file to be used in the last supported command.

Cancel a Scheduled Post

To cancel a scheduled post, we use the command cobalt_cancel with the file name without extension (or only part of it, but beware of ambiguities!): cobalt_cancel schedule-cobalt-post.

The at command can cancel a job, but it needs the job id to do so. That’s why we saved the information enriched with the file name so we can search the job by filename and keep out its id!

Here is the script:

cat ~/bin/jobs_list | gawk -v name="$1" '
$0 ~ name { if (NF > 0) {
    system("atrm "$2);
' && sed -i "/$1/d" ~/bin/jobs_list

We use gawk to find the proper job using the file name and extract its id, then use it with atrm to cancel the scheduling, and finally, we call sed to remove the job from the jobs_list file.


It’s indeed a lot of stuff to do something done with 2 clicks in WordPress. But this is the price to pay to have a scheduling system that suits me while keeping a statically generated blog. Moreover, I learned (or relearned) how to use awk, at, sed and shell scripting. 😄 So yeah, it’s a bit of work, but it fits in the philosophy of using a statically generated website! I hope this helps!