Finding and editing posts is one half of the daily routine. The other is everything around media. Images, attachments, shortcodes, plus local preview and deploying to the host. None of that needs a terminal switch or a file manager either.

:Hugo media

With no argument, :Hugo media (<leader>hm) opens a picker over all the subcommands, in case you don’t want to memorise them. Otherwise call the subcommand directly.

:Hugo media import

Copies a file from your machine into the site. Two steps:

  1. Pick the source file, the picker starts in your home directory.
  2. Pick the destination folder inside the site.

Name collisions get a -2, -3 suffix. The file is only copied, no markdown link is written into the post automatically. That’s what :Hugo media insert is for.

:Hugo media insert page

Needs a markdown file in the buffer. The picker lists every post in the site, picking one inserts [Title]({{< relref "path" >}}) at the cursor. Hugo resolves the link at build time and picks the right language.

:Hugo media insert image

Needs a markdown file in the buffer. The picker shows every image in the current bundle and in static/. Picking one inserts ![](path) at the cursor. Type the alt text into the square brackets afterwards.

Needs a markdown file in the buffer. Like insert image, but for non-markdown files (PDFs, ZIPs, MP3s). You pick the file, type a link text, the plugin inserts [text](path).

:Hugo media insert shortcode

Inserts a Hugo shortcode with all its parameters at the cursor. The picker lists every available shortcode, your own from layouts/shortcodes/, the ones the theme provides, and Hugo’s built-ins. The plugin reads the shortcode template, picks up the named parameters, and inserts them empty-prefilled.

Example for figure with parameters src, alt, caption:

{{< figure src="" alt="" caption="" >}}

The cursor lands inside the first empty "", ready for typing. For paired shortcodes like {{< quote >}}…{{< /quote >}} the cursor jumps into the body.

:Hugo media cover

Needs any file from the target bundle in the buffer. Sets the cover image for the bundle. The picker lets you pick an image (same pool as insert image), the plugin writes cover.image to every language sibling.

If the front matter already has a cover.alt field, you’re also prompted for alt text, which lands only in the current language. Other cover fields (relative, caption, hidden) aren’t touched. Put them in your archetype if you want them preseeded.

:Hugo media rename and :Hugo media delete

Rename or delete a media file directly inside the bundle. Markdown files aren’t in the pool. rename can’t cross directories, the file stays where it is.

Neither updates markdown links pointing at the file. After a rename or delete, a ripgrep scan over content/ runs automatically and tells you how many places still reference the old name. Resolving them is on you.

Preview

:Hugo preview

<leader>hp starts hugo server in a terminal split at the bottom and opens the current page in the browser. Drafts and future-dated posts are included in the preview.

The URL is derived from the current buffer’s path:

  • content/_index.mdhttp://localhost:1313/
  • content/posts/2026/hello.mdhttp://localhost:1313/posts/2026/hello/
  • content/posts/2026/hello/index.de.mdhttp://localhost:1313/de/posts/2026/hello/

Buffers outside content/ fall back to the site root. Unusual permalink or language configs may make the derived URL slightly off.

If the server is already running, another :Hugo preview just opens the current page in the browser without restarting it. Handy for jumping between posts.

:Hugo preview stop (<leader>hP) stops the server. Closing the terminal split works too. Quitting neovim takes the server with it. One preview at a time.

:Hugo publish

<leader>h! runs deploy.sh at the site root. Output streams into a terminal split at the bottom. If no deploy.sh is there, the plugin aborts with an error.

Why a script and not a built-in build-and-upload? Deployment looks completely different from site to site, rsync to a VPS, sftp to shared hosting, git push to a provider that builds itself, aws s3 sync, the Netlify CLI, whatever. Covering all of that natively would mean endless config knobs. Bundling build and upload into deploy.sh also gives you full control over the hugo flags (--minify, --gc, custom environments) without the plugin growing an option per wish.

Example with hugo --minify and rclone:

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"

hugo --minify

rclone copy public/ <remote>:/<target-path>/ \
  --exclude ".DS_Store" \
  --progress

<remote> is the destination you set up earlier with rclone config, <target-path> is the directory at the host. Make the script executable with chmod +x deploy.sh, then <leader>h! builds and uploads in one shot.

Before running, the plugin asks for confirmation and shows the script path and working directory. Default answer is No, you have to pick Yes explicitly.

Where Things Go From Here

That gives you the full hugo-cms.nvim toolset for a single-language site. How that ties into multilingual sites with per-language slugs and cover alt texts is the topic of its own post.