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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -239
  3. package/bin.js +91 -31
  4. package/cli.js +32 -0
  5. package/config.js +12 -0
  6. package/extractDate.js +1 -1
  7. package/package.json +8 -34
  8. package/pathDate.js +69 -0
  9. package/plugins/assets/index.js +1 -1
  10. package/plugins/atproto/body.js +3 -1
  11. package/plugins/atproto/index.js +5 -45
  12. package/plugins/atproto/publish.js +42 -11
  13. package/plugins/atproto/record.js +47 -4
  14. package/plugins/deploy-cloudflare/index.js +4 -1
  15. package/plugins/export/index.js +131 -0
  16. package/plugins/html/bundle/index.js +101 -44
  17. package/plugins/html/editor/Editor.svelte +9 -5
  18. package/plugins/html/editor/create-session.js +3 -1
  19. package/plugins/html/editor/directives.js +12 -0
  20. package/plugins/html/editor/frontmatter.js +77 -12
  21. package/plugins/html/editor/ingest.js +29 -31
  22. package/plugins/html/editor/main.js +5 -1
  23. package/plugins/html/editor/serialize.js +8 -4
  24. package/plugins/html/editor/socket.js +34 -4
  25. package/plugins/html/image.js +88 -27
  26. package/plugins/html/index.js +237 -109
  27. package/plugins/html/partials.js +41 -17
  28. package/plugins/html/socialLinks.js +13 -11
  29. package/plugins/html/writeRules.js +234 -42
  30. package/plugins/images/favicon.png +0 -0
  31. package/plugins/images/index.js +69 -8
  32. package/plugins/markdown/colorScheme.js +70 -14
  33. package/plugins/markdown/frontmatter.js +18 -8
  34. package/plugins/markdown/index.js +94 -25
  35. package/plugins/markdown/metadata.js +132 -32
  36. package/plugins/markdown/typography.js +11 -11
  37. package/plugins/styles/index.js +4 -4
  38. package/plugins/xml/index.js +17 -10
  39. package/stylesheets/DefaultStyles.css +163 -108
  40. package/stylesheets/TypographyStyles.css +148 -81
  41. 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
- - Bundled with [Votive](https://github.com/samlfair/votive)
6
- - Served with [Votive](https://github.com/samlfair/votive)'s dev server
7
-
8
- See the docs: [vowel.cc](https://vowel.cc).
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
- og:type - The type of your object, e.g., "video.movie". Depending on the type you specify, other properties may also be required.
238
- og:locale - The locale these tags are marked up in. Of the format language_TERRITORY. Default is en_US.
239
- article:published_time - datetime - When the article was first published.
240
- article:modified_time - datetime - When the article was last changed.
241
- article:expiration_time - datetime - When the article is out of date after.
242
- article:author - profile array - Writers of the article.
243
- article:section - string - A high-level section name. E.g. Technology
244
- article:tag - string array - Tag words associated with this article.
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 { intro, note, text, select, confirm, spinner, outro, box } from "@clack/prompts"
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
- if (domain) {
139
- frontmatter.push(`domain: ${domain}`)
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 tagline is a page's; the homepage's is the site's hero.
160
- const tagline = await text({
161
- message: "Homepage tagline (leave empty to skip)",
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 = tagline ? `---\ntagline: ${tagline}\n---\n` : ""
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
- const dbExists = await exists(config.databasePath)
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 on a first launch only: no database yet is the one
255
- // signal that legitimately means "new project", and --clear does not
256
- // change that - a cleared project is still one that was set up.
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
@@ -143,5 +143,5 @@ function extractDate(text) {
143
143
  }
144
144
 
145
145
  export default extractDate
146
- export { extractDate, dateSpan }
146
+ export { extractDate, dateSpan, monthIndex }
147
147