
The Hugo “Droste Effect”.
I’ve been working on nonpunctual.org quite a bit lately. It might not show - websites are harder than they look.
One of the things that is very helpful in Hugo for creating new content is the archetypes schema. Hugo users can create “archetype” files in their project - .md file templates that can be populated with:
- auto-generated content file metadata (frontmatter)
- markdown formatting / document structure
- headers
- lists
- links
- raw HTML
- text
- etc.
- shortcodes
- etc.
You can even use archetypes to create directory structures for content (which I am using for photos).
It’s a great feature, but it’s a bit limited. I wanted to be able to add frontmatter metadata & create new Hugo content files in a single workflow, so, I wrote an interactive wrapper script around hugo new (the hugo command for spinning up new content files with archetypes templates).
Hugo frontmatter is the content file metadata for controlling content publishing behavior. It’s YAML, so the script uses yq for populating the frontmatter fields as needed - yq is available for install via Homebrew.
brew install yq
Running the script without arguments drops you into an interactive prompt:
Select one of these content types at the prompt:
1) gear
2) place
3) post
4) project
content type> 3
name (filename without .md): my new post
Frontmatter (Press Enter to skip):
• description:
• categories:
• tags (comma-separated):
Created: content/posts/my new post.md
You can also pass the content type and name directly as arguments to skip the prompts:
./hugo-content.sh posts "my new post"
This script is a bit particular to my site but there’s not a ton to it. This post is really intended to describe the idea of doing more with the hugo new command. The script or the idea could be easily adapted for any Hugo site. If you’ve wanted some automation around creating new Hugo files there are lots of cooler workflows than this, but, this is what I am using for now & it’s working well.
Enjoy!
#!/bin/bash
# shellcheck disable=SC2207
# variables
contyp='gear places posts projects'
prjdir='/Users/Shared/nonpunctual/nonpunctual-site'
# locate hugo project dir
if [ ! -d "$prjdir/content" ]
then
echo "Error: $prjdir/content not found."; exit 1
fi
cd "$prjdir" || exit 1
# user interaction
type="$1"
name="$2"
if [ -z "$type" ]
then
while true
do
printf "\nSelect a content type at the prompt:\n\n1) gear\n2) place\n3) post\n4) project\n\ncontent type> "; read -r typchk
case "$typchk" in
1 ) type='gear'; break ;;
2 ) type='places'; break ;;
3 ) type='posts'; break ;;
4 ) type='projects'; break ;;
* ) printf "\nPlease enter 1, 2, 3, or 4. Try again...\n"; continue ;;
esac
done
fi
if ! echo "$type" | grep -Eq 'gear|places|posts|projects'
then
echo "Error: invalid type '$type'. Must be one of: $contyp"; exit 1
fi
if [ -z "$name" ]
then
printf "\nname (filename without .md): "
read -r name; echo
fi
if [ -z "$name" ]
then
echo "Error: name is required."; exit 1
fi
confil="content/$type/$name.md"
if [ -f "$confil" ]
then
echo "Error: $confil already exists."; exit 1
fi
# populate frontmatter
fldstr(){
local path="$1" value="$2"
yq --front-matter='process' -i "$path = \"$value\"" "$confil"
}
# tags
fldarr() {
local path="$1" value="$2"
local tagarr token
IFS=$'\n'
tagarr=($(printf '%s' "$value" | tr ',' '\n'))
for token in "${tagarr[@]}"
do
token="${token#"${token%%[![:space:]]*}"}"
token="${token%"${token##*[![:space:]]}"}"
token="${token#\"}"
token="${token%\"}"
yq --front-matter='process' -i "$path += [\"$token\"] | $path style = \"flow\" | ${path}[] style = \"double\"" "$confil"
done
}
# prompt for a comma-separated value written as an array; skip if empty
prmtarr() {
local label="$1" path="$2"
printf "• %s: " "$label"
read -r value
if [ -n "$value" ]
then
fldarr "$path" "$value"
fi
}
# prompt for a string field; skip if empty
prmtstr() {
local label="$1" path="$2"
printf "• %s: " "$label"
read -r value
if [ -n "$value" ]
then
fldstr "$path" "$value"
fi
}
# create .md file
hugo new "$type/$name.md"
fldstr ".title" "$name"
printf "\nFrontmatter (Press Enter to skip):\n"
case "$type" in
gear)
prmtarr "gear-categories" ".\"gear-categories\""
prmtarr "tags (comma-separated)" ".tags"
prmtstr "year" ".year"
prmtstr "description" ".description" ;;
places)
prmtarr "place-categories (been/not been)" ".\"place-categories\""
prmtarr "tags (comma-separated)" ".tags"
prmtarr "year(s) (comma-separated)" ".year"
prmtstr "description" ".description"
prmtstr "country" ".country"
prmtstr "region" ".region"
prmtstr "city" ".city"
prmtstr "timezone" ".timezone"
prmtstr "postal code" ".postal_code"
prmtstr "calling code" ".calling_code"
prmtstr "when" ".when" ;;
posts)
prmtarr "categories" ".categories"
prmtarr "tags (comma-separated)" ".tags"
prmtstr "description" ".description" ;;
projects)
prmtarr "project-categories" ".\"project-categories\""
prmtarr "tags (comma-separated)" ".tags"
prmtarr "people (comma-separated)" ".people"
prmtstr "year" ".year"
prmtstr "description" ".description" ;;
esac
printf "\nFrontmatter populated: %s\n" "$confil"