Hexo Plugin Development

Lately, I’ve found myself a little envious of The Verge’s slick image carousel feature. Let’s be real—when you’ve got an organized set of images, a carousel beats a static picture group any day. Unfortunately, Hexo doesn’t have a built-in plugin for that. So, naturally, I took matters into my own hands and whipped up a carousel plugin based on Splide, designed specifically for the NexT theme. Here’s a rundown of the journey from development to release—because why not document the process while you're at it?

Version:

  • Hexo: 7.3.0
  • npm: 10.8.2

Create Plugin

  1. First things first—create a folder to house all your plugin files.
    • Make sure the folder name starts with hexo-, like hexo-splide-carousel, or Hexo simply won’t recognize it. No exceptions.
    • No need to keep this folder inside your blog directory either.
    • Pro tip: use git to manage your code. You can set up a repo with the same name on GitHub and clone it locally.
  2. Next, fire up your terminal and run npm init to initialize the project. Just follow the prompts, and you’ll end up with a shiny new package.json. This file holds all the key info about your plugin—like its name, version, dependencies, etc. If you’re not sure about some options or just don’t care, stick with the defaults. You can always come back and tweak them later.
    • Want the full breakdown of each part of package.json? Check out the official documentation.
      • Here’s an example to get you started:
        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
        {
        "name": "hexo-splide-carousel",
        "version": "1.2.1",
        "description": "A package for Hexo blogs using the Next theme, provides image carousel and zoom functionality using Splide and medium-zoom libraries.",
        "main": "index.js",
        "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
        },
        "keywords": [
        "hexo",
        "theme-next",
        "plugin",
        "splide",
        "carousel",
        "medium-zoom"
        ],
        "author": "Siriusq",
        "homepage": "https://github.com/Siriusq/hexo-splide-carousel",
        "bugs": "https://github.com/Siriusq/hexo-splide-carousel/issues",
        "repository": {
        "type": "git",
        "url": "git://github.com:Siriusq/hexo-splide-carousel.git"
        },
        "license": "MIT"
        }
    • If you don’t feel like typing everything out, just go with npm init -y—this command auto-generates a package.json with all the default settings.
  3. Once that’s done, you should see two files in your folder: index.js and package.json. The index.js file is the main entry point for your plugin. This is where the magic happens.

Development

This section might get a bit long since it’s all about the details of the plugin files I wrote. Take it with a grain of salt—I mean, this is my first time developing a Hexo plugin, so expect a few bumps along the way.

Before diving in, I’d recommend taking the time to read through the official documentation.

Now, my plugin falls under the “tag” category. Here’s the gist: during Hexo’s static file generation process, if it detects a specific tag in a markdown file (like {% splide %}), it will convert that tag and its content into the HTML structure we’ve defined.

If your plugin works differently, you can always check out these websites for similar plugins to use as inspiration:

File Structure

Here’s how my plugin is structured. It’s not the gold standard of organization (for example, I wasn’t entirely sure where to put splide-init.js since I couldn’t find any solid guidelines). So I went with what looked sensible based on other plugins that seemed to be structured well enough.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
hexo-splide-carousel
├─index.js
├─package.json
├─LICENSE
├─README.md
├─.npmignore
├─lib
│ ├─scripts
│ │ ├─splide-init.js
│ │ └─tags
│ │ └─splide-tag.js
│ └─assets
│ └─splide-custom.css
└─ReadmeAssets
├─CNREADME.md
└─preview.jpg

index.js

As mentioned earlier, this is the main entry point for the plugin—the brain behind the operation. In my plugin, it handles the following:

  • Registers custom tags
  • Imports necessary modules
  • Injects JS scripts and CSS styles into the page

Register Tags

Tag plugins help developers quickly insert content into posts. You can find more details in the Tag Plugin Documentation.

For my plugin, I registered two tags: splide and sc.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Register Hexo custom tag and its shorthand
const splideTag = require('./lib/scripts/tags/splide-tag')(hexo);
hexo.extend.tag.register(
"splide",
splideTag,
{ ends: true },
);

hexo.extend.tag.register(
"sc",
splideTag,
{ ends: true },
);
  • const splideTag = require('./lib/scripts/tags/splide-tag')(hexo);
    • require('./lib/scripts/tags/splide-tag'): This imports the tag function as a module, making it easier to update later on. The module lives at ./lib/scripts/tags/splide-tag.js.
    • (hexo): If your tag function relies on Hexo configurations or settings, you’ll need to pass the Hexo instance into the function.
  • hexo.extend.tag.register is used to register the tag.
    • The first argument is the tag name, which you’ll use in markdown files.
    • The second argument is the tag function. Since I imported it as a module, it keeps things clean.
      • If your tag function is pretty simple, you could just pass it directly as function (args, content) { // ... }.
    • The third argument contains tag options. Hexo offers two options, ends and async, both of which default to false.
      • { ends: true } means this tag will require a closing tag. In markdown files, you’d use an end tag to indicate where the tag finishes.

Load Files

First up, I load Hexo’s built-in file I/O module. For more info on how this works, check out hexo-fs.

1
2
const fs = require('fs');
const path = require('path');

Then, using the fs module, I load additional files:

1
2
3
// Load files from the lib directory
const splideInit = fs.readFileSync(path.resolve(__dirname, "./lib/scripts/splide-init.js"), { encoding: "utf8" });
const customStyle = fs.readFileSync(path.resolve(__dirname, "./lib/assets/splide-custom.css"), { encoding: "utf8" });

Custom Configuration

I’ve baked in a few customizable features into the plugin, like picking which CDN provider to use or adjusting the style of the carousel. To make these adjustments easier for users, you’ll need to add some config options into the Hexo config file.

Note: In the _config.yml file, make sure you’re using two-space indentation.

For example, here’s how you can customize the CDN provider:

1
2
3
# _config.yml
splide:
cdn: unpkg # Options: unpkg, cdnjs, jsdelivr

Then, in index.js, you can read this configuration:

1
const cdn = hexo.config.splide.cdn || 'unpkg';

To prevent users from leaving options blank, it’s a good idea to set a default. The || 'unpkg' part ensures that if the CDN isn’t specified in the config, it’ll default to unpkg.

Injectors

Injectors are used to insert static code snippets into the generated HTML’s <head> and <body>. Hexo performs the injection before running the after_render:html filter. For more details, check out the Injector Documentation.

Earlier, we loaded the JS and CSS files, but these need to be injected into the generated HTML to take effect. Here’s an example of injecting custom styles:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Previously loaded CSS styles
const customStyle = fs.readFileSync(path.resolve(__dirname, "./lib/assets/splide-custom.css"), { encoding: "utf8" });

// Inject styles into the HTML
hexo.extend.injector.register(
"head_begin",
() => {
return `
<style type="text/css">${customStyle}</style>
`;
},
"default"
);
  • The first parameter specifies where the code gets injected:
    • head_begin: Injects right after <head> (default)
    • head_end: Injects just before </head>
    • body_begin: Injects right after <body>
    • body_end: Injects just before </body>
  • The second parameter is the code snippet to be injected.
  • The third parameter defines the scope of the injection:
    • default: Injects on every page (this is the default)
    • home: Injects only on the homepage
    • post: Injects only on post pages
    • page: Injects only on standalone pages
    • archive: Injects only on archive pages
    • category: Injects only on category pages
    • tag: Injects only on tag pages

splide-tag.js

./lib/scripts/tags/splide-tag.js is the tag function I imported earlier as a module. Here’s the file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Generate HTML structure
module.exports = hexo => function (args, content) {
// Fetch global default settings from Hexo config
const globalSplideOptions = hexo.config.splide.options || {};

// Parse local settings from the tag, e.g., {% splide type:'loop' autoplay:true %}
const splideSettings = args.reduce((acc, arg) => {
const [key, value] = arg.split(':');
acc[key] = value;
return acc;
}, {});

// Each line of images in Markdown
const lines = content.trim().split('\n');

// Omitted content

// Generate final HTML; generate description part and id only if at least one image has a description
return `
<div class="carousel-container">
// Omitted content
</div>
`;
};

The tag function receives two parameters: args and content. In Markdown files, my tag structure looks like this:

1
2
3
{% splide type:'loop' autoplay:true %}
![alt](url)
{% endsc %}
  • args contains the parameters passed to the tag plugin, like type:'loop' autoplay:true.
  • content is the content covered by the tag plugin, i.e., ![alt](url).

Other Files

splide-init.js listens for page events and determines whether to initialize the carousel component.
splide-custom.css applies my custom styles, overriding some default styles of Splide.

Local Test

During development, we need to frequently test our plugin to ensure it works correctly.

  1. First, install our plugin in the blog project. There are several installation methods, including but not limited to:
    • (Recommended) Execute the command npm install <plugin-path> in the blog root directory. This command creates a link, meaning any changes made in the plugin project’s folder will be synced in real-time to blog-root/node_modules/plugin-name.
    • If you don’t plan to upload the plugin, you can also directly move the plugin folder to ./node_modules and manually add the plugin and its version to the dependencies section of ./package.json in the blog root directory, like "hexo-splide-carousel": "^1.0.0",. If this is the last line, remember to add a comma at the end of the previous line and remove its own comma.
  2. If your plugin has custom configuration options, remember to add them to the Hexo configuration file _config.yml.
  3. Then, proceed with the usual hexo cl and hexo s testing.

Publish

Once the plugin is complete, you can choose to upload it to npm so others can install and use it.

  1. Create a README file to describe the plugin’s purpose and usage. For more details, see the official documentation about package README files.
  2. If there are files to ignore, you can create a .npmignore file, which has a similar syntax to .gitignore. Common files like the .git folder and the README file are ignored by default, so you don’t need to add them to .npmignore. More information can be found in the official documentation on keeping files out of your package.
  3. Complete the package.json.
  4. Register for an account on the npm website.
  5. Execute npm login in the plugin directory, then press Enter to open a browser and log in.
  6. Execute npm publish, then press Enter to open a browser for identity verification.
  7. Search for your plugin name on the npm homepage to find the recently published plugin.

Update

If you discover new bugs or add new features to the plugin, you need to push the updated plugin to npm again.

  1. Execute npm version <version-number>.
    • The default version number after initialization is 1.0.0.
    • If the update primarily involves bug fixes, just increase the last digit, like 1.0.1.
    • If you added new features that are backward-compatible, change the middle digit, like 1.1.0.
    • If there are significant changes and new features that are not backward-compatible, change the first digit, like 2.0.0.
    • For detailed versioning rules, see the official documentation on semantic versioning.
  2. Execute npm publish, then press Enter to open a browser for identity verification, and wait for the command in the terminal to complete.

Submit to Hexo

After publishing the plugin, we can also submit it to the official Hexo plugin list, making it easier for more people to find and use.

  1. Fork the hexojs/site repository to your own account.
  2. Clone the forked repository to your local machine.
  3. Create a new yaml file in ./source/_data/plugins/, naming it after your plugin.
  4. In the newly created file, fill in the relevant information in English. Here’s an example:
    1
    2
    3
    4
    5
    6
    description: Server module for Hexo.
    link: https://github.com/hexojs/hexo-server
    tags:
    - official
    - server
    - console
  5. Push the changes to your repository.
  6. Open a new pull request.
  7. In the title, describe the changes made, such as Plugin: add plugin hexo-splide-carousel.
  8. Check the relevant options in the Check List.
  9. Submit and wait for Hexo to complete the relevant checks.
  10. Finally, your plugin will appear in the Hexo official plugin list.

References

  1. hexo-插件开发规范@一人の境
  2. Hexo 插件开发步骤
  3. Hexo API