optimize static resources for multilingual site
2025-09-06  / site  / optimize

Foreword

Because post_asset_folder is set in _config.yml as below, resources in each post can be found at directory whose name is the same as post.

1
2
3
4
post_asset_folder: true
marked:
prependRoot: true
postAsset: true

However, redundant resources accumulate significantly after i18n is supported in this site. Post in different language should have different content, while the resources should be the same. Below is the directory structure of source. Resource directories with (*) in different language are duplicated and can be optimized.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
source
├── _posts
│ ├── en
│ │ └── 2025
│ │ ├── 07
│ │ │ ├── online-404-error-troubleshooting (*)
│ │ │ └── online-404-error-troubleshooting.md
│ │ └── 08
│ │ ├── exploring-multilingual-site-solutions-in-hexo (*)
│ │ └── exploring-multilingual-site-solutions-in-hexo.md
│ └── zh-CN
│ └── 2025
│ ├── 07
│ │ ├── online-404-error-troubleshooting (*)
│ │ └── online-404-error-troubleshooting.md
│ └── 08
│ ├── exploring-multilingual-site-solutions-in-hexo (*)
│ └── exploring-multilingual-site-solutions-in-hexo.md
├── en
│ ├── 404
│ ├── about
│ ├── albums
│ │ ├── animal
│ │ │ ├── cat (*)
│ │ │ ├── cat.md
│ │ │ ├── dog (*)
│ │ │ ├── dog.md
│ │ │ └── index.md
│ │ └── index.md
│ ├── categories
│ └── tags
└── zh-CN
├── 404
├── about
├── albums
│ ├── animal
│ │ ├── cat (*)
│ │ ├── cat.md
│ │ ├── dog (*)
│ │ ├── dog.md
│ │ └── index.md
│ └── index.md
├── categories
└── tags

Optimize

Design

At first, in order to reduce the impact on how resources are referred in the previous way, I need to design how static assets are organized. Luckily, just move all duplicated resource directories to /source/images as below (directories with (*)), then few changes need to be made if resources are referred by relative path.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.
├── _posts
│ ├── en
│ │ └── 2025
│ │ ├── 07
│ │ │ └── online-404-error-troubleshooting.md
│ │ └── 08
│ │ └── exploring-multilingual-site-solutions-in-hexo.md
│ └── zh-CN
│ └── 2025
│ ├── 07
│ │ └── online-404-error-troubleshooting.md
│ └── 08
│ └── exploring-multilingual-site-solutions-in-hexo.md
├── en
│ ├── 404
│ ├── about
│ ├── albums
│ │ ├── animal
│ │ │ ├── cat.md
│ │ │ ├── dog.md
│ │ │ └── index.md
│ │ └── index.md
│ ├── categories
│ └── tags
├── images
│ ├── 2025
│ │ ├── 07
│ │ │ └── online-404-error-troubleshooting (*)
│ │ └── 08
│ │ └── exploring-multilingual-site-solutions-in-hexo (*)
│ ├── albums
│ │ └── animal
│ │ ├── cat (*)
│ │ └── dog (*)
│ ├── my_avatar.png
│ └── my_favicon.png
└── zh-CN
├── 404
├── about
├── albums
│ ├── animal
│ │ ├── cat.md
│ │ ├── dog.md
│ │ └── index.md
│ └── index.md
├── categories
└── tags

Then, find a way to replace path of resource refered in each post. Because resource can be referred by relative or absolute path (starts with http or https or /), if by absolute path then just use that path, otherwise find resource in a directory with the same name as the post in the /images directory. So only relative path need to be converted to /images/[prefix]/[name]/[resource], in which name means post name, resource means resource name, while prefix means path from /source/[lang] to post.

For exmaple, for post /2025/07/my-post.md regardless of language.

  1. If resource is referred as ./image.png, resource path will be converted to /images/2025/07/my-post/image.png. In this case, prefix is 2025/07, name is my-post, resource is image.png. Notice if you use relative path, make sure it always starts with './'. If you use ../another-post/image.png, it will not be converted to /images/2025/07/another-post/image.png. If you do need to use resources belonging to another post, try to use abosulte path like /images/2025/07/another-post/image.png.
  2. If resource is referred as /images/2025/07/my-post/image.png, since it's absolute path, resource path will not be converted.
  3. If resource is referred as https://exmaple.com/image.png, resource path will not be converted either.

More exmaples can be found below.

Implement

Create a file named as hexo-filter.js in themes/arch/scripts and use following code. In callback of after_post_rende, all data including original content and parsed content and page meta information can be accessed. Get the src of all <img> and apply the above path conversion if src is relative path. That's it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// http, https, / 开头的都是绝对路径
const absPathRegExp = /^(https?:\/\/|\/)/i;

function replaceRelativePath(src, prefix) {
// 绝对路径,不变
if (absPathRegExp.test(src)) {
return src;
}
// 相对路径
// 去除开头的 ./ 或 ../
let cleanSrc = src.replace(/^(\.\/|\.\.\/)+/, "");

// 拼接新的路径
return [prefix, cleanSrc].join("/");
}

/**
* Hexo 过滤器,处理文章和页面中的相对图片路径
* 将相对路径替换为 /images/ 目录下对应的路径,绝对路径(以 http:// 或 https:// 或 / 开头)不做处理, / 表示网站根目录
* 例如
* 1. 对于 post,路径是 /2025/07/my-post.md,图片路径是 ./image.png,则替换为 /images/2025/07/my-post/image.png
* 2. 对于普通 page,路径是 /about/index.md,图片路径是 ./image.png,则替换为 /images/about/image.png
* 3. 对于 albums page,路径是 /albums/index.md,图片路径是 ./animial/dog/1.png,则替换为 /images/albums/animial/dog/1.png
* 4. 对于 album page,路径是 /albums/animal/dog.md,图片路径是 ./1.png,则替换为 /images/albums/animial/dog/1.png
*
*/
hexo.extend.filter.register("after_post_render", function (data) {
const { source, type } = data;

// 不处理 tags 和 categories
if (["tags", "categories"].includes(type)) {
return data;
}

// 从 source 中提取目录名
// 对于 post,格式是 _posts/:lang/:year/:month/:title.md,比如 _posts/en/2025/07/problems-using-hexo-theme.md
// 对于 page,格式是 :lang/xxx/:title.md,比如 en/about/index.md, en/albums/index.md, en/albums/animal/dog.md
const parts = source.split("/");

let dirParts = !type ? parts.slice(2, -1) : parts.slice(1, -1);
// post 和 album 类型,需要加上 title 作为最后一级目录
if (!type || type === "album") {
dirParts = dirParts.concat(parts[parts.length - 1].replace(/\.md$/, ""));
}

const prefix = `/images/${dirParts.join("/")}`;

// data.content 是文章渲染后的 HTML 内容
// 使用正则匹配所有 <img> 标签的 src 属性
data.content = data.content.replace(
/<img\s+([^>]*?)src=["']([^"']+)["']([^>]*?)>/gi,
function (match, pre, src, post) {
// 绝对路径,不变
if (absPathRegExp.test(src)) {
return match;
}
// 返回替换后的 img 标签
return `<img ${pre}src="${replaceRelativePath(src, prefix)}"${post}>`;
}
);

// 处理 albums 类型的 children 字段
if (type === "albums" && Array.isArray(data.children)) {
data.children.forEach((child) => {
if (child.cover) {
child.cover = replaceRelativePath(child.cover, prefix);
}
});
}

// 处理 album 类型的 resources 字段
if (type === "album" && Array.isArray(data.resources)) {
data.resources.forEach((resource) => {
if (resource.src) {
resource.src = replaceRelativePath(resource.src, prefix);
}
});
}

return data;
});

Usage

If resource is referred using <img /> by relative path in post, nothing needs to be done. For albums and album type page, since there are absolute path, some changes are made as below.

image.png


Please keep in mind that resource path is only converted when you using <img /> with relative path. Core logic as below.

1
2
3
4
5
6
7
8
9
10
11
data.content = data.content.replace(
/<img\s+([^>]*?)src=["']([^"']+)["']([^>]*?)>/gi,
function (match, pre, src, post) {
// 绝对路径,不变
if (absPathRegExp.test(src)) {
return match;
}
// 返回替换后的 img 标签
return `<img ${pre}src="${replaceRelativePath(src, prefix)}"${post}>`;
}
);

If you use markdown syntax or hexo tag using relative path like below, resource path will not be converted and image will fail to load.

1
2
3
![image.png](image.png)

{% asset_img "image.png" "image.png" %}