How to convert Grav markdown content to Hugo using powershell

| Dec 19, 2019

Photo by Clément H on Unsplash

In recent posts, I’ve talked about migrating from Grav to Hugo, at a high level.

Before I get down into the detail of setting up the new solution - I need to spend a little time on the changes you will need to make along the way for your content to be compatible with Hugo.

If you are migrating to Hugo from a different platform than Grav, the changes required are likely to be different, but may be similar to those that I experienced.

How do I start with Hugo?

I can’t recommend the Quick Start guide on the Hugo website enough - straightforward enough that even I could follow it ;-)

So, what’s different between Grav Markdown and Hugo Markdown?

Some of this will be theme dependent - I’m using the Casper theme.

  1. The directory structure is different by default, with only 4 top-level folders required:
  • archetypes : comes with the Hugo download - these are the default archetypes or templates for content in Hugo
  • content : surprisingly enough, your content goes in here - more detail on this below
  • static : static content such as images go here - I found that unlike Grav, I couldn’t have images in the content folder - they had to be here
  • themes : this contains any themes you’ve downloaded including their archetypes, config etc
  1. There is also a config file at the top level that can be in either TOML, YAML or JSON formats (more info) - Casper theme requires the use of the TOML format

  2. In the content directory, you can have any top-level pages other than the default index page (in the example here I have my Cookie/Privacy notice page and a mailing list signup page) and then a subdirectory for all other posts (in the case of the Casper theme - that directory has to be called post)

  3. In that content\post directory, all you can have in there is your markdown files for your blog posts

  4. In the case of the Casper theme, you can enable a number of features without installing plugins etc by editing the config file to enable them - below is mine with my particular settings mostly changes to examples or comments to explain them (# at the start of the line will comment out any setting that you don’t want processed - the defaults will then be used)

LanguageCode = "en-gb"
baseURL = ""
Title = "Website Title - displayed on the cover/header of the site"
canonifyurls = true
paginate = 5
DisqusShortname = "put in your Disqus shortname to enable Disqus comments on your posts"
theme = "choose the theme to use for your site"

    identifier = "Home"
    name = "Home"
    url = "/"
    weight = -100

## The menu Item's below I added so I could have ##
## more than just Home on the navigation menu.   ##
    identifier = "Mailing List"
    name = "Mailing List"
    url = "/mailing-list/"
    weight = -110

    identifier = "Cookies"
    name = "Cookies"
    url = "/cookie-notice/"
    weight = -120

  description = "Tagline for your website."
  cover = "images/my_cover_image.jpg"
  author = "Author name for posts on the site"
  authorlocation = "Scotland, United Kingdom"
  authorwebsite = ""
  authorbio= "author bio information here"
  logo = "images/my_website_logo.gif"
  googleAnalyticsUserID = "UA-123456789-1"
  # Optional RSS-Link, if not provided it defaults to the standard index.xml
  RSSLink = ""
  githubName = "vjeantet"
  twitterName = "to enable link to your twitter feed, add your handle here"
  facebookName = "to enable link to your facebook page, add your facebook page name here (not the full url)"
  codepenName = ""
  linkedinName = ""
  stackoverflowId = ""
  keybaseName = ""
  flickrName = ""
  instagramName = ""
  email = ""
  pinterestName = ""
  googlePlusName = ""
  set true if you are not proud of using Hugo (true will hide the footer note "Proudly published with Hugo.....")
  hideHUGOSupport = false
  # Setting a value will load highlight.js and enable syntax highlighting using the style selected.
  # See for available styles
  # A preview of above styles can be viewed at
  hjsStyle = "obsidian"
  changefreq = "daily"
  filename = "sitemap.xml"
  priority = 0.5
  1. I don’t plan to walk through everything in the config as that’s a post in itself - instead, a link to the relevant Hugo and Casper docs for this. The frontmatter/page headers format is different - specific information around Casper theme is at the doc link above.

How did you convert your site content?

So, as I mentioned in previous posts, I used powershell to help me convert my site rather than make all the require changes manually.

Specifically, here’s some snippets I came up with and what they achieved (anything in <angle brackets> should be changed to your own values and of course I’m using my D: drive here - change your path accordingly for your setup):

After copying all the Grav content (everything under user/pages/ in my case) to the content directory for Hugo, I needed to essentially flatten the directory and place all the markdown (*.md) files in the content directory for my Hugo site (I recommend first of all trying this step with the -whatif option so you can be sure what it’s going to do before you let it loose!): dir D:\Hugo\sites\<mysite>\content\*.md -recurse|rename-item -NewName {$_.Directory.BaseName +".md"}

Next I placed them in the content\post directory (again, -whatif is your friend until you are sure it is going to do what you want): dir D:\Hugo\sites\<mysite>\content\*.md -recurse|Move-Item -Destination D:\Hugo\Sites\<mysite>\content\post\

The next one is a bit of an eyesore (and I know a lot of powershell purists are going recoil in horror/object to my use of backticks as line continuation characters - feel free to smarten it up and share and I will update this post accordingly! This was thrown together and it shows):

dir D:\Hugo\sites\<mysite>\content\post\*.md | foreach {
    $fname = $_.FullName;
    $tags=Get-Content -raw $fname | Select-String '(?smi)\s{8}-\s(\w{1,})' -AllMatches | foreach {$_.Matches} | foreach { $_.Groups.Captures[1].Value };
    $tagline="tags: [";$tags|foreach {$tagline=$tagline+'"'+$_+'",'};$tagline=$tagline.TrimEnd(",")+"]";
    $content=(get-content $fname -Encoding UTF8 | `
        Select-String -NotMatch '^hero_','^blog_','^show_','taxonomy','category','tag').Line `
        -replace "(\d{2}\:\d{2}) (\d{2})\/(\d{2})\/(\d{4})\'","`$4-`$3-`$2T`$1:00+01:00'`r`n$tagline" -replace '/blog/','/post/'; 
    $content=($content | Select-String -NotMatch '^\s{8}').Line;
    [IO.File]::WriteAllLines($fname, $content);}

What the code above does is as follows.

For each file on the content\post directory:

  • Find all the blog tags
  • Build a new tags line for this post in the format tags: ["blog","tag1","tag2","tag3"] as opposed to the Grav format which was in YAML
  • Get the file content as UTF8 (I had issues with content not being rendered properly otherwise) - stripping off tags not supported in Hugo such as hero tags, taxonomy, category etc.
  • Amend the date tag value format from hh:mm:ss dd/MM/yyyy to yyyy-MM-ddThh:mm:ss+00:00
  • Amend any link to an article with /blog/ in the relative path to be /post/
  • Strip out lines beginning with 8 spaces - this was a hangover from removing unsupported tags earlier - any tags removed which had indented additional settings, those are now orphaned and need removed.
  • Write the file back out - I use [IO.File]::WriteAllLines as I found Write-Output wasn’t correctly outputting as UTF-8 with BOM.

For the few, posts on this site that had images in the post folder, I moved them manually - but if you have a lot of those, you can move them to static\images and amend your markdown to amend the links accordingly like I did with the blog/post change above.

Wrapping up

That covers the main challenges I had in converting my content - other than that, I found Hugo itself to be pretty straightforward to use. You can copy the code that Hugo generates for you to a webserver, an Azure Static Website, pretty much anywhere that can serve static web content.

In my next post I’ll drill down into how to setup a static website on Azure Blob Storage with Hugo content and utilising Azure DevOps to automatically build and deploy the site from a Github repo (and other tricks - such as compressing JPEG images during the build stage - smaller images take less time to deploy and crucially, load faster!).

As ever, thanks for reading and feel free to leave comments below.

If you like what I do and appreciate the time and effort and expense that goes into my content you can always Buy Me a Coffee at

comments powered by Disqus