Contents11
- The short answer — split public and public/resources
- Files in scope
- Why organize an image sitemap
- Comparison: image sitemap vs. regular sitemap
- Decide the directory for per-article images
- Put shared images and the favicon directly under public
- Install vuepress-plugin-sitemap and set hostname
- Place robots.txt directly under public
- Pitfalls — what tripped me up at the time
- FAQ
- Closing
2026 update: A post ported over from the old VuePress blog. Both VuePress 1.x and vuepress-plugin-sitemap are effectively no longer maintained, so this isn’t a reference for new builds (citation needed). It’s left up as a record of how image sitemaps and image-directory layout were thought through at the time. All code snippets assume VuePress 1.x as of 2021.
A record of how image placement and sitemap settings were laid out back when I was building a personal blog on VuePress 1.x. The setup that was standard at the time — managing images in the same folder structure as the Markdown posts, hard-coding the favicon into the head array of config.js, and emitting the sitemap with vuepress-plugin-sitemap — is kept here as-is.
What this post covers is four things: “how to split the image directory”, “how to declare the favicon”, “how to configure the sitemap plugin”, and “where to place robots.txt”. The broader VuePress directory layout itself is covered separately in VuePress/blog directory structure.
The short answer — split public and public/resources
Short answer: physically separate the files in image and sitemap territory between those served directly at the root and those organized per article.
VuePress 1.x copies the contents of src/.vuepress/public to the root of dist as-is. Anything placed directly under public is served from the site root after the build. The split I went with: files where “being at the root has meaning in itself” — the favicon, robots.txt, sitemap.xml — go directly under public, and files that need to be “organized per article” — the article images — go under public/resources.
The reason is that putting both at the same level kills visibility once the file count grows. With hundreds of article images directly under public, the favicon gets buried in the pile.
Files in scope
Short answer: the two places touched here are the public folder under src/.vuepress and config.js.
Specifically, the layout looks like this.
src
└─ .vuepress
├─ public
│ ├─ resources
│ │ └─ category
│ │ └─ page
│ │ └─ img.jpg
│ ├─ top.jpg
│ ├─ favicon.ico
│ ├─ favicon.png
│ └─ robots.txt
└─ config.js
A caveat: the directory layout at a broader scope is covered in VuePress/blog directory structure.
Why organize an image sitemap
Short answer: to tell crawlers where the images live, and to keep me from losing track of images on the authoring side.
An image sitemap is an extension that, inside sitemap.xml, lists “this page has these images” against each URL. A regular sitemap is just a list of page URLs; an image sitemap also passes image URLs to the crawler (citation needed / see Google’s image sitemap spec). In theory this opens up another inbound path from image search.
That said, vuepress-plugin-sitemap 2.x on VuePress 1.x doesn’t, by default, emit image entries to the sitemap. What this post covers stops at “images are served correctly and a page-level sitemap is being emitted” — it doesn’t cover a strict image sitemap with image entries included.
Comparison: image sitemap vs. regular sitemap
| Lens | Regular sitemap (sitemap.xml) | Image sitemap |
|---|---|---|
| What it conveys | List of page URLs | Each page URL + the images contained on that page |
| How it’s submitted to Google | Search Console / robots.txt | Same (embedded as extension elements) |
| vuepress-plugin-sitemap 2.x support | Generated by default | Not supported by default (citation needed) |
| How well it works for a personal blog | Essentially required | Worth considering if you’re targeting image-search traffic |
The reasoning is that image-search traffic moves the needle for photo media but doesn’t shift much for text-driven tech blogs. Setting up just the regular sitemap first and stepping into the image sitemap only after image-search traffic starts showing up was a good enough order.
Decide the directory for per-article images
Short answer: under src/.vuepress/public/resources, create per-article folders and place the images there.
src
└─ .vuepress
└─ public
└─ resources
└─ category
└─ page
└─ img.jpg
The reason is that aligning the Markdown folder structure with the image folder structure one-to-one makes it easier to trace where images live when posts get moved or deleted later. A single post often contains several images, so I split folders at the page level.
A caveat: match folder names to the article’s slug. Renaming on the article side and renaming the image folder at different times causes broken links.
Put shared images and the favicon directly under public
Short answer: the top image, profile image, and favicon all sit directly under src/.vuepress/public.
src
└─ .vuepress
└─ public
├─ favicon.ico
└─ favicon.png
The reason is that shared images at the time were only two or three files — not enough to warrant a folder. The plan was, once shared images grew, to break out a public/resources/common folder and move them in, like this.
src
└─ .vuepress
└─ public
└─ resources
├─ category
└─ common
└─ img.jpg
Because VuePress has no index.html, the favicon is declared by writing a link tag in the head array of config.js.
module.exports = {
head: [
["link", { rel: "icon", href: "/resources/favicon.ico" }],
],
}
If you want PWA support, prepare a PNG favicon separately from the ico and put it at the same level. At the time the site’s PWA support was only half done, so I’ve left out that part of the description.
A caveat: the head declaration is reflected in every built HTML file. Get the href path off by even one level and the favicon stops loading across the entire site.
Install vuepress-plugin-sitemap and set hostname
Short answer: add vuepress-plugin-sitemap to the plugins array, declare hostname and exclude, and sitemap.xml is generated automatically.
The site used vuepress-plugin-sitemap (at the time of writing, vuepress-plugin-sitemap: ^2.3.1). The setup is finished entirely in config.js.
module.exports = {
plugins: [
[
"sitemap",
{
hostname: "https://xxxx",
exclude: "https://xxxx/404.html"
},
]
],
}
The reasoning: hostname is required because it’s used to assemble the URLs inside the sitemap. exclude is where you list URLs you want kept out of the crawl, like the 404 page. The other fine-grained options didn’t really need touching at personal-blog scale.
A caveat: vuepress-plugin-sitemap 2.x is for VuePress 1.x. For VuePress 2.x, it’s a different package and a different configuration API (citation needed).
Place robots.txt directly under public
Short answer: robots.txt has to sit directly under src/.vuepress/public.
src
└─ .vuepress
├─ public
│ └─ robots.txt
└─ config.js
The reason is that VuePress copies the contents of public to the root of dist. Place it in a subfolder and it isn’t served from the root, so to crawlers the site looks like one without a robots.txt.
The contents are the same as for any other framework.
User-agent : *
Disallow :
Sitemap : https://xxxx/sitemap.xml
Including a Sitemap line lets crawlers find sitemap.xml even without going through Search Console.
A caveat: leaving Disallow blank means “allow everything”, but if you’ll later expose a staging environment through the same setup, an operation flow that swaps robots.txt per environment is safer.
Pitfalls — what tripped me up at the time
Short answer: two things tripped me up — image paths colliding with the favicon path, and sitemap.xml caching more aggressively than expected.
- Writing image references as
/img.jpgcollides with files that already sit directly under public. I standardized on writing article images with full paths like/resources/category/page/img.jpg - sitemap.xml caches easily in both CDNs and browsers, so right after a deploy it can still show the old contents when viewed from Search Console. Resubmit the rebuilt URL and confirm it’s picked up
- After every build, check that robots.txt and sitemap.xml are emitted directly under dist. Misplacing them under public is the most easily missed kind of accident — the build passes, but the file simply isn’t at the root
FAQ
Q. Is an image sitemap a different thing from a regular sitemap.xml? A. An image sitemap is an extension of sitemap.xml that lists image:image entries against each URL. A regular sitemap is just a list of pages; an image sitemap tells the crawler “which images live on which page” as well. vuepress-plugin-sitemap 2.x doesn’t emit image entries by default, so if you wanted to include images you had to extend it with another plugin or regenerate the sitemap yourself.
Q. Why split off public/resources instead of putting everything directly under public? A. If everything sits directly under public, article images, shared images, the favicon, and robots.txt all end up at the same level and visibility drops. Slipping public/resources in as one extra layer physically separates per-article images (resources/category/article-name) from things that need to live at the public root (favicon, robots.txt).
Q. Is the VuePress 1.x sitemap plugin still usable in 2026? A. Technically it still runs, but maintenance on both vuepress-plugin-sitemap and VuePress 1.x itself has effectively stopped. For new builds, picking an SSG with sitemap generation built in officially — Astro or Next.js — is easier in both the short and long term (citation needed). For maintaining an existing site, pinning Node LTS and plugin versions to extend its life is the realistic move.
Q. What happens if robots.txt is placed somewhere other than directly under public? A. It won’t be emitted under dist after the build, so it’s not served at the root and crawlers treat the site as one without a robots.txt. Because VuePress copies the contents of public to the root of dist, public is the only place to put robots.txt if you want it served at the same level as sitemap.xml.
Closing
The initial setup for image and sitemap territory on VuePress 1.x — splitting the roles of “directly under public” and “public/resources”, letting config.js’s head load the favicon, declaring hostname and exclude through vuepress-plugin-sitemap, and placing robots.txt directly under public — covers the whole loop on its own.
The settings themselves are small, but fixing the image-directory split later, after the post count has grown, hurts more than it should. Drawing the line once at the start — “per-article images and shared images live at different levels” — saves trouble down the road.
As of 2026 VuePress 1.x is hard to recommend as a choice for new builds, so this post is kept as material for comparing the build philosophy at the time against Astro and the like.