Hexo SEO

How to do SEO on a Hexo site — the Node.js static site generator. Hexo outputs pure HTML so content is crawlable on the first fetch, but sitemaps, robots.txt, meta descriptions, canonicals, structured data, and stable permalinks all depend on plugins and config.

First published: Jun 26, 2026 · Last updated: Jul 18, 2026 · Advanced
demand #4 in Static Site Generators#41 in Platform SEO#271 in Technical SEO#378 on the site

Hexo is a Node.js static site generator: hexo generate compiles Markdown into pure static HTML, so Googlebot reads your full content on the first fetch with no JavaScript rendering queue. That's the crawlability floor — but it's not finished SEO. Most default themes ship no sitemap, no robots.txt, no per-post meta description, no canonical, and no structured data; you add those with _config.yml settings, front matter fields, theme edits, and plugins (hexo-generator-sitemap, hexo-generator-robotstxt, hexo-generator-feed, hexo-abbrlink). The big traps are a wrong url: in _config.yml, the date-based default permalink that breaks links on rename, and auto-generated category/tag archives that create thin duplicate content. For Baidu-facing sites, the static output removes a real crawling obstacle — though that's not a guarantee of Baidu indexing or ranking.

TL;DR — Hexo (hexo generate) emits pure static HTML, so your content is in the first byte of the response — no Web Rendering Service, no Wave 2 delay. That’s the crawlability floor, not finished SEO. Default themes ship no sitemap, no robots.txt, no per-post meta description, no canonical, and no structured data; you add them via _config.yml, front matter, theme template edits, and plugins (hexo-generator-sitemap, hexo-generator-robotstxt, hexo-generator-feed, hexo-abbrlink, hexo-indexnow). The three traps that actually bite: a wrong url: in _config.yml breaks canonicals/sitemap/feed; the date-based default permalink ties URLs to filenames so a rename breaks links; and auto-generated category/tag archives create thin duplicate content. For Baidu-facing sites the static output is a real advantage.

Hexo’s static output is the crawling advantage

hexo generate runs your Markdown and theme templates through a build step and writes a folder of finished HTML, CSS, and assets. The SEO-relevant fact is when the HTML exists: at build time, once, for everyone — not per request, and not in the browser. Evidence for this claim The Hexo generate command builds static files into the configured public directory. Scope: Hexo generate command. Confidence: high · Verified: Hexo: Commands

Google describes a two-phase process for JavaScript pages: Wave 1 fetches the raw HTML and indexes static content immediately; Wave 2 is a deferred headless-Chromium rendering queue that can take seconds to days. Hexo output skips Wave 2 entirely — every page is raw HTML on the first fetch. Google’s own guidance is blunt that this is the recommended path: dynamic rendering was “a workaround and not a long-term solution… use server-side rendering, static rendering, or hydration as a solution,” and Hexo produces static rendering, one of the three explicitly recommended approaches.

Two more wins fall out of this:

  • Crawlable links by default. Hexo templates emit plain <a href> anchors in static HTML, so they satisfy Google’s requirement that links be real, crawlable URLs — no JavaScript-generated navigation to design around.
  • Cheap to crawl. Static files served from a CDN have effectively zero server-processing cost per request, so Googlebot can crawl at its allowed rate without tripping crawl-rate throttling. As Gary Illyes put it, “if you are making expensive database calls, that’s going to cost the server a lot” — Hexo makes none.

The caveat: zero render-blocking for crawlers is not the same as zero render-blocking for users. Theme JS/CSS still affects Core Web Vitals (more below).

Configure _config.yml first — url: is the big one

Set these global keys before your first deploy:

url: https://yourdomain.com       # must match your live domain, with https://
permalink: :title/                # or :year/:month/:day/:title/ (the default)
title: Site Title
description: Global meta description fallback
author: Your Name
language: en
meta_generator: false             # removes the <meta name="generator" content="Hexo"> tag
  • url: is the single most consequential setting. It’s the site’s configured base URL, which themes and generators can use for canonical tags, feeds, sitemaps, and pagination. A wrong value can propagate into those outputs. Evidence for this claim Hexo's url configuration supplies the website URL used by themes and generators. Scope: Exact downstream fields depend on the installed theme and plugins. Confidence: high · Verified: Hexo: Configuration
  • permalink: — the default :year/:month/:day/:title/ produces date-based URLs that look stale over time and tie the slug to the filename. Cleaner options are /:title/ or /:category/:title/.
  • meta_generator: false strips Hexo’s generator fingerprint from <head>.
  • Deploying to a subdirectory? url: alone isn’t enough — set root: to the subdirectory path too (e.g. root: /blog/). Hexo’s url_for helper builds root-relative links, while full_url_for prepends the full url: value; get root: wrong on a subdirectory deploy and internal links, feeds, and sitemap entries end up pointing at the domain root instead of the actual path.
  • Config precedence: the site’s _config.yml sets the base url/root/permalink values; a theme’s own config (its bundled _config.yml, or a _config.<theme>.yml at the project root) layers on top for display and template settings, but doesn’t override the site-level URL structure — a theme can’t fix a wrong url:.
  • Testing in safe mode? hexo server --safe (or hexo --safe) disables every plugin and script, so the output you’re viewing is missing your sitemap, robots.txt, and any SEO plugin’s tags. Don’t use a safe-mode preview to check SEO output — run a normal hexo generate first.

Front matter SEO controls (and what’s missing)

Native Hexo front matter you can rely on:

---
title: "Article Title for the <title> tag"
date: 2024-01-15
updated: 2024-06-01
tags: [tag1, tag2]
categories: [Category]
permalink: /custom-url/           # overrides the global pattern for this post
published: true
sitemap: false                    # exclude this post from sitemap.xml
---

What is not in native front matter and needs theme support or a template edit:

  • description (the meta description) — most themes don’t print it without a patch.
  • keywords (per-post keyword meta) — and largely pointless anyway; Google has ignored <meta name="keywords"> since 2009. Treat keywords as an organizational aid, not a ranking lever.
  • canonical (override the canonical URL) — theme-dependent.

Keep the updated: field current when you revise a post — sitemap freshness depends on it (next section).

Essential plugins

Sitemap — hexo-generator-sitemap (official):

npm install hexo-generator-sitemap --save
sitemap:
  path:
    - sitemap.xml
  tags: true
  categories: true

It reads lastmod from each post’s updated field (falling back to date) — which matters because Bing warns that lastmod “should reflect the true last modification time of the page content, not the sitemap file itself.” After hexo generate, submit the resulting sitemap.xml in Google Search Console and Bing Webmaster Tools. And heed Bing’s other warning: “Do not submit a static sitemap and forget it. New pages won’t get picked up” — for Hexo that means re-deploying and re-submitting (or using IndexNow) after every new post.

robots.txt — hexo-generator-robotstxt:

npm install hexo-generator-robotstxt --save
robotstxt:
  useragent: "*"
  allow:
    - /
  disallow:
    - /private/
  sitemap: https://yourdomain.com/sitemap.xml

Without it, Hexo produces no robots.txt at all — no sitemap pointer for crawlers.

RSS — hexo-generator-feed: relevant for aggregators and Google News eligibility; John Mueller includes an RSS feed on his own static site.

IndexNow — hexo-indexnow: Bing recommends pairing sitemaps with IndexNow for real-time, URL-level submission. Drop the generated key file into source/ so it deploys as yourdomain.com/<key>.txt. Note this is for Bing, Yandex, and others — not Google, which doesn’t use IndexNow for general pages.

The default permalink has two SEO problems: renaming the Markdown file changes the URL (because the slug comes from the filename), and non-Latin titles (Chinese, emoji) produce garbled URLs. hexo-abbrlink decouples the URL from the filename by writing a stable hash back into front matter:

npm install hexo-abbrlink --save
permalink: posts/:abbrlink/
abbrlink:
  alg: crc32    # crc16 or crc32
  rep: hex      # dec or hex

You get something like /posts/3a9c2b1d/ that survives file renames, category changes, and date edits. The underlying rule stands either way: once a URL is published, don’t change it without a 301 redirect — a rename without one destroys link equity and any accumulated ranking signals.

Two more permalink pitfalls worth knowing before you pick a pattern:

  • Don’t use the :id token. Hexo documents post IDs as not persistent across a database cache reset — if your permalink pattern includes :id, a cache rebuild can silently change every post’s URL. Stick to :title, :abbrlink, or another stable token.
  • trailing_index and trailing_html are separate controls, not one setting. Hexo’s pretty_urls config toggles them independently — one governs whether a trailing index.html is stripped from generated links, the other whether a trailing slash is added. If these don’t match how your host actually resolves URLs (some hosts redirect /post to /post/, others don’t), you can end up with internal links, canonicals, and sitemap entries disagreeing with the URL the server actually serves.

NexT theme SEO configuration

NexT is the most-used Hexo theme, configured in _config.next.yml:

canonical: true                 # builds canonical from url: + page path

webmaster_tools:
  google: VERIFICATION_STRING
  bing: VERIFICATION_STRING
  baidu: VERIFICATION_STRING

open_graph:
  enable: true
  twitter_card: summary_large_image

The NexT meta-description gap: NexT does not automatically inject a per-post <meta name="description"> from the front matter description:. You patch the theme’s head template to fall back from page.description to the site description. This is the single most common NexT SEO miss. (NexT does support canonical: true and built-in Open Graph / Twitter Cards, so those at least work once enabled.)

For non-NexT themes, you usually create or edit a head partial to emit og:title, og:description, og:image, og:url, and twitter:card — support varies widely by theme.

Who owns each tag when a theme and a plugin both emit it. A theme’s built-in Open Graph/canonical support and an all-in-one plugin like hexo-seo (which generates its own breadcrumb, website, and article schema) can both try to write the same tag. Turning both on produces duplicate or conflicting <meta> tags, canonicals, or JSON-LD blocks in the same <head>. Pick one layer as the owner for each tag category — theme or plugin, not both — and disable the corresponding feature on the other before you ship.

Category and tag pages: the duplicate-content trap

Hexo auto-generates an archive page for every category and every tag, each filled with post excerpts that also appear on the posts themselves. At scale that’s hundreds of thin pages competing for the same queries and diluting crawl attention. Your options:

  1. sitemap: false per archive (impractical at scale).
  2. Theme-level noindex on the category/tag archive templates.
  3. Keep them indexed only if a category is a genuine, curated topic hub with real value.
  4. Self-referencing canonical on each paginated archive page (page 2 canonicalizes to itself, not page 1).

One caveat on option 2: long-term noindex eventually makes Google stop following the links on that page, so noindexing tag pages can reduce crawling of posts that are only linked from those tags. Make sure such posts are reachable some other way.

Your route inventory is bigger than source/. Category, tag, archive, and pagination pages don’t come from Markdown files — Hexo’s generator API is what actually creates them, and themes or plugins can register their own generators too. That means the crawlable URL surface can silently expand beyond what you’d expect from browsing your source/ folder. After installing or updating a theme or plugin, run hexo generate and diff the route list in public/ rather than assuming you know every URL Hexo is producing.

Structured data / JSON-LD

Hexo injects no structured data by default. Either add a BlogPosting JSON-LD block to your theme’s post template (headline, datePublished, dateModified, author, publisher, url) or use an all-in-one plugin like hexo-seo that generates breadcrumb, website, and article schema for you.

Core Web Vitals

Static output is fast, but themes can undo that. NexT and many themes bundle large JS/CSS and icon-font libraries (Font Awesome is a frequent LCP/render-blocking culprit). Audit with Lighthouse, then reach for hexo-all-minifier (HTML/CSS/JS compression at build time), hexo-lazyload-image, and defer/async on third-party scripts like analytics and ads.

Targeting Baidu (Chinese-audience sites)

Baidu’s crawler handles JavaScript poorly, so Hexo’s static HTML removes one obstacle to Baidu discovery. Add baidu_site_verification in NexT, generate a Baidu-format sitemap with hexo-generator-baidu-sitemap, and push URLs actively with hexo-submit-urls-to-search-engine. One hosting note: GitHub Pages is blocked in mainland China — use Gitee Pages, Cloudflare, or a Chinese CDN for Baidu traffic.

Treat that as a crawlability advantage, not a guarantee. Static HTML doesn’t by itself prove Baidu is discovering, indexing, or ranking your pages — confirm actual crawl activity and indexing status directly in Baidu Ziyuan (Baidu’s webmaster tools) rather than assuming static output alone secures visibility.

Deploying to GitHub Pages — the SEO gotchas

  • Set url: to your live address (custom domain with https://) before the first deploy.
  • Put a CNAME file in source/ for custom domains so it isn’t reset on every hexo deploy.
  • Enforce HTTPS in repo Settings → Pages after DNS propagates.
  • Prefer a GitHub Actions workflow over hexo-deployer-git to avoid CNAME-reset issues.

Hexo vs. Hugo vs. Jekyll vs. Astro, for SEO

All four ship pure static HTML, so the indexability floor is identical — there’s no framework-level SEO disadvantage in Hexo. The differences are operational: Hugo (Go) builds far faster at large scale and has a built-in sitemap; Jekyll is the GitHub Pages default; Astro ships zero JS by default via islands. Hexo’s edge is a rich npm plugin ecosystem and a large Chinese-language community. Content quality, not the generator, decides rankings — as Mueller said of switching generators, “Will your site’s SEO suddenly explode? No. Also No.”

For the cluster overview and the other generators, see Static Site Generators; for the broader rendering picture, see JavaScript SEO.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin an expert quote first.