├── .github
└── workflows
│ ├── codeql.yml
│ └── linter.yml
├── .gitignore
├── LICENSE
├── README.md
├── eslint.config.js
├── index.js
├── lib
├── css.js
├── filter.js
├── html.js
├── index.js
├── js.js
└── util.js
├── package.json
├── renovate.json
└── test
├── deploy.sh
└── index.js
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 | schedule:
9 | - cron: "58 14 * * 4"
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ javascript ]
24 |
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v4
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v3
31 | with:
32 | languages: ${{ matrix.language }}
33 | queries: +security-and-quality
34 |
35 | - name: Autobuild
36 | uses: github/codeql-action/autobuild@v3
37 |
38 | - name: Perform CodeQL Analysis
39 | uses: github/codeql-action/analyze@v3
40 | with:
41 | category: "/language:${{ matrix.language }}"
42 |
--------------------------------------------------------------------------------
/.github/workflows/linter.yml:
--------------------------------------------------------------------------------
1 | name: Linter
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | run:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Use Node.js
13 | uses: actions/setup-node@v4
14 | - run: npm install
15 | - run: npm run lint
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
3 | .DS_Store
4 | tmp/
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 NexT [Reloaded]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hexo-optimize
2 |
3 | [![Build Status][github-image]][github-url]
4 | [![npm-image]][npm-url]
5 | [![lic-image]](LICENSE)
6 |
7 | A hexo plugin that optimize the pages loading speed. It will automatically filter your blog files and make optimizations. For example:
8 |
9 | - Minify CSS, JS and HTML files
10 | - Inline specific CSS files (like `main.css`) directly into the html page to improve First Contentful Paint performance
11 | - Add content hash to static resource filenames (versioning) for better cache control
12 |
13 | It will help you get a higher score in the [Google PageSpeed Insights](https://pagespeed.web.dev).
14 |
15 | ## Installation
16 |
17 | ![size-image]
18 | [![dm-image]][npm-url]
19 | [![dt-image]][npm-url]
20 |
21 | ```bash
22 | npm install hexo-optimize
23 | ```
24 |
25 | ## Usage
26 |
27 | Activate the plugin in hexo's `_config.yml` like this:
28 | ```yml
29 | filter_optimize:
30 | enable: true
31 | # static resource versioning
32 | versioning: false
33 | css:
34 | # minify all css files
35 | minify: true
36 | excludes:
37 | # use preload to load css elements dynamically
38 | delivery:
39 | - '@fortawesome/fontawesome-free'
40 | - 'fonts.googleapis.com'
41 | # make specific css content inline into the html page
42 | inlines:
43 | # support full path only
44 | - css/main.css
45 | js:
46 | # minify all js files
47 | minify: true
48 | excludes:
49 | # remove the comments in each of the js files
50 | remove_comments: false
51 | html:
52 | # minify all html files
53 | minify: true
54 | excludes:
55 | # set the priority of this plugin,
56 | # lower means it will be executed first, default of Hexo is 10
57 | priority: 12
58 | ```
59 |
60 | This plugin can be disabled by `NODE_ENV` in development to boost `hexo generate`:
61 | ```
62 | export NODE_ENV=development
63 | ```
64 |
65 | ## Comparison
66 |
67 | Here is a result from [GTmetrix](https://gtmetrix.com) to show you the changes between before and after. (Same web server located in Tokyo, Japan, vultr.com)
68 |
69 | * **Remove query strings from static resources** - let all the proxies could cache resources well. (https://gtmetrix.com/remove-query-strings-from-static-resources.html)
70 | * **Make fewer HTTP requests** - through combined the loaded js files into the one.
71 | * **Prefer asynchronous resources** - change the css delivery method using a script block instead of link tag.
72 | * And TODOs ...
73 |
74 | 
75 |
76 | [github-image]: https://img.shields.io/github/actions/workflow/status/next-theme/hexo-optimize/linter.yml?branch=main&style=flat-square
77 | [npm-image]: https://img.shields.io/npm/v/hexo-optimize.svg?style=flat-square
78 | [lic-image]: https://img.shields.io/npm/l/hexo-optimize?style=flat-square
79 |
80 | [size-image]: https://img.shields.io/github/languages/code-size/next-theme/hexo-optimize?style=flat-square
81 | [dm-image]: https://img.shields.io/npm/dm/hexo-optimize?style=flat-square
82 | [dt-image]: https://img.shields.io/npm/dt/hexo-optimize?style=flat-square
83 |
84 | [github-url]: https://github.com/next-theme/hexo-optimize/actions?query=workflow%3ALinter
85 | [npm-url]: https://www.npmjs.com/package/hexo-optimize
86 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | const config = require("@next-theme/eslint-config");
2 |
3 | module.exports = config;
4 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /* global hexo */
2 |
3 | 'use strict';
4 |
5 | const { deepMerge } = require('hexo-util');
6 |
7 | hexo.config.filter_optimize = deepMerge({
8 | enable : true,
9 | versioning: false,
10 | css : {
11 | minify : true,
12 | excludes: [],
13 | delivery: [],
14 | inlines : []
15 | },
16 | js: {
17 | minify : true,
18 | excludes : [],
19 | remove_comments: false
20 | },
21 | html: {
22 | minify : true,
23 | excludes: []
24 | },
25 | image: {
26 | minify : true,
27 | interlaced : false,
28 | multipass : false,
29 | optimizationLevel: 2,
30 | pngquant : false,
31 | progressive : false
32 | }
33 | }, hexo.config.filter_optimize);
34 |
35 | const config = hexo.config.filter_optimize;
36 | if (process.env.NODE_ENV !== 'development' && config.enable) {
37 | const { filter, css, js } = require('./lib/index');
38 | const priority = parseInt(config.priority, 10) || 10;
39 |
40 | // Enable one of the optimizations.
41 | if (config.css.delivery.length || config.css.inlines.length || config.html.minify || config.versioning) {
42 | hexo.extend.filter.register('after_generate', filter, priority);
43 | }
44 | if (config.css.minify) {
45 | hexo.extend.filter.register('after_render:css', css);
46 | }
47 | if (config.js.minify) {
48 | hexo.extend.filter.register('after_render:js', js);
49 | }
50 | if (config.image.minify) {
51 | //hexo.extend.filter.register('after_generate', image);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/css.js:
--------------------------------------------------------------------------------
1 | const css = require('lightningcss');
2 | const { inExcludes } = require('./util');
3 |
4 | module.exports = function(str, data) {
5 | const { excludes } = this.config.filter_optimize.css;
6 | if (inExcludes(data.path, excludes)) return str;
7 | return css.transform({
8 | code : Buffer.from(str),
9 | minify: true
10 | }).code.toString();
11 | };
12 |
--------------------------------------------------------------------------------
/lib/filter.js:
--------------------------------------------------------------------------------
1 | const Promise = require('bluebird');
2 | const micromatch = require('micromatch');
3 | const { URL } = require('url');
4 | const { join, parse } = require('path');
5 | const { streamRead, hash } = require('./util');
6 | const html = require('./html');
7 |
8 | function minify(str, path, config, { cssFiles, jsMap, cssMap }) {
9 | const { url, root, filter_optimize } = config;
10 | const { inlines, delivery } = filter_optimize.css;
11 | str = str.replace(/]*\shref="([^"]+)"[^>]*>/ig, (match, href) => {
12 | // Exit if the href attribute doesn't exist.
13 | if (!href) return match;
14 |
15 | const link = new URL(href, url);
16 | const originalURL = link.href;
17 | for (const path of inlines) {
18 | if (link.pathname === join(root, path)) {
19 | return ``;
20 | }
21 | }
22 | for (const path in cssMap) {
23 | if (link.pathname === join(root, path)) {
24 | link.pathname = join(root, cssMap[path]);
25 | continue;
26 | }
27 | }
28 | const newURL = link.hostname === new URL(url).hostname ? link.pathname : link.href;
29 | if (delivery.some(pattern => originalURL.includes(pattern))) {
30 | return ``;
31 | }
32 | return match.replace(href, newURL);
33 | }).replace(/