Ken Muse

Improved Blogging With Visual Studio Code Tasks


I’ve mentioned a few times that I use Visual Studio Code and Hugo for my blogging. I’ve also discussed some of the the ways I make it faster and easier to create my posts. In today’s post, I want to explore that topic further.

VS Code provides a very extensible editor experience. It allows you to add new commands and new UI elements. It even allows you to customize your workflows. In short, you have a lot of ways to customize the experience.

Some time ago, I noticed that there were four things that I did for each post that took excessive amounts of time. Being a DevOps practitioner, this seemed like a perfect opportunity to automate some processes. Taking inventory of the pain points, I identified a few areas that I could improve:

  • Creating a new post
  • Testing the site
  • Validating the metadata
  • Selecting tags and categories

With those in mind, I looked for ways to improve my process. Let’s start by looking at the first two items.

Creating new posts

The first item I tackled was creating new posts. Whether it’s a new blog article or a speaking engagement, there’s a lot of front matter YAML that I use for customizing how everything is presented. I also like to organize my articles by year so that I can find them more easily. For these, I took advantage of Hugo archetypes. These are essentially Go templates that Hugo can use to create new content. I created a custom archetype for my blog posts and a separate one for my speaking engagements. This way, I can keep the front matter separate and only include the fields that I need for each type of content. I can also include a default image for the blog posts that will be displayed if I haven’t created a custom banner.

If I put the template in the archetypes folder, I can call hugo new blog/{NAME} and it will create a new post with the correct title and front matter. I can then open the post in VS Code and start writing. My blog posts are created with a target publish date in mind. By using a convention of yyyy-mm-dd title for the name of the post, I can create a folder with that name containing an index.md. I can also use some light scripting in the template to automatically populate some of the details.

To make this work, I created a subdirectory in archetypes called blog. Within that, I put two files: index.md (the template) and images/banner.jpg (a default banner). The template looks like this:

 1---{{ $data := split .Name " " }}{{ $time := time (print (index $data 0) "T05:00:00") "America/New_York" }}{{ $docTitle := delimit (after 1 $data) " " }}{{ $uniqueId := .File.UniqueID }}
 2# yaml-language-server: $schema=/schema/blog.json
 3title: {{ $docTitle | title }}
 4categories: [General]
 5tags: [General]
 6postId: {{ substr $uniqueId 0 8 }}-{{ substr $uniqueId 8 4 }}-{{ substr $uniqueId 12 4 }}-{{ substr $uniqueId 16 4 }}-{{ substr $uniqueId 20 }}
 7draft: true
 8
 9publishDate: {{ $time.Format "2006-01-02T15:04:05-07:00" }}
10date: {{ $time.Format "2006-01-02" }}
11month: {{ $time.Format "2006/01" }}
12year: {{ $time.Format "2006" }}
13
14slug: {{ urlize (lower $docTitle) }}
15banner: images/banner.jpg
16---
17Content goes here ...

Once a post is created, I can open it in VS Code and start writing. I can also customize the front matter as needed, changing the title, categories, tags, and other details.

Understanding the archetype

If you’re not interested in understanding how some of this works with Hugo, feel free to skip to the next section. The Hugo template is a bit complex, so I want to deconstruct that just a bit. My template relies on knowing a few things about the process and how Hugo handles archetypes:

  1. My command line uses the format hugo new blog/{YEAR}/{DATE} {NAME}. As an example, this blog post used hugo new blog/2024/2024-10-01 Improved Blogging With Visual Studio Code Tasks. Hugo uses the first segment, blog, to identify the archetype to use. It then uses the template to populate the directory blog/{YEAR}/{DATE} {NAME}. In my case, creating index.md and images/banner.jpg.
  2. The template receives a value, .Name, which is just the last part of the path. In my case, {DATE YEAR} (or 2024-10-01 Improved Blogging With Visual Studio Code Tasks).
  3. Each file gets assigned a unique 32-character identifier such as 33893bb54801e282c95eaa65997071ea that is accessible using .File.UniqueID.

The template creates a variable, $data, that splits the provided .Name at the first space character. That creates an array with a date in the first element(index $data 0). I can use time to convert it into a date/time data type, and format to choose how it is formatted as a string. This value is used to set the metadata used for publishing the post and for organizing the posts by date on my site.

The remaining segments (after 1 $data) are joined back together to capture the title of the post as $doctitle. I use the Hugo title function to properly case it. I then use urlize to sanitize the title and create a default “slug”. A slug is the HTTP path to the post, such as improve-blogging-with-visual-studio-code (this post). These are all defaults, so I can customize and tweak it as needed.

All of my posts also have a unique GUID assigned to them. This was originally used to handle updating a Wordpress site with changes to existing posts and with the original RSS feeds. I use the UniqueID property that Hugo assigns to the file to create the GUID. I then use the substr function to take the first 20 characters of the GUID. This is the same format that I use for the file name, so I can easily find the post in my content folder.

This process has a limitation. It creates the file, but it doesn’t open it for me. For that, I need to start integrating with VS Code.

Integrating with Code

To improve the workflow, I wanted to integrate the experience with my Code environment. That would allow me to create the file and open it in one step. The easiest way to do that is with a custom task. Tasks are configure by creating a file, .vscode/tasks.json, in the root of the project. Tasks in that file are available under Task: Run Task in the command pallette using Ctrl+Shift+P or Cmd+Shift+P (⇧⌘P). Tasks can

The tasks.json file looks like this:

 1{
 2    "version": "2.0.0",
 3    "tasks": [
 4        {
 5            "label": "Create Post",
 6            "type": "shell",
 7            "command": "hugo new 'blog/${input:title}' && code -r '${workspaceFolder}/content/blog/${input:title}/index.md'",
 8            "problemMatcher": [],
 9            "presentation": {
10                "echo": true,
11                "reveal": "always",
12                "focus": false,
13                "panel": "dedicated",
14                "showReuseMessage": true,
15                "clear": false
16            }
17        }
18    ],
19    "inputs": [
20        {
21            "id": "title",
22            "type": "promptString",
23            "description": "Post title",
24            "default": ""
25        }
26    ]
27}

This task uses the shell type to invoke the Hugo command. This allows me to take advantage of the shell to do two distinct steps in a single command (by separating them with a &&, which executes the second command if the first succeeds). While I could use dependsOn to chain asks together, in this case the two components are always executed together and never individually. It makes sense in my case to combine them. I configure problemMatcher: [] to prevent VS Code from trying to parse the output of the Hugo command. I’ll come back to that one.

At the bottom of the file, I define an input called title. It prompts me to provide the input for that task. The task can then reference ${input:title} and automatically prompt me for the title of the post:

The task calls the Hugo command to create a new post, passing the input. It then uses the code command to open the generated _index.md file in the current VS Code window. The -r flag tells VS Code to reuse the current window.

The presentation section of the task is used to control how the task is displayed:

  • reveal: always
    Always bring the terminal panel where this is executing to the front and make it visible
  • echo: true
    Show the full command in the terminal panel
  • focus: false
    Don’t give the panel input focus. I want the focus to be on the editor.
  • showReuseMessage: true
    Displays the message indicating the terminal will be reused and I can press any key to close it. This allows me to review the output and close the terminal when I’m ready.
  • panel: dedicated
    The task gets its own terminal panel that is reused if I run the task again.
  • clear: false
    Allows the terminal to act as a running log of the task executions. Set this to true to clear the terminal before each run.

With the task in place, I can now quickly create posts and have a portion of the content automatically generated.

Instant access with key bindings

Using the command pallette is fine, but sometimes you just want things to be a simple keystroke. I can create an entry in my personal keybindings.json (or through the Keyboard Shortcuts):

1[
2    {
3        "key": "ctrl+shift+n",
4        "command": "workbench.action.tasks.runTask",
5        "args": "Create Post",
6        "when": "resourcePath =~ /^\/workspaces\/website/"
7    }
8]

I’m binding Ctrl+N to the workbench.action.tasks.runTask command. The args allow me to specify which Task I want to run. Because keybindings.json is a user-level file (for good reasons), it makes it a bit more challenging to have project-level configurations. I use the when clause to limit the binding to a known path in my dev container. This way, I can use the same key binding for different tasks in different projects.

Testing the site

The next item I wanted to improve was testing the site. Hugo has a built-in development server that I can run with hugo serve. This will build the site and start a server on http://localhost:1313/. I can then open a browser to that address to see the site. The -D flag tells Hugo to build the site in draft mode. This will include any posts that are marked as drafts. This lets me preview the post while I am creating in (with automatic reloading). To integrate that in, I’ll create another task:

1{
2        "label": "Serve Drafts",
3        "type": "shell",
4        "command": "hugo server -D -F --poll 700ms --enableGitInfo --port 1313"
5}

This lets me launch the site and automatically monitor changes while I’m working. If I wanted to monitor for issues, I could set the isBackground flag and define problem matchers. VS Code would execute the task in the background and report back any Problems. This is particularly useful for continuous background linting. If you’re interested in this, review the documentation for background tasks.

Validating the metadata

You may have noticed that my site defines a schema for the metadata. This lets me validate the content and ensures that the various fields have proper lengths to support Bing and Google searches. Normally, I can use RedHat’s YAML extension to validate my YAML. Unfortunately, the extension that natively support Markdown in VS Code is not yet able to pass YAML front matter to the language server. To solve this, I created a custom task in PowerShell to assist me. The script used [Test-Json](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/test-json?view=powershell-7.4) and ConvertFrom-Yaml (documentarian) to validate the schema for the posts.

 1{
 2    "label": "Validate Blog Metadata",
 3    "type": "process",
 4    "command": "pwsh",
 5    "args": ["-command", "cd content/blog; ../../build/check-frontmatter.ps1" ],
 6    "problemMatcher": [
 7    {
 8        "owner": "Markdown",
 9        "fileLocation": "absolute",
10        "pattern": {
11            "regexp": "^(?<file>.+);(?<message>.+);(?<line>1);(?<column>1)$",
12            "file": 1,
13            "message": 2,
14            "line": 3,
15            "column": 4
16        }
17    }]
18}

By outputting the errors in a structured format, I could use the problem matcher to show the errors in the Problems pane and in the appropriate editor windows. That made it quick and easy to find and fix issues. For those interested, the code used Get-Content to read each line of the markdown files. If the first line was ---, it continued to read the lines until the next ---. It used a regex to find the schema.

The basic code I used for testing the file looked like this:

 1$schemaPath = (Join-Path $_.Directory.FullName $schema) | Resolve-Path -ErrorAction SilentlyContinue
 2$finalSchemaPath =  $schemaPath ?? "/workspaces/website/schema/blog.json"
 3$converted = ConvertFrom-Yaml $yaml -ErrorAction Stop
 4$json = ConvertTo-Json -Depth 5 $converted
 5$err = $null
 6$valid = Test-Json -Json $json -SchemaFile $finalSchemaPath -ErrorAction SilentlyContinue -ErrorVariable err
 7[PSCustomObject]@{
 8  File = $_
 9  Directory = $_.Directory.Name
10  IsInvalidSchemaPath = $schemaPath -eq $null
11  Slug = $converted.slug
12  IsValid = $valid
13  SchemaError = $err.Exception.Message
14}

It allowed me to know when a file had a bad schema path and the specific errors (if any) for each file by directory, file name, or slug. To create the error messages for the problem matcher, I just used the SchemaError and generate the error messages with some parsing and ConvertTo-Csv. It wasn’t perfect, but it was good enough to get started. I like to time box any solutions and iterate on them later, so this was a fair compromise.

Just a few more tasks…

Over the years, I’ve added tasks for pain points as they would arise. I have tasks that validate all of the internal links between my posts. That came in handy as I migrated to Hugo! I don’t validate external links at the moment since there are a ton of those (and over time, many of them are likely to disappear). I also have a task that validates the file before publishing. I even have a process to validate the dates in the metadata and the folder remain aligned (since I may choose to postpone a post). There are even tasks for creating the speaking engagements, building and minifying the theme files, and validating the dev container.

In short – if it kept me from focusing on writing, I automated it. It’s the same approach I use for developing. I like to eliminate repetitive tasks that distract from core objectives. Many of these save only minutes per post. With 52 posts a year, each minute literally becomes a lost hour. My time is incredibly valuable to me.

Tasks are simple and very powerful, so they were a first choice for scripting any work that I needed to do frequently. With minimal code, you can quickly integrate the processes directly into VS Code. In my case, it allowed me to focus on being creative instead of the process behind providing the content. Hopefully you can see a few ways to use them yourself!