@chriswiegman/hugo-tools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE.txt +7 -0
- package/README.md +231 -0
- package/package.json +49 -0
- package/src/add-tags.js +143 -0
- package/src/book.mjs +725 -0
- package/src/book.test.mjs +834 -0
- package/src/config-init.js +29 -0
- package/src/config.js +41 -0
- package/src/draft.js +77 -0
- package/src/draft.test.mjs +77 -0
- package/src/extract-tags.js +234 -0
- package/src/extract-tags.test.mjs +121 -0
- package/src/pick-image.js +80 -0
- package/src/publish.js +428 -0
- package/src/publish.test.mjs +496 -0
- package/src/vscode.js +101 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
All notable changes to this project will be documented in this file.
|
|
3
|
+
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|
6
|
+
and is generated by [Changie](https://github.com/miniscruff/changie).
|
|
7
|
+
|
|
8
|
+
## 0.1.0 - 2026-05-04
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* Initial test release
|
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Chris Wiegman
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# hugo-tools
|
|
2
|
+
|
|
3
|
+
A collection of CLI tools for Hugo bloggers. Run them directly from the root of your Hugo site via `npx` or install globally.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 18 or later
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
**Run without installing (recommended):**
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx -p @chriswiegman/hugo-tools <command> [args]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
For example:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx -p @chriswiegman/hugo-tools draft
|
|
21
|
+
npx -p @chriswiegman/hugo-tools publish content/drafts/my-post.md now
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Install locally in your Hugo project:**
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install --save-dev @chriswiegman/hugo-tools
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then run commands via `npx` (no `-p` flag needed once installed) or add them as scripts in your `package.json`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"scripts": {
|
|
35
|
+
"publish": "publish",
|
|
36
|
+
"extract-tags": "extract-tags"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Or install globally and run from any Hugo site root:**
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm install -g @chriswiegman/hugo-tools
|
|
45
|
+
publish content/drafts/my-post.md now
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
All commands must be run from the **root of your Hugo site**.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
Run `config` from your Hugo site root to generate the file with all defaults:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
config
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or create `.hugo-tools.json` manually:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"timezone": "America/Chicago",
|
|
65
|
+
"postsDir": "content/posts",
|
|
66
|
+
"draftsDir": "content/drafts",
|
|
67
|
+
"notesDir": "content/notes",
|
|
68
|
+
"booksDir": "content/books",
|
|
69
|
+
"imagesDir": "assets/images",
|
|
70
|
+
"vscodeDir": ".vscode"
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
All fields are optional. Shown values are the defaults.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Commands
|
|
79
|
+
|
|
80
|
+
### `config`
|
|
81
|
+
|
|
82
|
+
Generates a `.hugo-tools.json` config file in the current directory with all default values.
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
config
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Run this once after installing to get a config file you can edit. Exits with an error if `.hugo-tools.json` already exists.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### `vscode`
|
|
93
|
+
|
|
94
|
+
Scaffolds VS Code configuration for your Hugo site in your `vscodeDir` (default `.vscode/`).
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
vscode
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Creates three files if they don't already exist — existing files are left untouched:
|
|
101
|
+
|
|
102
|
+
| File | Purpose |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `tasks.json` | Tasks for `draft`, `add-tags`, `publish` (now & later), and `pick-image` |
|
|
105
|
+
| `extensions.json` | Recommends the markdown word-count and spell-checker extensions |
|
|
106
|
+
| `settings.json` | Markdown editor settings (snippet suggestions, word-based suggestions off) |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### `draft`
|
|
111
|
+
|
|
112
|
+
Creates a new draft file in your `draftsDir` and opens it in VS Code.
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
draft
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Generates a timestamp-based filename (`YYYYMMDD-HHMMSS.md`) using your configured timezone, writes the standard draft front matter with a blank title and placeholder lists, and prints the path. No `date:` line is written — fill in the title before publishing.
|
|
119
|
+
|
|
120
|
+
**Generated front matter:**
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
---
|
|
124
|
+
title: ""
|
|
125
|
+
description: ""
|
|
126
|
+
draft: true
|
|
127
|
+
images:
|
|
128
|
+
-
|
|
129
|
+
categories:
|
|
130
|
+
-
|
|
131
|
+
tags:
|
|
132
|
+
-
|
|
133
|
+
---
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### `publish`
|
|
139
|
+
|
|
140
|
+
Publishes a Hugo draft post by moving it from your drafts directory to the appropriate content directory and updating its front matter.
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
publish <draft-path> [now|later] [YYYY-MM-DD]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Examples:**
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
# Publish immediately
|
|
150
|
+
publish content/drafts/my-post.md now
|
|
151
|
+
|
|
152
|
+
# Schedule for a future date (prompts if date omitted)
|
|
153
|
+
publish content/drafts/my-post.md later 2026-06-01
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**What it does:**
|
|
157
|
+
|
|
158
|
+
- Sets `draft: false` and `date:` to now or 08:00 on the scheduled date (in your configured timezone)
|
|
159
|
+
- Moves the file to `content/posts/YYYY/MM-DD-slug.md`
|
|
160
|
+
- Short posts (under 200 words) without categories are treated as notes and moved to `content/notes/` instead
|
|
161
|
+
- Warns if the draft already had a different `date:` value
|
|
162
|
+
|
|
163
|
+
**Requirements for posts** (not notes):
|
|
164
|
+
|
|
165
|
+
- A `title:` in front matter
|
|
166
|
+
- At least one tag
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### `extract-tags`
|
|
171
|
+
|
|
172
|
+
Scans all posts in your `postsDir` and writes two files to your `vscodeDir`:
|
|
173
|
+
|
|
174
|
+
- `tags-categories.json` — sorted list of all tags and categories with usage counts
|
|
175
|
+
- `markdown.code-snippets` — VSCode snippet definitions for autocomplete
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
extract-tags
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Run this periodically to keep your tag/category list fresh. The snippet abbreviation map in [src/extract-tags.js](src/extract-tags.js) is pre-populated with some common shorthands — edit it to match your own taxonomy.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### `add-tags`
|
|
186
|
+
|
|
187
|
+
Interactive prompt to select categories and tags from your existing taxonomy, then prints the formatted front matter block to paste into a draft.
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
add-tags
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Requires `extract-tags` to have been run at least once.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### `pick-image`
|
|
198
|
+
|
|
199
|
+
Lists images in your `imagesDir` for the current year/month, lets you pick one, and copies the Hugo-relative path to the clipboard.
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
pick-image
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Clipboard support: macOS (`pbcopy`), Windows (`clip`), Linux (`xclip`, `xsel`, or `wl-copy` — whichever is available).
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### `import-books`
|
|
210
|
+
|
|
211
|
+
Imports books from a [Goodreads CSV export](https://www.goodreads.com/review/import) into Hugo content files under `booksDir`.
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
import-books path/to/goodreads_library_export.csv
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**What it does:**
|
|
218
|
+
|
|
219
|
+
- Creates `content/books/<author-slug>/<title-slug>.md` for each book on your "read" shelf
|
|
220
|
+
- Front matter includes title, author, star rating, finish date(s), and links to Amazon, Open Library, and Goodreads
|
|
221
|
+
- Any Goodreads review text becomes the file body
|
|
222
|
+
- Safe to re-run: existing files are matched by Goodreads ID or ISBN, finish dates are merged, and unchanged files are skipped
|
|
223
|
+
- If a book's title changed in Goodreads, the file is renamed automatically
|
|
224
|
+
|
|
225
|
+
**To export from Goodreads:** Account → My Books → Import and Export → Export Library.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Contributing
|
|
230
|
+
|
|
231
|
+
Issues and PRs welcome at <https://github.com/ChrisWiegman/hugo-tools>.
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chriswiegman/hugo-tools",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A set of tools to make blogging easier with Hugo.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"hugo",
|
|
7
|
+
"blogging",
|
|
8
|
+
"drafts",
|
|
9
|
+
"publishing"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/ChrisWiegman/hugo-tools#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ChrisWiegman/hugo-tools/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ChrisWiegman/hugo-tools.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Chris Wiegman <contact@chriswiegman.com>",
|
|
21
|
+
"type": "commonjs",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src/",
|
|
27
|
+
"CHANGELOG.md"
|
|
28
|
+
],
|
|
29
|
+
"bin": {
|
|
30
|
+
"config": "src/config-init.js",
|
|
31
|
+
"vscode": "src/vscode.js",
|
|
32
|
+
"extract-tags": "src/extract-tags.js",
|
|
33
|
+
"add-tags": "src/add-tags.js",
|
|
34
|
+
"draft": "src/draft.js",
|
|
35
|
+
"pick-image": "src/pick-image.js",
|
|
36
|
+
"publish": "src/publish.js",
|
|
37
|
+
"import-books": "src/book.mjs"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"lint": "eslint src/",
|
|
41
|
+
"test": "node --test src/book.test.mjs src/publish.test.mjs src/extract-tags.test.mjs src/draft.test.mjs"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@eslint/js": "^10.0.1",
|
|
45
|
+
"changie": "^1.24.0",
|
|
46
|
+
"eslint": "^10.3.0",
|
|
47
|
+
"globals": "^17.6.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/add-tags.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interactive script to help add categories and tags to Hugo draft frontmatter.
|
|
5
|
+
* Reads from the extracted tags-categories.json file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const readline = require('readline');
|
|
11
|
+
const config = require('./config');
|
|
12
|
+
|
|
13
|
+
const DATA_FILE = path.join(process.cwd(), config.vscodeDir, 'tags-categories.json');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Display menu and get user selection
|
|
17
|
+
*/
|
|
18
|
+
async function getSelection(prompt, options) {
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
console.log(`\n${prompt}`);
|
|
26
|
+
options.forEach((opt, idx) => {
|
|
27
|
+
console.log(` ${idx + 1}. ${opt}`);
|
|
28
|
+
});
|
|
29
|
+
console.log(` 0. Done/Skip`);
|
|
30
|
+
|
|
31
|
+
rl.question('\nEnter number: ', (answer) => {
|
|
32
|
+
rl.close();
|
|
33
|
+
const num = parseInt(answer, 10);
|
|
34
|
+
if (num === 0) {
|
|
35
|
+
resolve(null);
|
|
36
|
+
} else if (num > 0 && num <= options.length) {
|
|
37
|
+
resolve(options[num - 1]);
|
|
38
|
+
} else {
|
|
39
|
+
console.log('Invalid selection');
|
|
40
|
+
resolve(null);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get multiple selections
|
|
48
|
+
*/
|
|
49
|
+
async function getMultipleSelections(prompt, options) {
|
|
50
|
+
const selections = [];
|
|
51
|
+
let done = false;
|
|
52
|
+
|
|
53
|
+
while (!done) {
|
|
54
|
+
const remaining = options.filter(opt => !selections.includes(opt));
|
|
55
|
+
if (remaining.length === 0) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const selection = await getSelection(
|
|
60
|
+
selections.length === 0 ? prompt : `${prompt} (Selected: ${selections.join(', ')})`,
|
|
61
|
+
remaining
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (selection === null) {
|
|
65
|
+
done = true;
|
|
66
|
+
} else {
|
|
67
|
+
selections.push(selection);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return selections;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Format frontmatter list
|
|
76
|
+
*/
|
|
77
|
+
function formatList(items) {
|
|
78
|
+
if (items.length === 0) {
|
|
79
|
+
return ' -';
|
|
80
|
+
}
|
|
81
|
+
return items.map(item => ` - ${item}`).join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Main execution
|
|
86
|
+
*/
|
|
87
|
+
async function main() {
|
|
88
|
+
config.requireHugoSite();
|
|
89
|
+
|
|
90
|
+
console.log('=== Hugo Draft Tags & Categories Helper ===\n');
|
|
91
|
+
|
|
92
|
+
// Check if data file exists
|
|
93
|
+
if (!fs.existsSync(DATA_FILE)) {
|
|
94
|
+
console.error(`Error: Data file not found at ${DATA_FILE}`);
|
|
95
|
+
console.error('Run "extract-tags" first to generate the data file.');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Load data
|
|
100
|
+
const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
|
|
101
|
+
|
|
102
|
+
console.log(`Loaded ${data.categories.length} categories and ${data.tags.length} tags`);
|
|
103
|
+
console.log(`(Generated from ${data.stats.totalFiles} posts on ${new Date(data.stats.generatedAt).toLocaleDateString()})\n`);
|
|
104
|
+
|
|
105
|
+
// Get category selection
|
|
106
|
+
const category = await getSelection(
|
|
107
|
+
'Select a category:',
|
|
108
|
+
data.categories
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Get tag selections
|
|
112
|
+
const tags = await getMultipleSelections(
|
|
113
|
+
'Select tags (you can select multiple):',
|
|
114
|
+
data.tags
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Generate output
|
|
118
|
+
console.log('\n=== Generated Frontmatter ===\n');
|
|
119
|
+
|
|
120
|
+
if (category) {
|
|
121
|
+
console.log('categories:');
|
|
122
|
+
console.log(` - ${category}`);
|
|
123
|
+
} else {
|
|
124
|
+
console.log('categories:');
|
|
125
|
+
console.log(' -');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log('tags:');
|
|
129
|
+
console.log(formatList(tags));
|
|
130
|
+
|
|
131
|
+
console.log('\n=== Copy the above into your draft frontmatter ===\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Handle Ctrl+C gracefully
|
|
135
|
+
process.on('SIGINT', () => {
|
|
136
|
+
console.log('\n\nCancelled.');
|
|
137
|
+
process.exit(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
main().catch(err => {
|
|
141
|
+
console.error('Error:', err);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
});
|