orga-build 0.9.0 → 0.10.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/README.org +282 -33
- package/cli.js +4 -4
- package/lib/__tests__/build.test.js +441 -22
- package/lib/__tests__/dev.test.d.ts +2 -0
- package/lib/__tests__/dev.test.d.ts.map +1 -0
- package/lib/__tests__/dev.test.js +295 -0
- package/lib/__tests__/fixtures.d.ts +8 -0
- package/lib/__tests__/fixtures.d.ts.map +1 -0
- package/lib/__tests__/fixtures.js +61 -0
- package/lib/app.jsx +23 -50
- package/lib/build.d.ts +4 -1
- package/lib/build.d.ts.map +1 -1
- package/lib/build.js +10 -175
- package/lib/config.d.ts +5 -3
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +8 -16
- package/lib/content.d.ts +6 -0
- package/lib/dev-ssr.d.ts +14 -0
- package/lib/dev-ssr.d.ts.map +1 -0
- package/lib/dev-ssr.js +137 -0
- package/lib/endpoint.d.ts +5 -0
- package/lib/endpoint.d.ts.map +1 -1
- package/lib/endpoint.js +1 -0
- package/lib/files.d.ts +8 -3
- package/lib/files.d.ts.map +1 -1
- package/lib/files.js +104 -20
- package/lib/fs.d.ts +0 -5
- package/lib/fs.d.ts.map +1 -1
- package/lib/fs.js +0 -18
- package/lib/html.d.ts +37 -0
- package/lib/html.d.ts.map +1 -0
- package/lib/html.js +101 -0
- package/lib/index.html +0 -1
- package/lib/island-client.d.ts +5 -0
- package/lib/island-client.d.ts.map +1 -0
- package/lib/island-client.js +41 -0
- package/lib/island.d.ts +18 -0
- package/lib/island.d.ts.map +1 -0
- package/lib/island.js +146 -0
- package/lib/island.jsx +108 -0
- package/lib/orga.d.ts +1 -1
- package/lib/orga.d.ts.map +1 -1
- package/lib/orga.js +54 -16
- package/lib/plugin.d.ts +20 -32
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +128 -178
- package/lib/prerender.d.ts +11 -0
- package/lib/prerender.d.ts.map +1 -0
- package/lib/prerender.js +184 -0
- package/lib/serve.d.ts.map +1 -1
- package/lib/serve.js +5 -23
- package/lib/ssr.jsx +9 -14
- package/lib/util.d.ts +0 -22
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +0 -62
- package/lib/vite.d.ts +5 -6
- package/lib/vite.d.ts.map +1 -1
- package/lib/vite.js +74 -23
- package/package.json +5 -8
- package/lib/components.d.ts +0 -2
- package/lib/components.d.ts.map +0 -1
- package/lib/components.js +0 -1
- package/lib/csr.jsx +0 -11
- package/lib/watch.d.ts +0 -10
- package/lib/watch.d.ts.map +0 -1
- package/lib/watch.js +0 -53
package/README.org
CHANGED
|
@@ -4,7 +4,9 @@ A simple tool that builds org-mode files into a website.
|
|
|
4
4
|
|
|
5
5
|
* Architecture
|
|
6
6
|
|
|
7
|
-
orga-build is built on top of Vite with a Vite-native architecture. The dev server uses Vite's native =createServer().listen()= pattern (no custom Express wrapper), which ensures maximum compatibility with the Vite ecosystem, including plugins like Cloudflare Workers.
|
|
7
|
+
orga-build is built on top of Vite (8+) with a Vite-native architecture. The dev server uses Vite's native =createServer().listen()= pattern (no custom Express wrapper), which ensures maximum compatibility with the Vite ecosystem, including plugins like Cloudflare Workers.
|
|
8
|
+
|
|
9
|
+
The build uses Vite's Environment API: =orgaBuildPlugin= configures an =ssr= and a =client= environment, and its =buildApp= hook builds both and prerenders every page and endpoint to static HTML. Pages ship no JavaScript unless they use an [[*Interactive Components (Islands)][island]]. The CLI is a thin wrapper, so =vite build= with the plugin produces the same site.
|
|
8
10
|
|
|
9
11
|
** Key Design Principles
|
|
10
12
|
|
|
@@ -31,30 +33,26 @@ export default {
|
|
|
31
33
|
|
|
32
34
|
Note: Do NOT add =orgaBuildPlugin()= to =vitePlugins= - it's already included by the CLI. Only add external plugins.
|
|
33
35
|
|
|
34
|
-
For advanced users integrating directly with Vite (without the orga-build CLI), use =orgaBuildPlugin= in your =vite.config.js
|
|
36
|
+
For advanced users integrating directly with Vite (without the orga-build CLI), use =orgaBuildPlugin= in your =vite.config.js=. =vite= then serves the site with SSR and =vite build= builds the static site:
|
|
35
37
|
|
|
36
38
|
#+begin_src javascript
|
|
37
39
|
// vite.config.js (advanced - direct Vite integration)
|
|
38
40
|
import { cloudflare } from '@cloudflare/vite-plugin'
|
|
39
|
-
import { orgaBuildPlugin
|
|
41
|
+
import { orgaBuildPlugin } from 'orga-build'
|
|
40
42
|
|
|
41
43
|
export default {
|
|
42
44
|
plugins: [
|
|
43
45
|
cloudflare(),
|
|
44
|
-
...orgaBuildPlugin({ root: 'pages', containerClass: [] })
|
|
45
|
-
]
|
|
46
|
-
resolve: { alias }
|
|
46
|
+
...orgaBuildPlugin({ root: 'pages', outDir: 'out', containerClass: [] })
|
|
47
|
+
]
|
|
47
48
|
}
|
|
48
49
|
#+end_src
|
|
49
50
|
|
|
50
51
|
** Default HTML Template
|
|
51
52
|
|
|
52
|
-
If your project doesn't have an =index.html= file, orga-build provides a default template that
|
|
53
|
-
- Sets up React rendering
|
|
54
|
-
- Enables client-side routing
|
|
55
|
-
- Works in both dev and production builds
|
|
53
|
+
If your project doesn't have an =index.html= file, orga-build provides a default template: a bare HTML document with a =<div id="root">= that pages are rendered into.
|
|
56
54
|
|
|
57
|
-
To customize the HTML shell, create your own =index.html= in your project root.
|
|
55
|
+
To customize the HTML shell, create your own =index.html= in your project root. Keep =<div id="root"></div>=; Vite processes the rest like any =index.html= (bundling linked scripts and stylesheets), and =%orga.<key>%= placeholders are filled from each page's exports (e.g. =%orga.title%=).
|
|
58
56
|
|
|
59
57
|
* Installation
|
|
60
58
|
|
|
@@ -62,24 +60,36 @@ To customize the HTML shell, create your own =index.html= in your project root.
|
|
|
62
60
|
npm install orga-build
|
|
63
61
|
#+end_src
|
|
64
62
|
|
|
63
|
+
* Usage
|
|
64
|
+
|
|
65
|
+
#+begin_src bash
|
|
66
|
+
orga-build dev # dev server with live reload
|
|
67
|
+
orga-build # build the static site into outDir
|
|
68
|
+
orga-build --outDir dist # override outDir (also -o)
|
|
69
|
+
#+end_src
|
|
70
|
+
|
|
65
71
|
* Configuration
|
|
66
72
|
|
|
67
|
-
orga-build uses =orga.config.js= (or =orga.config.mjs=) as the primary configuration file. This file should be placed in your project root.
|
|
73
|
+
orga-build uses =orga.config.js= (or =orga.config.mjs=) as the primary configuration file. This file should be placed in your project root. It is optional; a config file that fails to load fails the command.
|
|
68
74
|
|
|
69
75
|
#+begin_src javascript
|
|
70
76
|
// orga.config.js
|
|
71
77
|
export default {
|
|
72
|
-
// Directory containing your .org files (default: '
|
|
78
|
+
// Directory containing your .org files (default: '.', the project root)
|
|
73
79
|
root: 'pages',
|
|
74
80
|
|
|
75
|
-
// Output directory for production build (default: 'out')
|
|
81
|
+
// Output directory for production build (default: '.out')
|
|
76
82
|
outDir: 'out',
|
|
77
83
|
|
|
84
|
+
// Absolute URL the site is served from, for canonical URLs, feeds and
|
|
85
|
+
// sitemaps. Include the path if the site isn't at the domain root.
|
|
86
|
+
site: 'https://example.com',
|
|
87
|
+
|
|
78
88
|
// CSS class(es) to wrap rendered org content
|
|
79
89
|
containerClass: ['prose', 'prose-lg'],
|
|
80
90
|
|
|
81
91
|
// Global stylesheets — paths relative to orga.config.js (leading / optional).
|
|
82
|
-
//
|
|
92
|
+
// Linked from the HTML shell; Vite processes them (with HMR in dev).
|
|
83
93
|
styles: ['pages/style.css'],
|
|
84
94
|
|
|
85
95
|
// Extra rehype plugins appended to orga-build defaults
|
|
@@ -100,10 +110,11 @@ export default {
|
|
|
100
110
|
|
|
101
111
|
| Option | Type | Default | Description |
|
|
102
112
|
|----------------+-------------------+---------+-----------------------------------------------------------------|
|
|
103
|
-
| =root= | =string= | ='
|
|
104
|
-
| =outDir= | =string= | ='out'=
|
|
113
|
+
| =root= | =string= | ='.'= | Directory containing content files |
|
|
114
|
+
| =outDir= | =string= | ='.out'= | Output directory for production build |
|
|
115
|
+
| =site= | =string= | none | Absolute URL the site is served from, see [[*Site URL][Site URL]] |
|
|
105
116
|
| =containerClass= | =string \vert string[]= | =[]= | CSS class(es) for content wrapper |
|
|
106
|
-
| =styles= | =string[]= | =[]= | Stylesheets to
|
|
117
|
+
| =styles= | =string[]= | =[]= | Stylesheets to link; paths relative to =orga.config.js= |
|
|
107
118
|
| =exclude= | =string[]= | =[]= | Glob patterns (relative to =root=) excluded from content scanning |
|
|
108
119
|
| =rehypePlugins= | =PluggableList= | =[]= | Extra rehype plugins appended to orga-build defaults |
|
|
109
120
|
| =vitePlugins= | =PluginOption[]= | =[]= | Additional Vite plugins |
|
|
@@ -132,7 +143,7 @@ Page routes are discovered from =.org=, =.tsx=, and =.jsx= files.
|
|
|
132
143
|
|
|
133
144
|
At build time, page routes are emitted as HTML:
|
|
134
145
|
|
|
135
|
-
- =/about= ->
|
|
146
|
+
- =/about= -> =.out/about/index.html=
|
|
136
147
|
|
|
137
148
|
** Endpoint Routes
|
|
138
149
|
|
|
@@ -152,13 +163,65 @@ export async function GET(ctx) {
|
|
|
152
163
|
}
|
|
153
164
|
#+end_src
|
|
154
165
|
|
|
166
|
+
=ctx= has:
|
|
167
|
+
|
|
168
|
+
- =url=: the request URL. At build time it is =site + route= (or =http://localhost= + route without =site=).
|
|
169
|
+
- =site=: the configured =site=, see [[*Site URL][Site URL]]
|
|
170
|
+
- =mode=: ='dev'= or ='build'=
|
|
171
|
+
- =route.route=: the route, e.g. ='/rss.xml'=
|
|
172
|
+
|
|
155
173
|
At build time, endpoint routes are emitted to exact filenames:
|
|
156
174
|
|
|
157
|
-
- =/rss.xml= ->
|
|
158
|
-
- =/api/data.json= ->
|
|
175
|
+
- =/rss.xml= -> =.out/rss.xml=
|
|
176
|
+
- =/api/data.json= -> =.out/api/data.json=
|
|
159
177
|
|
|
160
178
|
Route conflicts (same final route path) fail fast during dev/build startup.
|
|
161
179
|
|
|
180
|
+
** Links
|
|
181
|
+
|
|
182
|
+
Links to =.org= files point at their pages, under Vite's =base=. Every heading gets an =id=: its =CUSTOM_ID= property when set, otherwise a slug of its text (=* Getting Started= → =#getting-started=).
|
|
183
|
+
|
|
184
|
+
| Link | Goes to |
|
|
185
|
+
|----------------------------------------+------------------------------|
|
|
186
|
+
| =[[file:guide.org]]= | =/guide= |
|
|
187
|
+
| =[[file:guide.org::*Getting Started]]= | =/guide#getting-started= |
|
|
188
|
+
| =[[file:guide.org::#install]]= | =/guide#install= |
|
|
189
|
+
| =[[*Getting Started]]= | heading on the same page |
|
|
190
|
+
| =[[#install]]= | =CUSTOM_ID= on the same page |
|
|
191
|
+
|
|
192
|
+
A =::*Heading= link to another file assumes that heading's id is the slug of its text. If the heading has a =CUSTOM_ID=, link to it with =::#id= instead. =id:= links aren't supported yet.
|
|
193
|
+
|
|
194
|
+
* Interactive Components (Islands)
|
|
195
|
+
|
|
196
|
+
Pages are static HTML. To run a component in the browser, start its module with React's ='use client'= directive, like in any React framework:
|
|
197
|
+
|
|
198
|
+
#+begin_src tsx
|
|
199
|
+
// _components.tsx
|
|
200
|
+
'use client'
|
|
201
|
+
import { useState } from 'react'
|
|
202
|
+
|
|
203
|
+
export function Counter({ start }: { start: number }) {
|
|
204
|
+
const [count, setCount] = useState(start)
|
|
205
|
+
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
206
|
+
}
|
|
207
|
+
#+end_src
|
|
208
|
+
|
|
209
|
+
#+begin_example
|
|
210
|
+
#+jsx: <Counter start={1} />
|
|
211
|
+
#+end_example
|
|
212
|
+
|
|
213
|
+
The component is still prerendered. On pages that use it, the browser imports just that module (plus a shared React chunk) and hydrates it in place; every other page ships no JavaScript. Only components become islands, recognized by their PascalCase name (or the default export); hooks and other exports of a ='use client'= module stay plain values. A page file (=.tsx=) can start with ='use client'= too, which makes the whole page an island.
|
|
214
|
+
|
|
215
|
+
Props cross to the browser as JSON, so they must be serializable: no functions and no =children=. Components rendered /inside/ an island are already client code and have no such limit.
|
|
216
|
+
|
|
217
|
+
An island is wrapped in an =<orga-island>= element, so it can't sit where HTML allows only specific children, such as directly inside =<table>=, =<tbody>=, =<tr>=, =<select>= or =<ul>=. Make the island the whole table or list instead.
|
|
218
|
+
|
|
219
|
+
Islands hydrate as soon as the page loads. Pass =client="visible"= to defer hydration until the island scrolls into view, which also defers loading React:
|
|
220
|
+
|
|
221
|
+
#+begin_example
|
|
222
|
+
#+jsx: <Counter start={1} client="visible" />
|
|
223
|
+
#+end_example
|
|
224
|
+
|
|
162
225
|
* TypeScript Setup
|
|
163
226
|
|
|
164
227
|
If you're using TypeScript and want type support for the =orga-build:content= virtual module, you need to add a reference to the type definitions.
|
|
@@ -224,9 +287,9 @@ const writing = getPages('writing')
|
|
|
224
287
|
// Get entries in a nested path
|
|
225
288
|
const posts2025 = getPages('content/writing/2025')
|
|
226
289
|
|
|
227
|
-
// Filter
|
|
228
|
-
const
|
|
229
|
-
return entry.data['
|
|
290
|
+
// Filter by metadata
|
|
291
|
+
const featured = getPages('writing', (entry) => {
|
|
292
|
+
return entry.data['featured'] === 'true'
|
|
230
293
|
})
|
|
231
294
|
#+end_src
|
|
232
295
|
|
|
@@ -268,6 +331,10 @@ const entries = getEntries([
|
|
|
268
331
|
])
|
|
269
332
|
#+end_src
|
|
270
333
|
|
|
334
|
+
*** =site=
|
|
335
|
+
|
|
336
|
+
The configured =site=, without a trailing slash, or =undefined=. See [[*Site URL][Site URL]].
|
|
337
|
+
|
|
271
338
|
*** Aliases
|
|
272
339
|
|
|
273
340
|
For Astro familiarity:
|
|
@@ -285,7 +352,7 @@ interface ContentEntry {
|
|
|
285
352
|
path: string // e.g., 'writing' or 'content/writing/2025'
|
|
286
353
|
filePath: string // absolute source file path
|
|
287
354
|
ext: 'org' | 'tsx' | 'jsx' // file extension
|
|
288
|
-
data: Record<string, unknown> //
|
|
355
|
+
data: Record<string, unknown> // org keywords, or TSX/JSX named exports
|
|
289
356
|
}
|
|
290
357
|
#+end_src
|
|
291
358
|
|
|
@@ -311,7 +378,48 @@ This becomes:
|
|
|
311
378
|
}
|
|
312
379
|
#+end_src
|
|
313
380
|
|
|
314
|
-
For =.tsx= and =.jsx= files,
|
|
381
|
+
For =.tsx= and =.jsx= files, =data= holds the page's named exports whose values are literals: strings, numbers, booleans, =null=, templates without =${}=, and arrays of those. Other exports (functions, computed values) are skipped, since pages aren't run to read them.
|
|
382
|
+
|
|
383
|
+
#+begin_src tsx
|
|
384
|
+
export const title = 'About'
|
|
385
|
+
export const date = '2025-01-15'
|
|
386
|
+
export const tags = ['meta', 'site']
|
|
387
|
+
|
|
388
|
+
export default function About() {
|
|
389
|
+
return <h1>{title}</h1>
|
|
390
|
+
}
|
|
391
|
+
#+end_src
|
|
392
|
+
|
|
393
|
+
** Drafts
|
|
394
|
+
|
|
395
|
+
Mark a page as a draft with =#+draft: t= (or =true=, =yes=), or in a TSX/JSX
|
|
396
|
+
page with =export const draft = true=. The dev server
|
|
397
|
+
serves drafts so you can preview them; the build leaves them out entirely: no
|
|
398
|
+
HTML, and no entry in =getPages=, so feeds, sitemaps, and index pages skip
|
|
399
|
+
them without filtering.
|
|
400
|
+
|
|
401
|
+
** Site URL
|
|
402
|
+
|
|
403
|
+
Set =site= in the config to the absolute URL the site is served from. It is
|
|
404
|
+
exported from =orga-build:content= and passed to endpoints as =ctx.site=,
|
|
405
|
+
without a trailing slash, so a page's absolute URL is =site + page.slug=:
|
|
406
|
+
|
|
407
|
+
#+begin_src tsx
|
|
408
|
+
// _layout.tsx
|
|
409
|
+
import { site } from 'orga-build:content'
|
|
410
|
+
|
|
411
|
+
export default function Layout({ slug, children }) {
|
|
412
|
+
const share = 'https://bsky.app/intent/compose?text=' + encodeURIComponent(site + slug)
|
|
413
|
+
return (
|
|
414
|
+
<>
|
|
415
|
+
{children}
|
|
416
|
+
<a href={share}>Share</a>
|
|
417
|
+
</>
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
#+end_src
|
|
421
|
+
|
|
422
|
+
If the site is served under a path, include it: =site: 'https://example.com/blog'=.
|
|
315
423
|
|
|
316
424
|
** Path Matching Behavior
|
|
317
425
|
|
|
@@ -338,9 +446,7 @@ Path matching works hierarchically:
|
|
|
338
446
|
import { getPages } from 'orga-build:content'
|
|
339
447
|
|
|
340
448
|
export default function BlogIndex() {
|
|
341
|
-
const posts = getPages('writing',
|
|
342
|
-
return entry.data['draft'] !== 'true'
|
|
343
|
-
}).sort((a, b) => {
|
|
449
|
+
const posts = getPages('writing').sort((a, b) => {
|
|
344
450
|
// Sort by date descending
|
|
345
451
|
return String(b.data['date']).localeCompare(String(a.data['date']))
|
|
346
452
|
})
|
|
@@ -390,12 +496,155 @@ export default function Post({ slug }: { slug: string }) {
|
|
|
390
496
|
}
|
|
391
497
|
#+end_src
|
|
392
498
|
|
|
393
|
-
*
|
|
499
|
+
* Feeds and Sitemaps
|
|
394
500
|
|
|
395
|
-
|
|
501
|
+
There is no built-in feed or sitemap. Write them as [[*Endpoint Routes][endpoints]] built on =getPages=, and adapt them to your site. Both need =site= for absolute URLs. Drafts are already left out of =getPages= in the build.
|
|
396
502
|
|
|
397
|
-
|
|
398
|
-
|
|
503
|
+
** RSS Feed
|
|
504
|
+
|
|
505
|
+
This feed lists the pages under =writing/= that have a =#+date:=, newest first. It reads the =YYYY-MM-DD= part of the date, so both =2025-01-15= and =<2025-01-15 Wed>= work.
|
|
506
|
+
|
|
507
|
+
#+begin_src ts
|
|
508
|
+
// rss.xml.ts
|
|
509
|
+
import { getPages, site } from 'orga-build:content'
|
|
510
|
+
|
|
511
|
+
const escape = (value: unknown) =>
|
|
512
|
+
String(value).replace(/[<>&'"]/g, (c) => `&#${c.charCodeAt(0)};`)
|
|
513
|
+
|
|
514
|
+
export function GET() {
|
|
515
|
+
if (!site) throw new Error('Set "site" in orga.config.js to build the feed')
|
|
516
|
+
|
|
517
|
+
const posts = getPages('writing')
|
|
518
|
+
.flatMap((page) => {
|
|
519
|
+
const date = String(page.data['date'] ?? '').match(/\d{4}-\d{2}-\d{2}/)
|
|
520
|
+
return date ? [{ page, date: date[0] }] : []
|
|
521
|
+
})
|
|
522
|
+
.sort((a, b) => b.date.localeCompare(a.date))
|
|
523
|
+
|
|
524
|
+
const items = posts.map(({ page, date }) => {
|
|
525
|
+
const url = escape(site + page.slug)
|
|
526
|
+
return `
|
|
527
|
+
<item>
|
|
528
|
+
<title>${escape(page.data['title'] ?? page.id)}</title>
|
|
529
|
+
<link>${url}</link>
|
|
530
|
+
<guid>${url}</guid>
|
|
531
|
+
<pubDate>${new Date(date).toUTCString()}</pubDate>
|
|
532
|
+
</item>`
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
return new Response(
|
|
536
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
537
|
+
<rss version="2.0">
|
|
538
|
+
<channel>
|
|
539
|
+
<title>My Site</title>
|
|
540
|
+
<link>${escape(site + '/')}</link>
|
|
541
|
+
<description>Latest writing</description>${items.join('')}
|
|
542
|
+
</channel>
|
|
543
|
+
</rss>
|
|
544
|
+
`,
|
|
545
|
+
{ headers: { 'content-type': 'application/xml; charset=utf-8' } }
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
#+end_src
|
|
549
|
+
|
|
550
|
+
Feed readers look for the feed in the page's =<head>=. Layouts render inside =<body>=, so add the link to the =<head>= of your own [[*Default HTML Template][index.html]] instead, with the feed's full URL (=site= + =/rss.xml=):
|
|
551
|
+
|
|
552
|
+
#+begin_src html
|
|
553
|
+
<link rel="alternate" type="application/rss+xml" href="https://example.com/rss.xml" />
|
|
554
|
+
#+end_src
|
|
555
|
+
|
|
556
|
+
** Sitemap
|
|
557
|
+
|
|
558
|
+
#+begin_src ts
|
|
559
|
+
// sitemap.xml.ts
|
|
560
|
+
import { getPages, site } from 'orga-build:content'
|
|
561
|
+
|
|
562
|
+
const escape = (value: unknown) =>
|
|
563
|
+
String(value).replace(/[<>&'"]/g, (c) => `&#${c.charCodeAt(0)};`)
|
|
564
|
+
|
|
565
|
+
export function GET() {
|
|
566
|
+
if (!site) throw new Error('Set "site" in orga.config.js to build the sitemap')
|
|
567
|
+
|
|
568
|
+
const urls = getPages().map(
|
|
569
|
+
(page) => `
|
|
570
|
+
<url><loc>${escape(site + page.slug)}</loc></url>`
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
return new Response(
|
|
574
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
575
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join('')}
|
|
576
|
+
</urlset>
|
|
577
|
+
`,
|
|
578
|
+
{ headers: { 'content-type': 'application/xml; charset=utf-8' } }
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
#+end_src
|
|
582
|
+
|
|
583
|
+
** robots.txt
|
|
584
|
+
|
|
585
|
+
A small endpoint, which can also point crawlers at the sitemap:
|
|
586
|
+
|
|
587
|
+
#+begin_src ts
|
|
588
|
+
// robots.txt.ts
|
|
589
|
+
import { site } from 'orga-build:content'
|
|
590
|
+
|
|
591
|
+
export function GET() {
|
|
592
|
+
if (!site) throw new Error('Set "site" in orga.config.js to build robots.txt')
|
|
593
|
+
return new Response(`User-agent: *\nAllow: /\nSitemap: ${site}/sitemap.xml\n`)
|
|
594
|
+
}
|
|
595
|
+
#+end_src
|
|
596
|
+
|
|
597
|
+
* Search with Pagefind
|
|
598
|
+
|
|
599
|
+
The build output is plain static HTML, so [[https://pagefind.app][Pagefind]] can index it without any orga-build integration. Install it and run it on =outDir= (default =.out=) after each build:
|
|
600
|
+
|
|
601
|
+
#+begin_src bash
|
|
602
|
+
npm install -D pagefind
|
|
603
|
+
#+end_src
|
|
604
|
+
|
|
605
|
+
#+begin_src json
|
|
606
|
+
{
|
|
607
|
+
"scripts": {
|
|
608
|
+
"build": "orga-build && pagefind --site .out"
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
#+end_src
|
|
612
|
+
|
|
613
|
+
Pagefind writes its index and UI to =.out/pagefind/=. Pagefind already skips =<nav>= and =<footer>= elements. To also leave out headers, sidebars and other page chrome, mark the content area with =data-pagefind-body= in your root =_layout.jsx=, and Pagefind indexes only that. Then add the search UI from the same layout. It is made of web components, so it doesn't need an island:
|
|
614
|
+
|
|
615
|
+
#+begin_src jsx
|
|
616
|
+
// pages/_layout.jsx
|
|
617
|
+
export default function Layout({ children }) {
|
|
618
|
+
return (
|
|
619
|
+
<>
|
|
620
|
+
<header>
|
|
621
|
+
<pagefind-modal-trigger></pagefind-modal-trigger>
|
|
622
|
+
<pagefind-modal></pagefind-modal>
|
|
623
|
+
</header>
|
|
624
|
+
<main data-pagefind-body>{children}</main>
|
|
625
|
+
<link href="/pagefind/pagefind-component-ui.css" rel="stylesheet" />
|
|
626
|
+
<script src="/pagefind/pagefind-component-ui.js" type="module"></script>
|
|
627
|
+
</>
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
#+end_src
|
|
631
|
+
|
|
632
|
+
Pages without =data-pagefind-body= are left out of the index, so put it in a layout that wraps every page you want searchable.
|
|
633
|
+
|
|
634
|
+
The index only exists after a build, so search doesn't work in =orga-build dev=. To try it locally, run =npx pagefind --site .out --serve= after building.
|
|
635
|
+
|
|
636
|
+
In a =_layout.tsx=, declare the elements so TypeScript accepts them:
|
|
637
|
+
|
|
638
|
+
#+begin_src tsx
|
|
639
|
+
declare module 'react' {
|
|
640
|
+
namespace JSX {
|
|
641
|
+
interface IntrinsicElements {
|
|
642
|
+
'pagefind-modal-trigger': React.HTMLAttributes<HTMLElement>
|
|
643
|
+
'pagefind-modal': React.HTMLAttributes<HTMLElement>
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
#+end_src
|
|
399
648
|
|
|
400
649
|
* License
|
|
401
650
|
|
package/cli.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import path from 'node:path'
|
|
3
4
|
import { argv } from 'node:process'
|
|
4
5
|
import { parseArgs } from 'node:util'
|
|
5
6
|
import { build } from './lib/build.js'
|
|
6
7
|
import { loadConfig } from './lib/config.js'
|
|
7
8
|
import { serve } from './lib/serve.js'
|
|
8
9
|
|
|
9
|
-
const { positionals } = parseArgs({
|
|
10
|
+
const { values, positionals } = parseArgs({
|
|
10
11
|
args: argv.slice(2),
|
|
11
12
|
options: {
|
|
12
|
-
|
|
13
|
-
outDir: { type: 'string', short: 'o', default: '.out' }
|
|
13
|
+
outDir: { type: 'string', short: 'o' }
|
|
14
14
|
},
|
|
15
|
-
tokens: true,
|
|
16
15
|
allowPositionals: true
|
|
17
16
|
})
|
|
18
17
|
|
|
@@ -20,6 +19,7 @@ const { config, projectRoot } = await loadConfig(
|
|
|
20
19
|
'orga.config.js',
|
|
21
20
|
'orga.config.mjs'
|
|
22
21
|
)
|
|
22
|
+
if (values.outDir) config.outDir = path.resolve(values.outDir)
|
|
23
23
|
|
|
24
24
|
await (positionals.includes('dev')
|
|
25
25
|
? serve(config, 3000, projectRoot)
|