Skip to content

Commit 2e4fa11

Browse files
authored
Merge pull request #10 from PureStackStudio/improve-hooks
Improve existing hooks.
2 parents f74becd + 5673f25 commit 2e4fa11

6 files changed

Lines changed: 216 additions & 110 deletions

File tree

‎packages/ts-ssg/README.md‎

Lines changed: 76 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ const contentDir = path.join(rootDir, 'content')
2828
const outDir = path.join(rootDir, 'dist', 'site')
2929

3030
await buildSite({
31-
rootDir,
32-
contentDir,
33-
outDir,
34-
siteTitle: 'My Docs',
31+
siteConfig: {
32+
rootDir,
33+
contentDir,
34+
outDir,
35+
siteTitle: 'My Docs',
36+
},
3537
})
3638
```
3739

@@ -42,9 +44,13 @@ import path from 'node:path'
4244
import { startDevServer } from '@purestack/ts-ssg'
4345

4446
await startDevServer({
45-
rootDir: process.cwd(),
46-
contentDir: path.join(process.cwd(), 'content'),
47-
outDir: path.join(process.cwd(), 'dist', 'site'),
47+
build: {
48+
siteConfig: {
49+
rootDir: process.cwd(),
50+
contentDir: path.join(process.cwd(), 'content'),
51+
outDir: path.join(process.cwd(), 'dist', 'site'),
52+
},
53+
},
4854
host: '127.0.0.1',
4955
port: 4173,
5056
})
@@ -306,57 +312,42 @@ Built-in templates:
306312
- `doc` (default)
307313
- `splash`
308314

309-
Provide custom templates via `buildSite({ templates })`:
315+
Provide custom templates through `options.templates` and select one with the page's `template` frontmatter:
310316

311317
```ts
312318
import { h, type TSNode } from '@purestack/ts-html'
313-
import type { PageTemplateMap } from '@purestack/ts-ssg'
319+
import { buildSite, type PageTemplateMap } from '@purestack/ts-ssg'
314320

315321
const templates: PageTemplateMap = {
316-
product: ({ head, bodyHtml }) =>
322+
product: ({ head, bodyHtml, headerHtml, footerHtml }) =>
317323
h('html').push(
318324
head,
319-
h('body').push(h('main').attr({ class: 'product' }).raw(bodyHtml)),
325+
h('body').push(
326+
h('').raw(headerHtml ?? ''),
327+
h('main').attr({ class: 'product' }).raw(bodyHtml),
328+
h('').raw(footerHtml ?? ''),
329+
),
320330
) as TSNode<'html'>,
321331
}
332+
333+
await buildSite({ siteConfig: { contentDir: './content' }, options: { templates } })
322334
```
323335

336+
A template receives a `PageTemplateInput`: the prepared `head`, the compiled `bodyHtml`, the page's nearest `headerHtml` and `footerHtml`, `site` config, `navigation`, `outline`, and `pageInfo`, whose `frontmatter` keeps any custom fields the page defines.
337+
324338
## Components and Regor MDX
325339

326-
Built-in component sets are initialized automatically each build:
327-
328-
- alert: `alertBox`
329-
- card grid: `card`, `cardGrid`
330-
- consent: `consent`
331-
- contact: `contactForm`
332-
- expandable panel: `expandablePanel`
333-
- panel: `panel`
334-
- footer: `siteFooter`
335-
- top bar: `topBar`
336-
- logo: `siteLogo`
337-
- navigation: `navMenu`, `navList`, `navItem`
338-
- page links: `pageLinks`
339-
- page toc: `pageToc`
340-
- pricing: `pricingTable`, `pricingPlan`, `pricingFeature`
341-
- search: `searchBox`
342-
- tabs: `tabs`, `tabPane`
343-
- theme toggle: `themeToggle`
344-
345-
Important rendering constraint: Regor components are rendered statically. Component state/events are not runtime-hydrated.
340+
Every component in `@purestack/ts-components` is registered for each build, so content can use them directly. The [component reference](https://purestack.studio/components/) documents each one.
346341

347-
## Theming
342+
Register your own Regor components through `options.components`, or assign `context.components` in the `onConfigResolved` hook.
348343

349-
Built-in skins export:
344+
Components render to static HTML at build time. For behavior in the browser, load a page script with `PageScript` or mount a browser-side Regor app with `RegorApp`.
350345

351-
- `standard`
346+
## Theming
352347

353-
Theme utilities:
348+
The built-in skin is `standard`. Select a skin with `style.theme.skin` in `siteConfig.json`.
354349

355-
- `builtInSkins`
356-
- `themes.resolve(...)`
357-
- `themes.setOptions(...)`
358-
- `themes.getOptions()`
359-
- `styleBuilder`
350+
Skins come from `@purestack/ts-style`: `themeSkins` holds the registered skins, and `registerSkin(name, skin)` adds your own before the build starts. See the [Themes guide](https://purestack.studio/guides/themes/) for creating one.
360351

361352
`style.themes` controls generated files:
362353

@@ -370,27 +361,45 @@ Theme utilities:
370361
- Regor MDX (`.mdx`, `.rmdx`): `remark-parse` + `remark-gfm` with Regor component markup preservation
371362
- HTML output via HAST + rehype
372363
- H2/H3 outline extraction for page TOC
373-
- Optional Shiki highlighting
364+
- Code highlighting with highlight.js or Shiki
374365

375-
`BuildInput.mdx` options:
366+
`siteConfig.mdx` options:
376367

377-
- `highlighter`: custom highlighter (`codeToHtml`)
378-
- `themes`: `{ light, dark }` for Shiki
379-
- `langs`: language list for Shiki
368+
- `highlighter`: `"highlightjs"` (default) or `"shiki"`
380369
- `disableHighlighter`: skip highlighting
370+
- `compileMdAsMdx`: compile `.md` files as Regor MDX (default `true`); set `false` to keep `.md` as plain Markdown
381371

382372
## Build Hooks
383373

384-
Hook into build lifecycle with `BuildHooks`:
374+
Pass `BuildHooks` through `options.hooks` to `buildSite` or `startDevServer`. Hooks, templates, and components need the programmatic API; the CLI reads only `siteConfig.json`.
375+
376+
```ts
377+
import { type BuildHooks, startDevServer } from '@purestack/ts-ssg'
385378

386-
- `onConfigResolved`
387-
- `onContentDiscovered`
388-
- `onNavigationBuilt`
389-
- `onPageStart`
390-
- `onPageRendered`
391-
- `onPageWritten`
392-
- `onStylesWritten`
393-
- `onBuildComplete`
379+
const hooks: BuildHooks = {
380+
onPageRendered(context, page) {
381+
page.html = page.html.replace('</body>', '<!-- PureStack --></body>')
382+
},
383+
}
384+
385+
await startDevServer({ build: { siteConfig: { contentDir: './content' }, options: { hooks } } })
386+
```
387+
388+
Every page render, in a full build or a dev server re-render, runs the page hooks:
389+
390+
- `onPageStart(context, file)`: before the page renders.
391+
- `onPageRendered(context, page)`: after it renders; changes to `page.html` are written.
392+
- `onPageWritten(context, page)`: after the HTML file is written.
393+
394+
Each full build also runs, in order:
395+
396+
- `onConfigResolved(context)`: before any output; register components on `context.components` here.
397+
- `onContentDiscovered(context, files)`: after content discovery, before pages render.
398+
- `onNavigationBuilt(context, navigation)`
399+
- `onStylesWritten(context, result)`
400+
- `onBuildComplete(context, result)`
401+
402+
The dev server runs a full build when it starts and when `siteConfig.json` changes; other edits re-render only the affected pages.
394403

395404
Build option:
396405

@@ -551,27 +560,25 @@ Behavior:
551560

552561
## API Surface
553562

554-
Primary exports:
563+
Functions:
555564

556-
- `buildSite`
557-
- `startDevServer`
558-
- `resolveConfig` (`resolveSiteConfig`)
565+
- `buildSite`, `startDevServer`, `runCli`
566+
- `resolveSiteConfig`
559567
- `normalizeFrontmatter`, `parseFrontmatterSource`
560568
- `buildNavigation`, `resolveNavigationConfig`, `resolvePageNavigation`
561-
- `createMdxHighlighter`
562-
- `componentRegistry`
569+
- `createMdxHighlighter`, `DEFAULT_MDX_CODE_LANGS`, `DEFAULT_MDX_CODE_THEMES`
563570
- `defaultTemplates`, `resolvePageTemplate`
564-
- `builtInSkins`, `themes`, `styleBuilder`
565571

566-
Useful types:
572+
Types:
573+
574+
- Build: `BuildInput`, `BuildOptions`, `BuildResult`, `BuildCountSummary`, `PublishOptions`
575+
- Hooks: `BuildHooks`, `BuildContext`, `ResolvedContentFile`, `PageRenderResult`, `NavigationTree`, `WriteStylesResult`
576+
- Templates: `PageTemplateMap`, `PageTemplate`, `PageTemplateInput`, `PageInfo`, `PageFrontmatter`
577+
- Config and components: `SiteConfig`, `SiteConfigInput`, `TsSsgContext`
578+
- Dev server: `DevServerInput`, `DevServerOptions`, `DevServerHandle`
579+
- Highlighting: `MdxCodeHighlighter`, `MdxCodeLangs`, `MdxCodeThemes`
567580

568-
- `BuildInput`, `BuildResult`, `BuildHooks`
569-
- `SiteConfig`, `PartialConfig`
570-
- `DevServerInput`, `DevServerHandle`
571-
- `PageFrontmatter`
572-
- `NavigationConfig`, `NavigationTree`, `NavItem`
573-
- `ThemeOptions`, `ThemePalette`
574-
- `TsSsgContext`
581+
Related packages: `@purestack/ts-html` builds template markup (`h`), `@purestack/ts-style` provides skins (`registerSkin`, `themeSkins`), and `@purestack/ts-components` provides the built-in components.
575582

576583
## Build Result Shape
577584

‎packages/ts-ssg/src/build/incremental/content-state.ts‎

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
import { resolveOutPath } from '../out-path'
1717
import {
1818
type BuildContext,
19-
buildPage,
2019
renderPageFromFile,
2120
resolveHeaderFooterHtml,
2221
resolvePagePartials,
@@ -36,6 +35,7 @@ import type { IncrementalBuildResult } from './types'
3635
interface IncrementalContentStateInput {
3736
config: SiteConfig
3837
context: BuildContext
38+
hooks: BuildHooks
3939
log: Logger
4040
onPageBuilt: (relPath: string, scriptEntrypoints: string[]) => void
4141
persistManifest: () => Promise<void>
@@ -86,32 +86,41 @@ export class IncrementalContentState {
8686
return count
8787
}
8888

89-
async renderAllPages(contentFiles: ResolvedContentFile[], hooks: BuildHooks) {
89+
async renderAllPages(contentFiles: ResolvedContentFile[]) {
9090
let pages = 0
9191
for (const file of contentFiles) {
92-
try {
93-
await hooks.onPageStart?.(this.input.context, file)
94-
const page = await renderPageFromFile(this.input.context, file)
95-
this.input.onPageBuilt(file.relPath, page.scriptEntrypoints)
96-
await hooks.onPageRendered?.(this.input.context, page)
97-
await writePage(page, this.input.config.html.minify)
98-
await hooks.onPageWritten?.(this.input.context, page)
99-
pages += 1
100-
} catch (error) {
101-
if (this.input.context.writeErrorPages !== true) {
102-
throw error
103-
}
104-
this.input.log.error('page build failed', {
105-
relPath: file.relPath,
106-
error: error instanceof Error ? error.message : String(error),
107-
})
108-
const page = await writePageError(this.input.context, file, error)
109-
this.input.onPageBuilt(file.relPath, page.scriptEntrypoints)
110-
}
92+
if (await this.writePageWithHooks(file)) pages += 1
11193
}
11294
return pages
11395
}
11496

97+
/**
98+
* Renders and writes one page inside its page hooks. Full builds and
99+
* incremental renders both come through here, so `build` and `serve`
100+
* produce the same output. Returns false when an error page was written.
101+
*/
102+
private async writePageWithHooks(file: ResolvedContentFile) {
103+
const { context, hooks, config } = this.input
104+
try {
105+
await hooks.onPageStart?.(context, file)
106+
const page = await renderPageFromFile(context, file)
107+
this.input.onPageBuilt(file.relPath, page.scriptEntrypoints)
108+
await hooks.onPageRendered?.(context, page)
109+
await writePage(page, config.html.minify)
110+
await hooks.onPageWritten?.(context, page)
111+
return true
112+
} catch (error) {
113+
if (context.writeErrorPages !== true) throw error
114+
this.input.log.error('page build failed', {
115+
relPath: file.relPath,
116+
error: error instanceof Error ? error.message : String(error),
117+
})
118+
const page = await writePageError(context, file, error)
119+
this.input.onPageBuilt(file.relPath, page.scriptEntrypoints)
120+
return false
121+
}
122+
}
123+
115124
async renderIfDirtyByOutPath(outPath: string) {
116125
const relPath = this.contentIndex.getRelPathByOutPath(outPath)
117126
if (!relPath) return false
@@ -218,8 +227,7 @@ export class IncrementalContentState {
218227
const contentFile =
219228
contentFiles.find((file) => file.relPath === relPath) ??
220229
this.toResolvedContentFile(relPath, ext)
221-
const page = await buildPage(this.input.context, contentFile)
222-
this.input.onPageBuilt(contentFile.relPath, page.scriptEntrypoints)
230+
await this.writePageWithHooks(contentFile)
223231
this.upsertContentManifestEntry(relPath, contentFile.ext, signature)
224232
result.changedPages += 1
225233
this.dirtyPages.delete(relPath)
@@ -228,17 +236,15 @@ export class IncrementalContentState {
228236
async rebuildSingleContent(input: RebuildSingleContentInput) {
229237
const { relPath, ext, signature, result } = input
230238
const contentFile = this.toResolvedContentFile(relPath, ext)
231-
const page = await buildPage(this.input.context, contentFile)
232-
this.input.onPageBuilt(contentFile.relPath, page.scriptEntrypoints)
239+
await this.writePageWithHooks(contentFile)
233240
this.upsertContentManifestEntry(relPath, contentFile.ext, signature)
234241
result.changedPages += 1
235242
}
236243

237244
async renderAndPersistRelPath(relPath: string, signature: FileSignature) {
238245
const ext = this.resolveContentExt(relPath)
239246
const contentFile = this.toResolvedContentFile(relPath, ext)
240-
const page = await buildPage(this.input.context, contentFile)
241-
this.input.onPageBuilt(contentFile.relPath, page.scriptEntrypoints)
247+
await this.writePageWithHooks(contentFile)
242248
this.upsertContentManifestEntry(relPath, contentFile.ext, signature)
243249
this.dirtyPages.delete(relPath)
244250
await this.input.persistManifest()

0 commit comments

Comments
 (0)