orga-build 0.8.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 +296 -41
- package/cli.js +4 -4
- package/lib/__tests__/build.test.js +444 -23
- 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 +12 -176
- package/lib/config.d.ts +9 -3
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +14 -18
- 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 +10 -3
- package/lib/files.d.ts.map +1 -1
- package/lib/files.js +111 -23
- 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 +25 -32
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +130 -170
- 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 +7 -6
- package/lib/vite.d.ts.map +1 -1
- package/lib/vite.js +75 -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,45 +60,64 @@ 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
|
-
// Global
|
|
82
|
-
//
|
|
83
|
-
styles: ['/style.css'],
|
|
91
|
+
// Global stylesheets — paths relative to orga.config.js (leading / optional).
|
|
92
|
+
// Linked from the HTML shell; Vite processes them (with HMR in dev).
|
|
93
|
+
styles: ['pages/style.css'],
|
|
84
94
|
|
|
85
95
|
// Extra rehype plugins appended to orga-build defaults
|
|
86
96
|
// Useful for syntax highlighting (e.g. rehype-pretty-code).
|
|
87
97
|
rehypePlugins: [],
|
|
88
98
|
|
|
89
99
|
// Additional Vite plugins
|
|
90
|
-
vitePlugins: []
|
|
100
|
+
vitePlugins: [],
|
|
101
|
+
|
|
102
|
+
// Glob patterns (relative to root) to exclude from content scanning.
|
|
103
|
+
// Useful for generated or declaration files that must live inside root
|
|
104
|
+
// but should not be treated as pages or endpoints.
|
|
105
|
+
exclude: ['**/*.d.ts']
|
|
91
106
|
}
|
|
92
107
|
#+end_src
|
|
93
108
|
|
|
94
109
|
** Configuration Options
|
|
95
110
|
|
|
96
|
-
| Option | Type | Default | Description
|
|
97
|
-
|
|
98
|
-
| =root= | =string= | ='
|
|
99
|
-
| =outDir= | =string= | ='out'=
|
|
100
|
-
| =
|
|
101
|
-
| =
|
|
102
|
-
| =
|
|
103
|
-
| =
|
|
111
|
+
| Option | Type | Default | Description |
|
|
112
|
+
|----------------+-------------------+---------+-----------------------------------------------------------------|
|
|
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]] |
|
|
116
|
+
| =containerClass= | =string \vert string[]= | =[]= | CSS class(es) for content wrapper |
|
|
117
|
+
| =styles= | =string[]= | =[]= | Stylesheets to link; paths relative to =orga.config.js= |
|
|
118
|
+
| =exclude= | =string[]= | =[]= | Glob patterns (relative to =root=) excluded from content scanning |
|
|
119
|
+
| =rehypePlugins= | =PluggableList= | =[]= | Extra rehype plugins appended to orga-build defaults |
|
|
120
|
+
| =vitePlugins= | =PluginOption[]= | =[]= | Additional Vite plugins |
|
|
104
121
|
|
|
105
122
|
** Syntax Highlighting Example
|
|
106
123
|
|
|
@@ -126,7 +143,7 @@ Page routes are discovered from =.org=, =.tsx=, and =.jsx= files.
|
|
|
126
143
|
|
|
127
144
|
At build time, page routes are emitted as HTML:
|
|
128
145
|
|
|
129
|
-
- =/about= ->
|
|
146
|
+
- =/about= -> =.out/about/index.html=
|
|
130
147
|
|
|
131
148
|
** Endpoint Routes
|
|
132
149
|
|
|
@@ -146,13 +163,65 @@ export async function GET(ctx) {
|
|
|
146
163
|
}
|
|
147
164
|
#+end_src
|
|
148
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
|
+
|
|
149
173
|
At build time, endpoint routes are emitted to exact filenames:
|
|
150
174
|
|
|
151
|
-
- =/rss.xml= ->
|
|
152
|
-
- =/api/data.json= ->
|
|
175
|
+
- =/rss.xml= -> =.out/rss.xml=
|
|
176
|
+
- =/api/data.json= -> =.out/api/data.json=
|
|
153
177
|
|
|
154
178
|
Route conflicts (same final route path) fail fast during dev/build startup.
|
|
155
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
|
+
|
|
156
225
|
* TypeScript Setup
|
|
157
226
|
|
|
158
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.
|
|
@@ -218,9 +287,9 @@ const writing = getPages('writing')
|
|
|
218
287
|
// Get entries in a nested path
|
|
219
288
|
const posts2025 = getPages('content/writing/2025')
|
|
220
289
|
|
|
221
|
-
// Filter
|
|
222
|
-
const
|
|
223
|
-
return entry.data['
|
|
290
|
+
// Filter by metadata
|
|
291
|
+
const featured = getPages('writing', (entry) => {
|
|
292
|
+
return entry.data['featured'] === 'true'
|
|
224
293
|
})
|
|
225
294
|
#+end_src
|
|
226
295
|
|
|
@@ -262,6 +331,10 @@ const entries = getEntries([
|
|
|
262
331
|
])
|
|
263
332
|
#+end_src
|
|
264
333
|
|
|
334
|
+
*** =site=
|
|
335
|
+
|
|
336
|
+
The configured =site=, without a trailing slash, or =undefined=. See [[*Site URL][Site URL]].
|
|
337
|
+
|
|
265
338
|
*** Aliases
|
|
266
339
|
|
|
267
340
|
For Astro familiarity:
|
|
@@ -279,7 +352,7 @@ interface ContentEntry {
|
|
|
279
352
|
path: string // e.g., 'writing' or 'content/writing/2025'
|
|
280
353
|
filePath: string // absolute source file path
|
|
281
354
|
ext: 'org' | 'tsx' | 'jsx' // file extension
|
|
282
|
-
data: Record<string, unknown> //
|
|
355
|
+
data: Record<string, unknown> // org keywords, or TSX/JSX named exports
|
|
283
356
|
}
|
|
284
357
|
#+end_src
|
|
285
358
|
|
|
@@ -305,7 +378,48 @@ This becomes:
|
|
|
305
378
|
}
|
|
306
379
|
#+end_src
|
|
307
380
|
|
|
308
|
-
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'=.
|
|
309
423
|
|
|
310
424
|
** Path Matching Behavior
|
|
311
425
|
|
|
@@ -332,9 +446,7 @@ Path matching works hierarchically:
|
|
|
332
446
|
import { getPages } from 'orga-build:content'
|
|
333
447
|
|
|
334
448
|
export default function BlogIndex() {
|
|
335
|
-
const posts = getPages('writing',
|
|
336
|
-
return entry.data['draft'] !== 'true'
|
|
337
|
-
}).sort((a, b) => {
|
|
449
|
+
const posts = getPages('writing').sort((a, b) => {
|
|
338
450
|
// Sort by date descending
|
|
339
451
|
return String(b.data['date']).localeCompare(String(a.data['date']))
|
|
340
452
|
})
|
|
@@ -384,12 +496,155 @@ export default function Post({ slug }: { slug: string }) {
|
|
|
384
496
|
}
|
|
385
497
|
#+end_src
|
|
386
498
|
|
|
387
|
-
*
|
|
499
|
+
* Feeds and Sitemaps
|
|
500
|
+
|
|
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.
|
|
502
|
+
|
|
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.
|
|
388
635
|
|
|
389
|
-
|
|
636
|
+
In a =_layout.tsx=, declare the elements so TypeScript accepts them:
|
|
390
637
|
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
393
648
|
|
|
394
649
|
* License
|
|
395
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)
|