vowel 0.7.1-beta.0 → 0.7.3
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/LICENSE +21 -0
- package/README.md +41 -239
- package/bin.js +91 -31
- package/cli.js +32 -0
- package/config.js +12 -0
- package/extractDate.js +1 -1
- package/package.json +8 -34
- package/pathDate.js +69 -0
- package/plugins/assets/index.js +1 -1
- package/plugins/atproto/body.js +3 -1
- package/plugins/atproto/index.js +5 -45
- package/plugins/atproto/publish.js +42 -11
- package/plugins/atproto/record.js +47 -4
- package/plugins/deploy-cloudflare/index.js +4 -1
- package/plugins/export/index.js +131 -0
- package/plugins/html/bundle/index.js +101 -44
- package/plugins/html/editor/Editor.svelte +9 -5
- package/plugins/html/editor/create-session.js +3 -1
- package/plugins/html/editor/directives.js +12 -0
- package/plugins/html/editor/frontmatter.js +77 -12
- package/plugins/html/editor/ingest.js +29 -31
- package/plugins/html/editor/main.js +5 -1
- package/plugins/html/editor/serialize.js +8 -4
- package/plugins/html/editor/socket.js +34 -4
- package/plugins/html/image.js +88 -27
- package/plugins/html/index.js +237 -109
- package/plugins/html/partials.js +41 -17
- package/plugins/html/socialLinks.js +13 -11
- package/plugins/html/writeRules.js +234 -42
- package/plugins/images/favicon.png +0 -0
- package/plugins/images/index.js +69 -8
- package/plugins/markdown/colorScheme.js +70 -14
- package/plugins/markdown/frontmatter.js +18 -8
- package/plugins/markdown/index.js +94 -25
- package/plugins/markdown/metadata.js +132 -32
- package/plugins/markdown/typography.js +11 -11
- package/plugins/styles/index.js +4 -4
- package/plugins/xml/index.js +17 -10
- package/stylesheets/DefaultStyles.css +163 -108
- package/stylesheets/TypographyStyles.css +148 -81
- package/utils.js +74 -21
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sam Littlefair
|
|
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.
|
package/README.md
CHANGED
|
@@ -2,245 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
*Markdown websites in milliseconds*
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## Roadmap
|
|
11
|
-
|
|
12
|
-
### Priorities
|
|
13
|
-
|
|
14
|
-
- [ ] Create CONTRIBUTING.md
|
|
15
|
-
- [ ]
|
|
16
|
-
|
|
17
|
-
### Launch checklist
|
|
18
|
-
|
|
19
|
-
- [x] CLI
|
|
20
|
-
- [x] Docs
|
|
21
|
-
- [x] Classes
|
|
22
|
-
- [x] Route patterns
|
|
23
|
-
- [x] Post list metadata
|
|
24
|
-
- [.] Transaction management
|
|
25
|
-
- [ ] Default 404 pages
|
|
26
|
-
|
|
27
|
-
- [.] Fixes
|
|
28
|
-
- [x] Global nav sublinks
|
|
29
|
-
- [x] Hot reload
|
|
30
|
-
- [x] Explicit asset handling
|
|
31
|
-
- [.] Change "job" to "resources"
|
|
32
|
-
- [.] ReadResource types
|
|
33
|
-
- [x] Processor utilities param
|
|
34
|
-
- [.] Delete old SQLite logic
|
|
35
|
-
- [.] Make metadata explicit in SQLiteTarget type
|
|
36
|
-
- [x] Add SQLite begin/commit logic
|
|
37
|
-
- [.] Remove "loadDatabase"
|
|
38
|
-
- [.] Check all types
|
|
39
|
-
- [.] File handling
|
|
40
|
-
- [x] CSS prefixing
|
|
41
|
-
- [x] Homepage in global nav
|
|
42
|
-
- [x] Metadata in header
|
|
43
|
-
- [x] Page list sort
|
|
44
|
-
- [x] Limit in sort
|
|
45
|
-
- [x] Metafiles
|
|
46
|
-
- [x] Robots.txt
|
|
47
|
-
- [x] Sitemap.xml
|
|
48
|
-
- [x] RSS
|
|
49
|
-
- [x] 404.html (just make 404.md)
|
|
50
|
-
- [ ] Markup
|
|
51
|
-
- [x] Image as figure
|
|
52
|
-
- [x] Admonitions
|
|
53
|
-
- [x] Heading anchors
|
|
54
|
-
- [ ] GFM emoji
|
|
55
|
-
- [ ] Mermaid
|
|
56
|
-
- [x] Slogan
|
|
57
|
-
- [x] TOC
|
|
58
|
-
- [x] ==mark==
|
|
59
|
-
- [ ] Non-urgent fixes / improvements
|
|
60
|
-
- [ ] Compare metadata on file update
|
|
61
|
-
- [x] Prune/cleanup step
|
|
62
|
-
- [x] Dependency type (file, settings, folder)
|
|
63
|
-
- [ ] New systems
|
|
64
|
-
- [x] Tags
|
|
65
|
-
- [ ] External links
|
|
66
|
-
- [ ] Syntax highlighting
|
|
67
|
-
- [ ] Social links
|
|
68
|
-
|
|
69
|
-
### High priority
|
|
70
|
-
|
|
71
|
-
- [x] Global nav
|
|
72
|
-
- [x] Breadcrumbs
|
|
73
|
-
- [x] Page classes
|
|
74
|
-
- [ ] GetMany "path exists" filter
|
|
75
|
-
- [ ] GetMany "extension" filter
|
|
76
|
-
- [ ] Delete settings logic
|
|
77
|
-
- [ ] Update settings logic
|
|
78
|
-
- [x] Logical operators for filters
|
|
79
|
-
- [ ] Customize menus
|
|
80
|
-
- [ ] More file handling plugins
|
|
81
|
-
- [ ] Fonts
|
|
82
|
-
- [x] .woff
|
|
83
|
-
- [x] .woff2
|
|
84
|
-
- [x] .ttf
|
|
85
|
-
- [x] .otf
|
|
86
|
-
- [ ] Handling
|
|
87
|
-
- [ ] Images
|
|
88
|
-
- [x] .png
|
|
89
|
-
- [x] .jpg
|
|
90
|
-
- [x] .jpeg
|
|
91
|
-
- [x] .webp
|
|
92
|
-
- [x] .gif
|
|
93
|
-
- [ ] Formatting
|
|
94
|
-
- [ ] Icons
|
|
95
|
-
- [x] .ico
|
|
96
|
-
- [ ] Vectors
|
|
97
|
-
- [x] .svg
|
|
98
|
-
- [ ] Styles
|
|
99
|
-
- [x] .css
|
|
100
|
-
- [x] Bundling
|
|
101
|
-
- [x] PDFs
|
|
102
|
-
- [x] Videos
|
|
103
|
-
- [x] Arbitrary data
|
|
104
|
-
- [x] Add homepage to global nav
|
|
105
|
-
- [x] Page lists
|
|
106
|
-
- [x] Dates
|
|
107
|
-
- [x] robots.txt
|
|
108
|
-
- [x] sitemap.xml
|
|
109
|
-
- [x] 404.html
|
|
110
|
-
- [ ] Better URL normalization (see notes)
|
|
111
|
-
- [x] Name casing
|
|
112
|
-
- [x] TUI
|
|
113
|
-
- [x] Create settings.md
|
|
114
|
-
- [x] Site title
|
|
115
|
-
- [ ] Webmentions
|
|
116
|
-
- [x] Logo
|
|
117
|
-
- [x] Wordmark
|
|
118
|
-
- [ ] Identity (rel=me)
|
|
119
|
-
- [x] Filename breadcrumbs
|
|
120
|
-
- [x] RSS
|
|
121
|
-
- [x] Sitemap
|
|
122
|
-
- [x] Create home.md
|
|
123
|
-
- [ ] Create folder settings files
|
|
124
|
-
- [ ] Title
|
|
125
|
-
- [ ] Breadcrumb
|
|
126
|
-
- [ ] Remove index file rules
|
|
127
|
-
- [x] Custom CSS
|
|
128
|
-
- [x] styles.css
|
|
129
|
-
- [ ] Any CSS file
|
|
130
|
-
- [x] Tags
|
|
131
|
-
- [ ] Date format settings
|
|
132
|
-
- [ ] ::mark::
|
|
133
|
-
- [x] Infer images
|
|
134
|
-
- [ ] Favicon
|
|
135
|
-
- [-] Webmentions
|
|
136
|
-
- [x] HTML boilerplate
|
|
137
|
-
- [ ] View transitions
|
|
138
|
-
- [x] Logo
|
|
139
|
-
- [x] Wordmark
|
|
140
|
-
- [ ] Sort nav items
|
|
141
|
-
- [ ] Canonical URL
|
|
142
|
-
- [ ] Handle external links
|
|
143
|
-
- [x] Admonitions
|
|
144
|
-
- [ ] Use hgroup for site title, page title, etc
|
|
145
|
-
- [x] Images as `<figure>`
|
|
146
|
-
- [ ] Hidden routes
|
|
147
|
-
- [ ] Frontmatter settings
|
|
148
|
-
- [ ] HTML
|
|
149
|
-
- [ ] RSS
|
|
150
|
-
- [ ] Sitemap
|
|
151
|
-
- [x] Heading anchors
|
|
152
|
-
- [ ] Taxonomy pages and smart frontmatter
|
|
153
|
-
- [x] CSS cache busting
|
|
154
|
-
- [x] Slogan in homepage title
|
|
155
|
-
- [ ] GFM emojis (:smile:)
|
|
156
|
-
|
|
157
|
-
### Medium priority
|
|
158
|
-
|
|
159
|
-
- [ ] Tests
|
|
160
|
-
- [ ] Internal backlinks
|
|
161
|
-
- [ ] Break code into multiple files
|
|
162
|
-
- [ ] Image optimization (unpic)
|
|
163
|
-
- [ ] [SVG by mask](https://pqina.nl/blog/set-svg-background-image-fill-color/) and [CSS icons](https://antfu.me/posts/icons-in-pure-css)
|
|
164
|
-
- [ ] WYSIWYG editor
|
|
165
|
-
- [x] Better signals
|
|
166
|
-
- [x] File-written callback
|
|
167
|
-
- [ ] Themes
|
|
168
|
-
- [ ] Deploy
|
|
169
|
-
- [ ] Cloudflare pages
|
|
170
|
-
- [ ] GitHub pages
|
|
171
|
-
- [-] Post-publish work (ping webmentions)
|
|
172
|
-
- [ ] [Desktop app](https://blackboard.sh/electrobun/docs/)
|
|
173
|
-
- [ ] Mermaid
|
|
174
|
-
- [ ] Codeblock syntax highlighting
|
|
175
|
-
- [ ] Extraction utilities (regex in archive)
|
|
176
|
-
- [ ] Smarter frontmatter
|
|
177
|
-
- [ ] Object dl
|
|
178
|
-
- [ ] Array ul
|
|
179
|
-
- [ ] Image
|
|
180
|
-
- [ ] URL
|
|
181
|
-
- [ ] Date
|
|
182
|
-
- [x] TOC
|
|
183
|
-
- [ ] Versioning/publishing script
|
|
184
|
-
- [ ] Verify all element types from Obsidian
|
|
185
|
-
- [ ] Order/position/kanban
|
|
186
|
-
- [ ] Next/prev links
|
|
187
|
-
- [ ] Banner
|
|
188
|
-
- [ ] CTA
|
|
189
|
-
- [ ] Menu toggle
|
|
190
|
-
- [ ] Icons sets
|
|
191
|
-
- [ ] Auto-icons (:fa-rocket:)
|
|
192
|
-
- [ ] Frontmatter icons
|
|
193
|
-
|
|
194
|
-
### Low priority
|
|
195
|
-
|
|
196
|
-
- [ ] Footnotes
|
|
197
|
-
- [ ] Frontmatter taxonomies
|
|
198
|
-
- [ ] Recursive frontmatter
|
|
199
|
-
- [ ] Browser search
|
|
200
|
-
- [ ] Pagination
|
|
201
|
-
- [ ] Dialogue/spoiler alert
|
|
202
|
-
- [ ] Copy code button
|
|
203
|
-
- [ ] Tabbed element
|
|
204
|
-
- [ ] Multi-lingual
|
|
205
|
-
- [ ] Routing rules (see notes)
|
|
206
|
-
- [ ] ATProto
|
|
207
|
-
- [ ] Liking
|
|
208
|
-
- [ ] Commenting
|
|
209
|
-
- [ ] More inferred properties
|
|
210
|
-
- [ ] Email
|
|
211
|
-
- [ ] Phone number
|
|
212
|
-
- [ ] Location
|
|
213
|
-
- [ ] Color
|
|
214
|
-
- [ ] Price
|
|
215
|
-
- [ ] ISBN
|
|
216
|
-
- Contact form
|
|
217
|
-
- Subscription form
|
|
218
|
-
- Faceted search
|
|
219
|
-
|
|
220
|
-
## Notes and questions
|
|
221
|
-
|
|
222
|
-
rel=author? rel=bookmark? rel=external?
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
Annotations (see notes)
|
|
226
|
-
|
|
227
|
-
Interactivity (see notes)
|
|
228
|
-
|
|
229
|
-
Calculated properties?
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
OG tags to consider:
|
|
5
|
+
Vowel turns a folder of markdown files into a website. Write pages as
|
|
6
|
+
plain `.md` files, in Obsidian or any editor; Vowel works out titles,
|
|
7
|
+
dates, breadcrumbs, menus, tags, a feed and a sitemap from the files
|
|
8
|
+
and how they're arranged, so there's almost nothing to configure.
|
|
233
9
|
|
|
10
|
+
**Documentation: [vowel.cc](https://vowel.cc)**
|
|
11
|
+
|
|
12
|
+
## Start
|
|
13
|
+
|
|
14
|
+
Vowel needs [Node.js](https://nodejs.org) 26 or later. In the folder
|
|
15
|
+
that holds your markdown:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx vowel@latest
|
|
234
19
|
```
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
*/
|
|
20
|
+
|
|
21
|
+
The first time, a short setup offers to write a `settings.md` and a
|
|
22
|
+
`home.md`. Vowel then builds the site and serves a preview at
|
|
23
|
+
`http://localhost:8000`, updating it as you edit.
|
|
24
|
+
|
|
25
|
+
## Publish
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
vowel export ../my-site # build into a folder, to upload anywhere
|
|
29
|
+
vowel --command deploy # build and deploy to Cloudflare Pages
|
|
246
30
|
```
|
|
31
|
+
|
|
32
|
+
See [the docs](https://vowel.cc) for deploying, AT Protocol publishing,
|
|
33
|
+
themes, and every setting.
|
|
34
|
+
|
|
35
|
+
## How it's built
|
|
36
|
+
|
|
37
|
+
Vowel is a set of plugins for [Votive](https://github.com/samlfair/votive),
|
|
38
|
+
a file processor that turns sources into targets and rebuilds only what
|
|
39
|
+
an edit changed.
|
|
40
|
+
|
|
41
|
+
## Contributing
|
|
42
|
+
|
|
43
|
+
Run the tests with `node --test tests/*.js`. Plans and open questions
|
|
44
|
+
are in [ROADMAP.md](./ROADMAP.md).
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
[MIT](./LICENSE)
|
package/bin.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs/promises"
|
|
4
|
-
import
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { intro, note, text, select, confirm, spinner, outro, box, isCancel, cancel } from "@clack/prompts"
|
|
5
7
|
import { parse } from "@bomb.sh/args"
|
|
6
8
|
import t from "@bomb.sh/tab"
|
|
7
9
|
import votive from "votive"
|
|
@@ -19,7 +21,23 @@ function wait(ms) {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
import init from "./index.js"
|
|
22
|
-
import { shouldClear } from "./cli.js"
|
|
24
|
+
import { shouldClear, normalizeDomain, wrongFolderWarning } from "./cli.js"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A prompt's answer - or, when the author pressed Ctrl-C or Escape, a
|
|
28
|
+
* clean exit. clack returns a cancel symbol instead of an answer, and
|
|
29
|
+
* the wizard used to carry on with it until a template string threw
|
|
30
|
+
* "Cannot convert a Symbol value to a string". Nothing has been written
|
|
31
|
+
* at that point: the wizard writes its files last.
|
|
32
|
+
* @template T
|
|
33
|
+
* @param {T | symbol} value
|
|
34
|
+
* @returns {T}
|
|
35
|
+
*/
|
|
36
|
+
function answer(value) {
|
|
37
|
+
if (!isCancel(value)) return /** @type {T} */ (value)
|
|
38
|
+
cancel("Setup cancelled. Nothing was written.")
|
|
39
|
+
process.exit(0)
|
|
40
|
+
}
|
|
23
41
|
|
|
24
42
|
/**
|
|
25
43
|
* Discards the build database. Only under --clear (alias --reset): an
|
|
@@ -38,6 +56,22 @@ async function removeDatabase(verbose) {
|
|
|
38
56
|
}
|
|
39
57
|
}
|
|
40
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Empties the buffer cache: an image's measurements and derivative list,
|
|
61
|
+
* cached by votive so a warm start doesn't decode it again. Without
|
|
62
|
+
* this, a reset still reused every image's old read. Only the `.json`
|
|
63
|
+
* results go. The `cache-key` file stays, because votive empties a
|
|
64
|
+
* folder that has no key, so removing it would discard the next
|
|
65
|
+
* launch's fresh cache. URL fetches are not cached here.
|
|
66
|
+
* @param {boolean} verbose
|
|
67
|
+
*/
|
|
68
|
+
async function removeBufferCache(verbose) {
|
|
69
|
+
const entries = await fs.readdir(config.cacheDirectory).catch(() => [])
|
|
70
|
+
const results = entries.filter(name => name.endsWith(".json"))
|
|
71
|
+
await Promise.all(results.map(name => fs.rm(path.join(config.cacheDirectory, name), { force: true })))
|
|
72
|
+
if (verbose) console.info(`${styleText("dim", "loading:")} ${styleText("green", results.length ? "buffer cache cleared" : "no buffer cache found")}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
41
75
|
/** @param {boolean} verbose */
|
|
42
76
|
async function removeOutput(verbose) {
|
|
43
77
|
try {
|
|
@@ -64,14 +98,14 @@ async function wizard() {
|
|
|
64
98
|
{
|
|
65
99
|
box(`Welcome!\n\nVowel is a static blog generator. It bundles markdown into HTML.`)
|
|
66
100
|
|
|
67
|
-
await select({
|
|
101
|
+
answer(await select({
|
|
68
102
|
message: "Proceed",
|
|
69
103
|
options: [
|
|
70
104
|
{
|
|
71
105
|
value: true,
|
|
72
106
|
label: "OK"
|
|
73
107
|
}]
|
|
74
|
-
})
|
|
108
|
+
}))
|
|
75
109
|
|
|
76
110
|
box(`Vowel infers most relevant information from common markdown conventions, so it requires very little configuration.
|
|
77
111
|
|
|
@@ -80,25 +114,25 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
80
114
|
|
|
81
115
|
|
|
82
116
|
|
|
83
|
-
const proceed = await confirm({
|
|
117
|
+
const proceed = answer(await confirm({
|
|
84
118
|
message: "Generate settings file"
|
|
85
|
-
})
|
|
119
|
+
}))
|
|
86
120
|
|
|
87
121
|
if (proceed) {
|
|
88
122
|
const frontmatter = [`---`]
|
|
89
123
|
|
|
90
124
|
box(`Change these configurations by editing 'settings.md'.`)
|
|
91
125
|
|
|
92
|
-
const websiteName = await text({
|
|
126
|
+
const websiteName = answer(await text({
|
|
93
127
|
message: "Website name",
|
|
94
128
|
placeholder: "My Cool Website"
|
|
95
|
-
})
|
|
129
|
+
}))
|
|
96
130
|
|
|
97
131
|
frontmatter.push(`name: ${websiteName}`)
|
|
98
132
|
|
|
99
133
|
box(`Vowel comes loaded with themes. You can override these themes by writing styles in a 'styles.css' file at the root of your project.`)
|
|
100
134
|
|
|
101
|
-
const theme = await select({
|
|
135
|
+
const theme = answer(await select({
|
|
102
136
|
message: "Theme",
|
|
103
137
|
options: [
|
|
104
138
|
{
|
|
@@ -122,7 +156,7 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
122
156
|
hint: 'write your own styles in `./styles.css`'
|
|
123
157
|
}
|
|
124
158
|
]
|
|
125
|
-
})
|
|
159
|
+
}))
|
|
126
160
|
|
|
127
161
|
if (theme) {
|
|
128
162
|
frontmatter.push(`theme: ${theme}`)
|
|
@@ -130,21 +164,24 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
130
164
|
|
|
131
165
|
box(`To publish a sitemap and RSS feed, provide the domain that you plan to use.`)
|
|
132
166
|
|
|
133
|
-
const domain = await text({
|
|
167
|
+
const domain = answer(await text({
|
|
134
168
|
message: "Domain",
|
|
135
169
|
placeholder: "thebomb.com"
|
|
136
|
-
})
|
|
170
|
+
}))
|
|
137
171
|
|
|
138
|
-
|
|
139
|
-
|
|
172
|
+
// As pasted from a browser: https://Example.com/ is example.com.
|
|
173
|
+
const siteDomain = normalizeDomain(domain)
|
|
174
|
+
|
|
175
|
+
if (siteDomain) {
|
|
176
|
+
frontmatter.push(`domain: ${siteDomain}`)
|
|
140
177
|
}
|
|
141
178
|
|
|
142
179
|
box(`To credit an author on your RSS feed, provide an author.`)
|
|
143
180
|
|
|
144
|
-
const author = await text({
|
|
181
|
+
const author = answer(await text({
|
|
145
182
|
message: "Author",
|
|
146
183
|
placeholder: "George Constanza"
|
|
147
|
-
})
|
|
184
|
+
}))
|
|
148
185
|
|
|
149
186
|
if (author) {
|
|
150
187
|
frontmatter.push(`author: ${author}`)
|
|
@@ -152,15 +189,16 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
152
189
|
|
|
153
190
|
box(`Markdown files are pages. Filenames are breadcrumbs. Folders are website sections. 'home.md' is your homepage.`)
|
|
154
191
|
|
|
155
|
-
const homepage = await confirm({
|
|
192
|
+
const homepage = answer(await confirm({
|
|
156
193
|
message: "Generate homepage"
|
|
157
|
-
})
|
|
194
|
+
}))
|
|
158
195
|
|
|
159
|
-
// A
|
|
160
|
-
|
|
161
|
-
|
|
196
|
+
// A slogan is a page's; the homepage's is the site's hero. Only
|
|
197
|
+
// asked for the homepage the wizard is about to write.
|
|
198
|
+
const slogan = homepage ? answer(await text({
|
|
199
|
+
message: "Homepage slogan (leave empty to skip)",
|
|
162
200
|
placeholder: "All the news that's fit to blog"
|
|
163
|
-
})
|
|
201
|
+
})) : undefined
|
|
164
202
|
|
|
165
203
|
const spin = spinner()
|
|
166
204
|
|
|
@@ -178,14 +216,14 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
178
216
|
|
|
179
217
|
const homeExists = await exists("home.md")
|
|
180
218
|
|
|
181
|
-
if (!homeExists) {
|
|
182
|
-
const homeFrontmatter =
|
|
219
|
+
if (homepage && !homeExists) {
|
|
220
|
+
const homeFrontmatter = slogan ? `---\nslogan: ${slogan}\n---\n` : ""
|
|
183
221
|
await fs.writeFile("home.md", `${homeFrontmatter}# Home\n\nWelcome home!`, "utf-8")
|
|
184
222
|
}
|
|
185
223
|
|
|
186
224
|
spin.clear()
|
|
187
225
|
|
|
188
|
-
await select({
|
|
226
|
+
answer(await select({
|
|
189
227
|
message: "Ready to go. Vowel will now launch your website.",
|
|
190
228
|
options: [
|
|
191
229
|
{
|
|
@@ -193,7 +231,7 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
|
|
|
193
231
|
label: "Let's go"
|
|
194
232
|
}
|
|
195
233
|
]
|
|
196
|
-
})
|
|
234
|
+
}))
|
|
197
235
|
|
|
198
236
|
|
|
199
237
|
}
|
|
@@ -243,18 +281,40 @@ async function main() {
|
|
|
243
281
|
return
|
|
244
282
|
}
|
|
245
283
|
|
|
246
|
-
|
|
284
|
+
// `vowel export <folder>`: the export command (plugins/export), spelled
|
|
285
|
+
// as a word rather than --command/--payload.
|
|
286
|
+
if (args._?.[0] === "export") {
|
|
287
|
+
await runCLICommand("export", JSON.stringify({ path: args._[1] }))
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// A site is a folder with a settings.md; the wizard writes one. Not "no
|
|
292
|
+
// database yet", which was also true of every site cloned onto a new
|
|
293
|
+
// machine. And only with a terminal to ask in: under CI or a pipe the
|
|
294
|
+
// prompts waited forever.
|
|
295
|
+
const settingsExists = await exists("settings.md")
|
|
296
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
297
|
+
|
|
298
|
+
if (!settingsExists && interactive) {
|
|
299
|
+
const entries = await fs.readdir(".")
|
|
300
|
+
const folder = process.cwd()
|
|
301
|
+
const warning = wrongFolderWarning(entries, { folder, home: folder === os.homedir() })
|
|
302
|
+
if (warning && !answer(await confirm({ message: warning, initialValue: false }))) {
|
|
303
|
+
cancel("Run vowel inside your site's folder, or in a new, empty one.")
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
}
|
|
247
307
|
|
|
248
308
|
if (shouldClear(args)) {
|
|
249
309
|
await removeDatabase(args.logging === "verbose")
|
|
310
|
+
await removeBufferCache(args.logging === "verbose")
|
|
250
311
|
await removeOutput(args.logging === "verbose")
|
|
251
312
|
}
|
|
252
313
|
await fs.mkdir(config.targetFolder, { recursive: true })
|
|
253
314
|
|
|
254
|
-
// The wizard runs
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
if (!args.skip && !dbExists) {
|
|
315
|
+
// The wizard runs for a new site only; --clear doesn't change that - a
|
|
316
|
+
// cleared project is still one that was set up.
|
|
317
|
+
if (!args.skip && !settingsExists && interactive) {
|
|
258
318
|
// const loading = votive({ ...config, verbose: false })
|
|
259
319
|
await wizard()
|
|
260
320
|
// await (loading)
|
package/cli.js
CHANGED
|
@@ -13,3 +13,35 @@
|
|
|
13
13
|
export function shouldClear(args) {
|
|
14
14
|
return Boolean(args?.clear || args?.reset)
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A domain as settings.md wants it: `https://Example.com/` → `example.com`.
|
|
19
|
+
* What the wizard's prompt gets is whatever someone pasted from a browser.
|
|
20
|
+
* @param {unknown} value
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeDomain(value) {
|
|
24
|
+
if (typeof value !== "string") return ""
|
|
25
|
+
return value.trim().replace(/^[a-z]+:\/\//i, "").replace(/\/+$/, "").toLowerCase()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether this looks like the wrong folder to make a site in, and why,
|
|
30
|
+
* as the question to ask - or undefined when it looks fine. Vowel makes
|
|
31
|
+
* a site from everything in the folder it runs in, so starting it in a
|
|
32
|
+
* home folder, or a folder of other things, sets up and scans all of
|
|
33
|
+
* it. A new, empty folder is fine; so is one with markdown in it.
|
|
34
|
+
* Only asked when there is no settings.md: a folder that has one is a
|
|
35
|
+
* site already.
|
|
36
|
+
* @param {string[]} entries - the folder's own entries, by name
|
|
37
|
+
* @param {{ folder: string, home: boolean }} where
|
|
38
|
+
* @returns {string | undefined}
|
|
39
|
+
*/
|
|
40
|
+
export function wrongFolderWarning(entries, { folder, home }) {
|
|
41
|
+
const visible = entries.filter(name => !name.startsWith("."))
|
|
42
|
+
const markdown = visible.filter(name => name.toLowerCase().endsWith(".md"))
|
|
43
|
+
if (home) return `${folder} is your home folder. Vowel makes a website from everything in the folder it runs in. Set up a site here anyway?`
|
|
44
|
+
if (markdown.length || !visible.length) return undefined
|
|
45
|
+
const others = visible.length === 1 ? "1 other thing" : `${visible.length} other things`
|
|
46
|
+
return `${folder} has no markdown files, and ${others} in it. Vowel makes a website from everything in the folder it runs in. Set up a site here anyway?`
|
|
47
|
+
}
|
package/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
2
3
|
import { resolveProjectFolder } from "votive"
|
|
3
4
|
import { systemDirectoryFor } from "./systemPaths.js"
|
|
4
5
|
import { secretRouter } from "./secretPaths.js"
|
|
@@ -16,6 +17,7 @@ import vowelAtprotoPlugin from "./plugins/atproto/index.js"
|
|
|
16
17
|
import vowelPublishPlugin from "./plugins/publish/index.js"
|
|
17
18
|
import vowelURLsPlugin from "./plugins/urls/index.js"
|
|
18
19
|
import vowelSecretPagesPlugin from "./plugins/secret-pages/index.js"
|
|
20
|
+
import vowelExportPlugin from "./plugins/export/index.js"
|
|
19
21
|
|
|
20
22
|
/** @import {VotiveConfig} from "votive" */
|
|
21
23
|
|
|
@@ -29,6 +31,14 @@ import vowelSecretPagesPlugin from "./plugins/secret-pages/index.js"
|
|
|
29
31
|
* the values), and a hex digest is lowercase already.
|
|
30
32
|
* @param {string} sourcePath
|
|
31
33
|
*/
|
|
34
|
+
/**
|
|
35
|
+
* Vowel's part of the cache key (votive's cacheKey.js): a warm start
|
|
36
|
+
* reuses the build cache only if this same version of vowel made it, so
|
|
37
|
+
* an upgrade that changes a bundled stylesheet or how pages render is
|
|
38
|
+
* seen without a `--clear`.
|
|
39
|
+
*/
|
|
40
|
+
const VOWEL_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version
|
|
41
|
+
|
|
32
42
|
function vowelRouter(sourcePath) {
|
|
33
43
|
return secretRouter(sourcePath).toLowerCase()
|
|
34
44
|
}
|
|
@@ -43,6 +53,7 @@ const defaultPlugins = [
|
|
|
43
53
|
vowelFontsPlugin,
|
|
44
54
|
vowelAssetsPlugin,
|
|
45
55
|
vowelDeployCloudflarePlugin,
|
|
56
|
+
vowelExportPlugin,
|
|
46
57
|
vowelURLsPlugin,
|
|
47
58
|
vowelAtprotoPlugin,
|
|
48
59
|
vowelPublishPlugin,
|
|
@@ -103,6 +114,7 @@ function createConfig(sourceFolder = ".", overrides = {}) {
|
|
|
103
114
|
databasePath: path.join(systemDirectory, ".votive.db"),
|
|
104
115
|
cacheDirectory: path.join(systemDirectory, ".cache"),
|
|
105
116
|
plugins: [...defaultPlugins, ...(extraPlugins ?? [])],
|
|
117
|
+
cacheKey: `vowel@${VOWEL_VERSION}`,
|
|
106
118
|
...rest
|
|
107
119
|
}
|
|
108
120
|
}
|
package/extractDate.js
CHANGED