vowel 0.7.0-beta.0 → 0.7.1

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -239
  3. package/bin.js +73 -31
  4. package/cli.js +32 -0
  5. package/config.js +14 -0
  6. package/extractDate.js +2 -2
  7. package/index.js +6 -1
  8. package/package.json +9 -34
  9. package/pathDate.js +69 -0
  10. package/plugins/assets/index.js +1 -1
  11. package/plugins/atproto/body.js +413 -0
  12. package/plugins/atproto/index.js +10 -44
  13. package/plugins/atproto/lexicons.js +179 -0
  14. package/plugins/atproto/mapper.js +399 -0
  15. package/plugins/atproto/publish.js +195 -105
  16. package/plugins/atproto/record.js +57 -68
  17. package/plugins/deploy-cloudflare/index.js +4 -1
  18. package/plugins/export/index.js +131 -0
  19. package/plugins/html/bundle/index.js +57 -23
  20. package/plugins/html/editor/Editor.svelte +9 -5
  21. package/plugins/html/editor/create-session.js +3 -1
  22. package/plugins/html/editor/directives.js +12 -0
  23. package/plugins/html/editor/frontmatter.js +42 -11
  24. package/plugins/html/editor/ingest.js +19 -8
  25. package/plugins/html/editor/serialize.js +8 -4
  26. package/plugins/html/image.js +11 -5
  27. package/plugins/html/index.js +217 -100
  28. package/plugins/html/partials.js +14 -8
  29. package/plugins/html/socialLinks.js +5 -8
  30. package/plugins/html/writeRules.js +197 -37
  31. package/plugins/images/favicon.png +0 -0
  32. package/plugins/images/index.js +41 -3
  33. package/plugins/markdown/colorScheme.js +70 -14
  34. package/plugins/markdown/frontmatter.js +32 -6
  35. package/plugins/markdown/index.js +105 -20
  36. package/plugins/markdown/metadata.js +74 -22
  37. package/plugins/markdown/typography.js +11 -11
  38. package/plugins/secret-pages/index.js +75 -0
  39. package/plugins/styles/index.js +4 -4
  40. package/plugins/xml/entryContent.js +22 -11
  41. package/plugins/xml/index.js +19 -11
  42. package/stylesheets/DefaultStyles.css +198 -111
  43. package/stylesheets/ResetStyles.css +2 -1
  44. package/stylesheets/TypographyStyles.css +153 -74
  45. package/utils.js +74 -1
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,8 @@
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 { intro, note, text, select, confirm, spinner, outro, box, isCancel, cancel } from "@clack/prompts"
5
6
  import { parse } from "@bomb.sh/args"
6
7
  import t from "@bomb.sh/tab"
7
8
  import votive from "votive"
@@ -19,7 +20,23 @@ function wait(ms) {
19
20
  }
20
21
 
21
22
  import init from "./index.js"
22
- import { shouldClear } from "./cli.js"
23
+ import { shouldClear, normalizeDomain, wrongFolderWarning } from "./cli.js"
24
+
25
+ /**
26
+ * A prompt's answer - or, when the author pressed Ctrl-C or Escape, a
27
+ * clean exit. clack returns a cancel symbol instead of an answer, and
28
+ * the wizard used to carry on with it until a template string threw
29
+ * "Cannot convert a Symbol value to a string". Nothing has been written
30
+ * at that point: the wizard writes its files last.
31
+ * @template T
32
+ * @param {T | symbol} value
33
+ * @returns {T}
34
+ */
35
+ function answer(value) {
36
+ if (!isCancel(value)) return /** @type {T} */ (value)
37
+ cancel("Setup cancelled. Nothing was written.")
38
+ process.exit(0)
39
+ }
23
40
 
24
41
  /**
25
42
  * Discards the build database. Only under --clear (alias --reset): an
@@ -64,14 +81,14 @@ async function wizard() {
64
81
  {
65
82
  box(`Welcome!\n\nVowel is a static blog generator. It bundles markdown into HTML.`)
66
83
 
67
- await select({
84
+ answer(await select({
68
85
  message: "Proceed",
69
86
  options: [
70
87
  {
71
88
  value: true,
72
89
  label: "OK"
73
90
  }]
74
- })
91
+ }))
75
92
 
76
93
  box(`Vowel infers most relevant information from common markdown conventions, so it requires very little configuration.
77
94
 
@@ -80,25 +97,25 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
80
97
 
81
98
 
82
99
 
83
- const proceed = await confirm({
100
+ const proceed = answer(await confirm({
84
101
  message: "Generate settings file"
85
- })
102
+ }))
86
103
 
87
104
  if (proceed) {
88
105
  const frontmatter = [`---`]
89
106
 
90
107
  box(`Change these configurations by editing 'settings.md'.`)
91
108
 
92
- const websiteName = await text({
109
+ const websiteName = answer(await text({
93
110
  message: "Website name",
94
111
  placeholder: "My Cool Website"
95
- })
112
+ }))
96
113
 
97
114
  frontmatter.push(`name: ${websiteName}`)
98
115
 
99
116
  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
117
 
101
- const theme = await select({
118
+ const theme = answer(await select({
102
119
  message: "Theme",
103
120
  options: [
104
121
  {
@@ -122,7 +139,7 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
122
139
  hint: 'write your own styles in `./styles.css`'
123
140
  }
124
141
  ]
125
- })
142
+ }))
126
143
 
127
144
  if (theme) {
128
145
  frontmatter.push(`theme: ${theme}`)
@@ -130,21 +147,24 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
130
147
 
131
148
  box(`To publish a sitemap and RSS feed, provide the domain that you plan to use.`)
132
149
 
133
- const domain = await text({
150
+ const domain = answer(await text({
134
151
  message: "Domain",
135
152
  placeholder: "thebomb.com"
136
- })
153
+ }))
137
154
 
138
- if (domain) {
139
- frontmatter.push(`domain: ${domain}`)
155
+ // As pasted from a browser: https://Example.com/ is example.com.
156
+ const siteDomain = normalizeDomain(domain)
157
+
158
+ if (siteDomain) {
159
+ frontmatter.push(`domain: ${siteDomain}`)
140
160
  }
141
161
 
142
162
  box(`To credit an author on your RSS feed, provide an author.`)
143
163
 
144
- const author = await text({
164
+ const author = answer(await text({
145
165
  message: "Author",
146
166
  placeholder: "George Constanza"
147
- })
167
+ }))
148
168
 
149
169
  if (author) {
150
170
  frontmatter.push(`author: ${author}`)
@@ -152,15 +172,16 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
152
172
 
153
173
  box(`Markdown files are pages. Filenames are breadcrumbs. Folders are website sections. 'home.md' is your homepage.`)
154
174
 
155
- const homepage = await confirm({
175
+ const homepage = answer(await confirm({
156
176
  message: "Generate homepage"
157
- })
177
+ }))
158
178
 
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)",
179
+ // A slogan is a page's; the homepage's is the site's hero. Only
180
+ // asked for the homepage the wizard is about to write.
181
+ const slogan = homepage ? answer(await text({
182
+ message: "Homepage slogan (leave empty to skip)",
162
183
  placeholder: "All the news that's fit to blog"
163
- })
184
+ })) : undefined
164
185
 
165
186
  const spin = spinner()
166
187
 
@@ -178,14 +199,14 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
178
199
 
179
200
  const homeExists = await exists("home.md")
180
201
 
181
- if (!homeExists) {
182
- const homeFrontmatter = tagline ? `---\ntagline: ${tagline}\n---\n` : ""
202
+ if (homepage && !homeExists) {
203
+ const homeFrontmatter = slogan ? `---\nslogan: ${slogan}\n---\n` : ""
183
204
  await fs.writeFile("home.md", `${homeFrontmatter}# Home\n\nWelcome home!`, "utf-8")
184
205
  }
185
206
 
186
207
  spin.clear()
187
208
 
188
- await select({
209
+ answer(await select({
189
210
  message: "Ready to go. Vowel will now launch your website.",
190
211
  options: [
191
212
  {
@@ -193,7 +214,7 @@ The few configurations that Vowel uses live in a 'settings.md' file at the root
193
214
  label: "Let's go"
194
215
  }
195
216
  ]
196
- })
217
+ }))
197
218
 
198
219
 
199
220
  }
@@ -243,7 +264,29 @@ async function main() {
243
264
  return
244
265
  }
245
266
 
246
- const dbExists = await exists(config.databasePath)
267
+ // `vowel export <folder>`: the export command (plugins/export), spelled
268
+ // as a word rather than --command/--payload.
269
+ if (args._?.[0] === "export") {
270
+ await runCLICommand("export", JSON.stringify({ path: args._[1] }))
271
+ return
272
+ }
273
+
274
+ // A site is a folder with a settings.md; the wizard writes one. Not "no
275
+ // database yet", which was also true of every site cloned onto a new
276
+ // machine. And only with a terminal to ask in: under CI or a pipe the
277
+ // prompts waited forever.
278
+ const settingsExists = await exists("settings.md")
279
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY)
280
+
281
+ if (!settingsExists && interactive) {
282
+ const entries = await fs.readdir(".")
283
+ const folder = process.cwd()
284
+ const warning = wrongFolderWarning(entries, { folder, home: folder === os.homedir() })
285
+ if (warning && !answer(await confirm({ message: warning, initialValue: false }))) {
286
+ cancel("Run vowel inside your site's folder, or in a new, empty one.")
287
+ return
288
+ }
289
+ }
247
290
 
248
291
  if (shouldClear(args)) {
249
292
  await removeDatabase(args.logging === "verbose")
@@ -251,10 +294,9 @@ async function main() {
251
294
  }
252
295
  await fs.mkdir(config.targetFolder, { recursive: true })
253
296
 
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) {
297
+ // The wizard runs for a new site only; --clear doesn't change that - a
298
+ // cleared project is still one that was set up.
299
+ if (!args.skip && !settingsExists && interactive) {
258
300
  // const loading = votive({ ...config, verbose: false })
259
301
  await wizard()
260
302
  // 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"
@@ -15,6 +16,8 @@ import vowelWriteHTMLPlugin from "./plugins/html/index.js"
15
16
  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"
19
+ import vowelSecretPagesPlugin from "./plugins/secret-pages/index.js"
20
+ import vowelExportPlugin from "./plugins/export/index.js"
18
21
 
19
22
  /** @import {VotiveConfig} from "votive" */
20
23
 
@@ -28,6 +31,14 @@ import vowelURLsPlugin from "./plugins/urls/index.js"
28
31
  * the values), and a hex digest is lowercase already.
29
32
  * @param {string} sourcePath
30
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
+
31
42
  function vowelRouter(sourcePath) {
32
43
  return secretRouter(sourcePath).toLowerCase()
33
44
  }
@@ -42,9 +53,11 @@ const defaultPlugins = [
42
53
  vowelFontsPlugin,
43
54
  vowelAssetsPlugin,
44
55
  vowelDeployCloudflarePlugin,
56
+ vowelExportPlugin,
45
57
  vowelURLsPlugin,
46
58
  vowelAtprotoPlugin,
47
59
  vowelPublishPlugin,
60
+ vowelSecretPagesPlugin,
48
61
  vowelWriteHTMLPlugin
49
62
  ]
50
63
 
@@ -101,6 +114,7 @@ function createConfig(sourceFolder = ".", overrides = {}) {
101
114
  databasePath: path.join(systemDirectory, ".votive.db"),
102
115
  cacheDirectory: path.join(systemDirectory, ".cache"),
103
116
  plugins: [...defaultPlugins, ...(extraPlugins ?? [])],
117
+ cacheKey: `vowel@${VOWEL_VERSION}`,
104
118
  ...rest
105
119
  }
106
120
  }
package/extractDate.js CHANGED
@@ -16,7 +16,7 @@ function monthIndex(month) {
16
16
  // Deliberately unanchored: a caller may want the date out of a longer
17
17
  // string. Use dateSpan() to tell "this text *is* a date" from "this text
18
18
  // mentions one".
19
- const DATE_PATTERN = /(?:\b(?<first_segment>(?<first_MMM_or_MMMM>January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Sep|Oct|Nov|Dec)|(?<first_YYYY>\d\d\d\d)|(?<first_do>(?:\d|[0-3]\d)(?:st|nd|rd|th))|(?<first_dd_or_MM_or_yy>[0-3]\d)|(?<first_d_or_M>\d))(?<first_delimiter>\.| |, |\/|-)?)(?:\b(?<second_segment>(?<second_MMM_or_MMMM>January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Sep|Oct|Nov|Dec)|(?<second_YYYY>\d\d\d\d)|(?<second_do>(\d\d)(:?st|nd|rd|th))|(?<second_dd_or_MM_or_yy>[0-3]\d)|(?<second_d_or_M>\d))(?<second_delimiter>\.| |, |\/|-)?)(?:\b(?<third_segment>(?<third_MMM_or_MMMM>January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Sep|Oct|Nov|Dec)|(?<third_YYYY>\d\d\d\d)|(?<third_do>(\d\d)(?:st|nd|rd|th))|(?<third_dd_or_MM_or_yy>[0-3]\d)|(?<third_d_or_M>\d)))(?<time>(?<time_delimiter>T|\ | at )(?<H_or_HH>[0-2]?\d):(?<MM>[0-6]\d)(?::(?<SS>00))?(?:\.(?<SSS>\d\d\d))?(?<Z>Z|(?:\+(\d\d):(\d\d)))?(?<meridiam_indicator>am|pm)?)?/i
19
+ const DATE_PATTERN = /(?:\b(?<first_segment>(?<first_MMM_or_MMMM>January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Sep|Oct|Nov|Dec)|(?<first_YYYY>\d\d\d\d)|(?<first_do>(?:\d|[0-3]\d)(?:st|nd|rd|th))|(?<first_dd_or_MM_or_yy>[0-3]\d)|(?<first_d_or_M>\d))(?<first_delimiter>\.| |, |\/|-)?)(?:\b(?<second_segment>(?<second_MMM_or_MMMM>January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Sep|Oct|Nov|Dec)|(?<second_YYYY>\d\d\d\d)|(?<second_do>(\d\d)(:?st|nd|rd|th))|(?<second_dd_or_MM_or_yy>[0-3]\d)|(?<second_d_or_M>\d))(?<second_delimiter>\.| |, |\/|-)?)(?:\b(?<third_segment>(?<third_MMM_or_MMMM>January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Sep|Oct|Nov|Dec)|(?<third_YYYY>\d\d\d\d)|(?<third_do>(\d\d)(?:st|nd|rd|th))|(?<third_dd_or_MM_or_yy>[0-3]\d)|(?<third_d_or_M>\d)))(?<time>(?<time_delimiter>T|\ | at )(?<H_or_HH>[0-2]?\d):(?<MM>[0-6]\d)(?::(?<SS>00))?(?:\.(?<SSS>\d\d\d))?(?<Z>Z|(?:[\+\-](\d\d):(\d\d)))?(?<meridiam_indicator>am|pm)?)?/i
20
20
 
21
21
  /**
22
22
  * The part of `text` that looks like a date, or undefined.
@@ -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
 
package/index.js CHANGED
@@ -12,10 +12,15 @@ import { createConfig } from "./config.js"
12
12
  * differ from the one requested; `close()` releases the watchers and the
13
13
  * listener, which a host serving more than one project over a session
14
14
  * needs before opening the next one.
15
+ *
16
+ * The dev server prints each secret page's url as it first sees it
17
+ * (plugins/secret-pages), which is how an author finds them. Only here:
18
+ * a build that deploys or publishes may be logged somewhere public.
19
+ * Pass `announceSecretPages: false` to turn it off.
15
20
  */
16
21
  async function init(options = {}) {
17
22
  const { sourceFolder, ...overrides } = options
18
- const config = createConfig(sourceFolder, overrides)
23
+ const config = createConfig(sourceFolder, { announceSecretPages: true, ...overrides })
19
24
  return startServer(config)
20
25
  }
21
26