Auto-Deploy Tool Makes Hexo Easier to Use

Since our blogs deployed, now we can post our articles. Hexo is a static blog system, so we need to run a command to generate and deploy the new article. I think it’s not that convenient and wonder is there any possible ways to make it easier. Searching online, I found some existing tools that can be used. But I love doing things on my own, plus the principal of this feature is not complicated, so I decided to build my own tool. This article well introduces my auto-deploy tool. You are free to use or edit it for your blogs.

Node.js

At first, I need to select a coding language. The blog is on Ubuntu, a Linux environment, which means Shell and Python are the most convenient coding language to use. Besides, the Node.js required by Hexo is on the waiting list too. I used Shell a lot but seldom write Shell scripts; I love Python, but currently, I use Node.js every day due to internship. So I’d choose Node.js to do this job. If time promise, I will add Python and Shell version of this tool. No intend to update this; A more powerful tool comes out: Hexo-Auto-Deployer

Principal

Now, let’s look at what we should do if we post new articles manually.

  1. Upload .md file to _post directory (what hexo new command does is generate .md file by given template)
  2. Use hexo d -g to generate and deploy

For the first step, I currently simply use sftp to upload. And I’m considering building a tool to download new articles from OneDrive.

The second step is the main point of this article. And it contains two parts:

  • Watch the changes of _post directory:
    • add
    • delete
    • edit
  • Run run command

Directory watching

How does a program know the changes within a directory? The first idea is to use setInterval function to obtain all files of the directory consistently. And then, by comparing the newly acquired last-modified-time(mTime) and last obtained time, we can know whether files were edited. But this method has a drawback: the interval time is tricky to set. Too short to waste resources while too long to have a slow response. No one wants to wait a long time before they can see what they’ve just written. If so, why not just publish it manually?

Looking through the official document of fs module, I find the function watch which exactly meets my desire. It can know the changes of given files on time by an event handler. Codes below are the basic version.:

1
2
3
4
5
6
7
8
9
const fs = require('fs');		// file system module
const path = require('path'); // path module

fs.watch(path.resolve(__dirname, 'blog', 'source', '_posts'), (event, filename) => {
if (filename) {
console.log(`Changes may happen on file ${filename}`);
// core code: deploy
}
});

Straight forward. Function watch takes a param and a call-back function. The param is the directory to watch, where we use path.resolve to build the path. By the way, __dirname is the current working directory. The call-back function takes two params, event is the type of event which I will not use in this tool, and the filename is the name of the file that triggered the change event.

Key point

If you run the codes above and try to add or edit files, you may find the output of every action is more than once. This is because some actions, like writing, some editors may clean old content and write the new one. And some actions may have some middle states that continue tens of or hundreds of milliseconds.

You may at first think that’s not a big problem. But obviously, this wastes lots of resources. You don’t want the tool to deploy your blog twice more whenever you post a new article. So let’s make some changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs');		// file system module
const path = require('path'); // path module

let blogWait = false;
fs.watch(path.resolve(__dirname, 'blog', 'source', '_posts'), (event, filename) => {
if (filename) {
if (blogWait) return;
blogWait = setTimeout(() => {
blogWait = false;
console.log(`Changes may happen on file ${filename}`);
// core code: deploy
}, 200);
}
});

You see, I introduced a signal variable wait that initially set to false. The first time a change occurs, a delayed function will be placed: set that signal variable to false again. Meanwhile set the wait signal variable to the return value of setTimeout function. Because the return value is greater than zero, wait variable will be treated as true in if condition.

Deploy

Running Shell commands need child_process module, which can be used after import. For example, ‘ll` command:

1
2
3
4
5
6
7
8
9
10
const { exec } = require('child_process');

exec('ls -l', (error, stdout, stderr) => {
if (error) {
console.error(error);
return;
}
console.log(stdout);
console.error(stderr);
});

The stdout is the output when success and stderr is that when an error occurs. I won’t use them in this tool.

Combining codes of running commands and codes of watching changes, we can see the whole block of codes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');

let blogWait = false;
fs.watch(path.resolve(__dirname, 'blog', 'source', '_posts'), (event, filename) => {
if (filename) {
if (blogWait) return;
blogWait = setTimeout(() => {
blogWait = false;
console.log(`${filename} file Changed`);
exec('hexo d -g', {
cwd: path.resolve(__dirname, 'blog'), // switch working directory
}, (err) => {
if(err) {
console.log('Cannot deploy Hexo');
} else {
console.log('Blog Deployed');
}
})
}, 200);
}
});

Function exec can take a second param, options. The deploy command needs to run under the root directory of your blog. So I add a cwd option to change the working directory. The reason why I do not simply place the program under the root directory of my blog is that I’m running a multi-language blog. And there are two directories to watch. If you are running a multi-language blog too, you may want to have some reference here.

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
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');

let blogWait = false;
fs.watch(path.resolve(__dirname, 'blog', 'source', '_posts'), (event, filename) => {
if (filename) {
if (blogWait) return;
blogWait = setTimeout(() => {
blogWait = false;
console.log(`${filename} file Changed`);
exec('hexo d -g', {
cwd: path.resolve(__dirname, 'blog'), // switch working directory
}, (err) => {
if(err) {
console.log('Cannot deploy Hexo');
} else {
console.log('Blog Deployed');
}
})
}, 200);
}
});

let zhBlogWait = false;
fs.watch(path.resolve(__dirname, 'zhblog', 'source', '_posts'), (event, filename) => {
if (filename) {
if (zhBlogWait) return;
zhBlogWait = setTimeout(() => {
zhBlogWait = false;
console.log(`${filename} file Changed`);
exec('hexo d -g', {
cwd: path.resolve(__dirname, 'zhblog'), // switch working directory
}, (err) => {
if(err) {
console.log('Cannot deploy Hexo');
} else {
console.log('ZhBlog Deployed');
}
})
}, 200);
}
});

Comments