methanol 0.0.19 → 0.0.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "methanol",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "Static site generator powered by rEFui and MDX",
5
5
  "main": "./index.js",
6
6
  "type": "module",
@@ -26,6 +26,7 @@ import { VitePWA } from 'vite-plugin-pwa'
26
26
  import { state, cli } from './state.js'
27
27
  import { resolveUserViteConfig } from './config.js'
28
28
  import { buildPagesContext } from './pages.js'
29
+ import { selectFeedPages } from './feed.js'
29
30
  import { buildComponentRegistry } from './components.js'
30
31
  import { createBuildWorkers, runWorkerStage, terminateWorkers } from './workers/build-pool.js'
31
32
  import { methanolVirtualHtmlPlugin, methanolResolverPlugin } from './vite-plugins.js'
@@ -98,6 +99,10 @@ export const buildHtmlEntries = async () => {
98
99
  const { workers, assignments } = createBuildWorkers(totalPages)
99
100
  const excludedRoutes = Array.from(pagesContext.excludedRoutes || [])
100
101
  const excludedDirs = Array.from(pagesContext.excludedDirs || [])
102
+ const rssContent = new Map()
103
+ const intermediateOutputs = []
104
+ let feedIds = []
105
+ let feedAssignments = null
101
106
  try {
102
107
  await runWorkerStage({
103
108
  workers,
@@ -136,6 +141,10 @@ export const buildHtmlEntries = async () => {
136
141
  })
137
142
  stageLogger.end(compileToken)
138
143
 
144
+ const titleUpdates = updates
145
+ .filter((update) => update && update.title !== undefined)
146
+ .map((update) => ({ id: update.id, title: update.title }))
147
+
139
148
  for (const update of updates) {
140
149
  const page = pages[update.id]
141
150
  if (!page) continue
@@ -149,7 +158,6 @@ export const buildHtmlEntries = async () => {
149
158
  pagesContext.refreshPagesTree?.()
150
159
  state.PAGES_CONTEXT = pagesContext
151
160
 
152
- const titleSnapshot = pages.map((page) => page.title)
153
161
  await runWorkerStage({
154
162
  workers,
155
163
  stage: 'sync',
@@ -158,15 +166,25 @@ export const buildHtmlEntries = async () => {
158
166
  message: {
159
167
  type: 'sync',
160
168
  stage: 'sync',
161
- updates,
162
- titles: titleSnapshot
169
+ updates: titleUpdates
163
170
  }
164
171
  }))
165
172
  })
173
+ if (state.RSS_ENABLED) {
174
+ const feedPages = selectFeedPages(pages, state.RSS_OPTIONS || {})
175
+ const pageIndex = new Map(pages.map((page, index) => [page, index]))
176
+ feedIds = feedPages.map((page) => pageIndex.get(page)).filter((id) => id != null)
177
+ if (feedIds.length) {
178
+ feedAssignments = Array.from({ length: workers.length }, () => [])
179
+ for (const id of feedIds) {
180
+ feedAssignments[id % workers.length].push(id)
181
+ }
182
+ }
183
+ }
166
184
 
167
185
  const renderToken = stageLogger.start('Rendering pages')
168
186
  completed = 0
169
- const rendered = await runWorkerStage({
187
+ await runWorkerStage({
170
188
  workers,
171
189
  stage: 'render',
172
190
  messages: workers.map((worker, index) => ({
@@ -174,7 +192,8 @@ export const buildHtmlEntries = async () => {
174
192
  message: {
175
193
  type: 'render',
176
194
  stage: 'render',
177
- ids: assignments[index]
195
+ ids: assignments[index],
196
+ feedIds: feedAssignments ? feedAssignments[index] : []
178
197
  }
179
198
  })),
180
199
  onProgress: (count) => {
@@ -182,27 +201,39 @@ export const buildHtmlEntries = async () => {
182
201
  completed = count
183
202
  stageLogger.update(renderToken, `Rendering pages [${completed}/${totalPages}]`)
184
203
  },
185
- collect: (message) => message.results || []
204
+ onResult: (result) => {
205
+ if (!result || typeof result.id !== 'number') return
206
+ const page = pages[result.id]
207
+ if (!page) return
208
+ const html = result.html
209
+ const name = resolveOutputName(page)
210
+ const id = normalizePath(resolve(state.VIRTUAL_HTML_OUTPUT_ROOT, `${name}.html`))
211
+ entry[name] = id
212
+ htmlCache.set(id, html)
213
+ if (state.INTERMEDIATE_DIR) {
214
+ intermediateOutputs.push({ name, id })
215
+ }
216
+ if (result.feedContent != null) {
217
+ const key = page.path || page.routePath
218
+ if (key) {
219
+ rssContent.set(key, result.feedContent || '')
220
+ }
221
+ }
222
+ }
186
223
  })
187
224
  stageLogger.end(renderToken)
188
-
189
- for (const item of rendered) {
190
- const page = pages[item.id]
191
- if (!page) continue
192
- const html = item.html
193
- const name = resolveOutputName(page)
194
- const id = normalizePath(resolve(state.VIRTUAL_HTML_OUTPUT_ROOT, `${name}.html`))
195
- entry[name] = id
196
- htmlCache.set(id, html)
197
- if (state.INTERMEDIATE_DIR) {
198
- const outPath = resolve(state.INTERMEDIATE_DIR, `${name}.html`)
199
- await ensureDir(dirname(outPath))
200
- await writeFile(outPath, html)
201
- }
202
- }
203
225
  } finally {
204
226
  await terminateWorkers(workers)
205
227
  }
228
+ if (state.INTERMEDIATE_DIR) {
229
+ for (const output of intermediateOutputs) {
230
+ const html = htmlCache.get(output.id)
231
+ if (typeof html !== 'string') continue
232
+ const outPath = resolve(state.INTERMEDIATE_DIR, `${output.name}.html`)
233
+ await ensureDir(dirname(outPath))
234
+ await writeFile(outPath, html)
235
+ }
236
+ }
206
237
 
207
238
  const htmlFiles = await collectHtmlFiles(state.PAGES_DIR)
208
239
  const htmlExcludedDirs = pagesContext.excludedDirs || new Set()
@@ -238,7 +269,7 @@ export const buildHtmlEntries = async () => {
238
269
  }
239
270
  }
240
271
 
241
- return { entry, htmlCache, pagesContext }
272
+ return { entry, htmlCache, pagesContext, rssContent }
242
273
  }
243
274
 
244
275
  export const runViteBuild = async (entry, htmlCache) => {
package/src/components.js CHANGED
@@ -52,6 +52,8 @@ export const register = reframeEnv.register
52
52
  export const invalidateRegistryEntry = reframeEnv.invalidate
53
53
  export const genRegistryScript = reframeEnv.genRegistryScript
54
54
  export const resetReframeRenderCount = () => reframeEnv.resetRenderCount()
55
+ export const setReframeHydrationEnabled = (value) => reframeEnv.setHydrationEnabled(value)
56
+ export const getReframeHydrationEnabled = () => reframeEnv.getHydrationEnabled()
55
57
 
56
58
  const resolveComponentExport = (componentPath, exportName, ext) => {
57
59
  const staticCandidate = `${componentPath}.static${ext}`
package/src/config.js CHANGED
@@ -23,6 +23,7 @@ import { existsSync } from 'fs'
23
23
  import { resolve, isAbsolute, extname, basename, dirname } from 'path'
24
24
  import { pathToFileURL, fileURLToPath } from 'url'
25
25
  import { mergeConfig } from 'vite'
26
+ import { isMainThread } from 'worker_threads'
26
27
  import { projectRequire } from './node-loader.js'
27
28
  import { cli, state } from './state.js'
28
29
  import { logger } from './logger.js'
@@ -122,6 +123,7 @@ const normalizeViteBase = (value) => {
122
123
  }
123
124
 
124
125
  const warnDevBase = (value) => {
126
+ if (!isMainThread) return
125
127
  if (devBaseWarningShown) return
126
128
  devBaseWarningShown = true
127
129
  const label = value ? ` (received "${value}")` : ''
@@ -191,6 +193,28 @@ const resolvePagefindBuild = (config) => {
191
193
  return null
192
194
  }
193
195
 
196
+ const resolveFeedEnabled = (config) => {
197
+ if (config?.feed == null) return false
198
+ if (typeof config.feed === 'boolean') return config.feed
199
+ if (typeof config.feed === 'object') {
200
+ if (hasOwn(config.feed, 'enabled')) {
201
+ return config.feed.enabled !== false
202
+ }
203
+ return true
204
+ }
205
+ return false
206
+ }
207
+
208
+ const resolveFeedOptions = (config) => {
209
+ const value = config?.feed
210
+ if (!value || typeof value !== 'object') return null
211
+ const { enabled, ...rest } = value
212
+ if (Object.keys(rest).length) {
213
+ return { ...rest }
214
+ }
215
+ return null
216
+ }
217
+
194
218
  const resolveStarryNightConfig = (value) => {
195
219
  if (value == null) return { enabled: true, options: null }
196
220
  if (typeof value === 'boolean') {
@@ -301,10 +325,15 @@ export const applyConfig = async (config, mode) => {
301
325
  state.ROOT_DIR = root
302
326
  const configSiteName = cli.CLI_SITE_NAME ?? config.site?.name ?? null
303
327
  state.SITE_NAME = configSiteName || basename(root) || 'Methanol Site'
328
+ const configOwner = cli.CLI_OWNER ?? config.site?.owner ?? null
329
+ state.SITE_OWNER = configOwner ? String(configOwner) : null
304
330
  const userSite = config.site && typeof config.site === 'object' ? { ...config.site } : null
305
331
  const siteBase = normalizeSiteBase(cli.CLI_BASE || userSite?.base)
306
332
  state.SITE_BASE = siteBase
307
333
  if (userSite) {
334
+ if (configOwner != null) {
335
+ userSite.owner = String(configOwner)
336
+ }
308
337
  if (siteBase == null) {
309
338
  delete userSite.base
310
339
  } else {
@@ -426,6 +455,21 @@ export const applyConfig = async (config, mode) => {
426
455
  }
427
456
  state.PAGEFIND_OPTIONS = resolvePagefindOptions(config)
428
457
  state.PAGEFIND_BUILD = resolvePagefindBuild(config)
458
+
459
+ state.RSS_ENABLED = resolveFeedEnabled(config)
460
+ if (cli.CLI_RSS !== undefined) {
461
+ state.RSS_ENABLED = cli.CLI_RSS
462
+ }
463
+ state.RSS_OPTIONS = resolveFeedOptions(config)
464
+ if (cli.CLI_RSS !== undefined && cli.CLI_ATOM === undefined) {
465
+ state.RSS_OPTIONS = { ...(state.RSS_OPTIONS || {}), atom: false }
466
+ }
467
+ if (cli.CLI_ATOM !== undefined) {
468
+ if (cli.CLI_ATOM === true) {
469
+ state.RSS_ENABLED = true
470
+ }
471
+ state.RSS_OPTIONS = { ...(state.RSS_OPTIONS || {}), atom: cli.CLI_ATOM }
472
+ }
429
473
 
430
474
  if (hasOwn(config, 'pwa')) {
431
475
  if (config.pwa === true) {
package/src/dev-server.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  } from './components.js'
39
39
  import { buildPagesContext, buildPageEntry, routePathFromFile } from './pages.js'
40
40
  import { compilePageMdx, renderHtml } from './mdx.js'
41
- import { DevErrorPage } from './error-page.jsx'
41
+ import { DevErrorPage } from './templates/error-page.jsx'
42
42
  import { HTMLRenderer } from './renderer.js'
43
43
  import { methanolResolverPlugin } from './vite-plugins.js'
44
44
  import { preparePublicAssets, updateAsset } from './public-assets.js'
@@ -251,6 +251,10 @@ export const runViteDev = async () => {
251
251
  })
252
252
  if (token !== pagesContextToken) return
253
253
 
254
+ const titleUpdates = updates
255
+ .filter((update) => update && update.title !== undefined)
256
+ .map((update) => ({ id: update.id, title: update.title }))
257
+
254
258
  for (const update of updates) {
255
259
  const page = pages[update.id]
256
260
  if (!page) continue
@@ -266,7 +270,6 @@ export const runViteDev = async () => {
266
270
  invalidateHtmlCache()
267
271
  const renderEpoch = htmlCacheEpoch
268
272
 
269
- const titleSnapshot = pages.map((page) => page.title)
270
273
  await runWorkerStage({
271
274
  workers,
272
275
  stage: 'sync',
@@ -275,8 +278,7 @@ export const runViteDev = async () => {
275
278
  message: {
276
279
  type: 'sync',
277
280
  stage: 'sync',
278
- updates,
279
- titles: titleSnapshot
281
+ updates: titleUpdates
280
282
  }
281
283
  }))
282
284
  })
package/src/feed.js ADDED
@@ -0,0 +1,189 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ import { dirname, resolve } from 'path'
22
+ import { mkdir, writeFile } from 'fs/promises'
23
+ import { state } from './state.js'
24
+ import { HTMLRenderer } from './renderer.js'
25
+ import { extractExcerpt } from './text-utils.js'
26
+ import { withBase } from './config.js'
27
+ import { logger } from './logger.js'
28
+ import { setReframeHydrationEnabled, getReframeHydrationEnabled } from './components.js'
29
+ import RssFeed from './templates/rss-feed.jsx'
30
+ import AtomFeed from './templates/atom-feed.jsx'
31
+
32
+ const DEFAULT_RSS_PATH = '/rss.xml'
33
+ const DEFAULT_ATOM_PATH = '/atom.xml'
34
+ const DEFAULT_RSS_LIMIT = 10
35
+
36
+ const isAbsoluteUrl = (value) =>
37
+ typeof value === 'string' && /^https?:\/\//i.test(value.trim())
38
+
39
+ const normalizeFeedPath = (value, isAtom) => {
40
+ if (!value || typeof value !== 'string') return isAtom ? DEFAULT_ATOM_PATH : DEFAULT_RSS_PATH
41
+ const trimmed = value.trim()
42
+ if (!trimmed) return isAtom ? DEFAULT_ATOM_PATH : DEFAULT_RSS_PATH
43
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`
44
+ }
45
+
46
+ const ensureDir = async (dir) => {
47
+ await mkdir(dir, { recursive: true })
48
+ }
49
+
50
+ const resolveFeedLimit = (options) => {
51
+ return typeof options?.limit === 'number' && Number.isFinite(options.limit)
52
+ ? Math.max(0, Math.floor(options.limit))
53
+ : DEFAULT_RSS_LIMIT
54
+ }
55
+
56
+ const wrapCdata = (value) => {
57
+ const text = value == null ? '' : String(value)
58
+ if (!text) return ''
59
+ const trimmed = text.trim()
60
+ if (trimmed.startsWith('<![CDATA[') && trimmed.endsWith(']]>')) {
61
+ return trimmed
62
+ }
63
+ return `<![CDATA[${text.replace(/]]>/g, ']]]]><![CDATA[>')}]]>`
64
+ }
65
+
66
+ const resolveSiteUrl = (options, site) => {
67
+ if (options?.siteUrl) return options.siteUrl
68
+ if (state.SITE_BASE) return state.SITE_BASE
69
+ if (site?.base) return site.base
70
+ return null
71
+ }
72
+
73
+ const buildItem = (page, siteUrl, htmlContent = null, isAtom = false, siteOwner = null) => {
74
+ if (!page) return null
75
+ const href = page.routeHref || withBase(page.routePath)
76
+ if (!href) return null
77
+ const link = new URL(href, siteUrl).href
78
+ const title = page.title || page.name || page.routePath || link
79
+ const description = extractExcerpt(page)
80
+ const contentSource = htmlContent ?? page.content ?? ''
81
+ const content = contentSource
82
+ ? (isAtom
83
+ ? contentSource
84
+ : HTMLRenderer.rawHTML(wrapCdata(contentSource)))
85
+ : null
86
+ const authorValue = page.frontmatter?.author
87
+ const author = Array.isArray(authorValue)
88
+ ? authorValue.filter(Boolean).join(', ')
89
+ : authorValue || siteOwner || null
90
+ const pubDate = page.date ? new Date(page.date).toUTCString() : null
91
+ const updated = page.date ? new Date(page.date).toISOString() : null
92
+ return {
93
+ title,
94
+ link,
95
+ description,
96
+ content,
97
+ author,
98
+ pubDate,
99
+ updated
100
+ }
101
+ }
102
+
103
+ const getSortTime = (page) => {
104
+ const value = page?.date
105
+ if (!value) return 0
106
+ const time = Date.parse(value)
107
+ return Number.isNaN(time) ? 0 : time
108
+ }
109
+
110
+ export const selectFeedPages = (pages, options) => {
111
+ const limit = resolveFeedLimit(options)
112
+ if (!limit) return []
113
+ return (Array.isArray(pages) ? pages : [])
114
+ .filter((page) => page && !page.hidden)
115
+ .sort((a, b) => getSortTime(b) - getSortTime(a))
116
+ .slice(0, limit)
117
+ }
118
+
119
+ export const renderRssFeed = ({ site, items }) => {
120
+ const prevHydration = getReframeHydrationEnabled()
121
+ const prevThemeHydration = state.THEME_ENV?.getHydrationEnabled?.()
122
+ setReframeHydrationEnabled(false)
123
+ state.THEME_ENV?.setHydrationEnabled?.(false)
124
+ try {
125
+ return HTMLRenderer.serialize(HTMLRenderer.c(RssFeed, { site, items }))
126
+ } finally {
127
+ setReframeHydrationEnabled(prevHydration)
128
+ if (prevThemeHydration != null) {
129
+ state.THEME_ENV?.setHydrationEnabled?.(prevThemeHydration)
130
+ }
131
+ }
132
+ }
133
+
134
+ export const renderAtomFeed = ({ site, items }) => {
135
+ const prevHydration = getReframeHydrationEnabled()
136
+ const prevThemeHydration = state.THEME_ENV?.getHydrationEnabled?.()
137
+ setReframeHydrationEnabled(false)
138
+ state.THEME_ENV?.setHydrationEnabled?.(false)
139
+ try {
140
+ return HTMLRenderer.serialize(HTMLRenderer.c(AtomFeed, { site, items }))
141
+ } finally {
142
+ setReframeHydrationEnabled(prevHydration)
143
+ if (prevThemeHydration != null) {
144
+ state.THEME_ENV?.setHydrationEnabled?.(prevThemeHydration)
145
+ }
146
+ }
147
+ }
148
+
149
+ export const generateRssFeed = async (pagesContext, rssContent = null) => {
150
+ if (!state.RSS_ENABLED) return null
151
+ const options = state.RSS_OPTIONS || {}
152
+ const site = pagesContext?.site || state.USER_SITE || {}
153
+ const siteUrl = resolveSiteUrl(options, site)
154
+ if (!isAbsoluteUrl(siteUrl)) {
155
+ logger.warn('Feed skipped: site.base must be an absolute URL (e.g. https://example.com/).')
156
+ return null
157
+ }
158
+ const isAtom = options.atom === true
159
+ const path = normalizeFeedPath(options.path, isAtom)
160
+ const now = new Date()
161
+ const finalSite = {
162
+ name: site.name,
163
+ title: Object.prototype.hasOwnProperty.call(options, 'title') ? options.title : site.name,
164
+ description: Object.prototype.hasOwnProperty.call(options, 'description') ? options.description : site.description,
165
+ language: Object.prototype.hasOwnProperty.call(options, 'language') ? options.language : (site.language || site.lang),
166
+ url: siteUrl,
167
+ feedUrl: new URL(path, siteUrl).href,
168
+ generator: 'Methanol',
169
+ lastBuildDate: now.toUTCString(),
170
+ updated: now.toISOString()
171
+ }
172
+ const pages = selectFeedPages(pagesContext?.pages || [], options)
173
+ const siteOwner = site.owner || null
174
+ const items = pages
175
+ .map((page) => ({
176
+ page,
177
+ content: rssContent?.get(page.path) ?? rssContent?.get(page.routePath) ?? null
178
+ }))
179
+ .map((entry) => buildItem(entry.page, siteUrl, entry.content, isAtom, siteOwner))
180
+ .filter(Boolean)
181
+ const xml = isAtom
182
+ ? renderAtomFeed({ site: finalSite, items })
183
+ : renderRssFeed({ site: finalSite, items })
184
+ const outPath = resolve(state.DIST_DIR, path.slice(1))
185
+ await ensureDir(dirname(outPath))
186
+ await writeFile(outPath, xml)
187
+ logger.success(`${isAtom ? 'Atom' : 'RSS'} feed generated: ${path} (${items.length} ${items.length === 1 ? 'item' : 'items'})`)
188
+ return { path, count: items.length }
189
+ }
package/src/main.js CHANGED
@@ -22,6 +22,7 @@ import { loadUserConfig, applyConfig } from './config.js'
22
22
  import { runViteDev } from './dev-server.js'
23
23
  import { buildHtmlEntries, runViteBuild } from './build-system.js'
24
24
  import { runPagefind } from './pagefind.js'
25
+ import { generateRssFeed } from './feed.js'
25
26
  import { runVitePreview } from './preview-server.js'
26
27
  import { cli, state } from './state.js'
27
28
  import { HTMLRenderer } from './renderer.js'
@@ -77,6 +78,7 @@ const main = async () => {
77
78
  ...userSite,
78
79
  base: siteBase,
79
80
  name: state.SITE_NAME,
81
+ owner: state.SITE_OWNER,
80
82
  root: state.ROOT_DIR,
81
83
  pagesDir: state.PAGES_DIR,
82
84
  componentsDir: state.COMPONENTS_DIR,
@@ -111,7 +113,7 @@ const main = async () => {
111
113
  const startTime = performance.now()
112
114
  await runHooks(state.USER_PRE_BUILD_HOOKS)
113
115
  await runHooks(state.THEME_PRE_BUILD_HOOKS)
114
- const { entry, htmlCache, pagesContext } = await buildHtmlEntries()
116
+ const { entry, htmlCache, pagesContext, rssContent } = await buildHtmlEntries()
115
117
  const buildContext = pagesContext
116
118
  ? {
117
119
  pagesContext,
@@ -129,6 +131,9 @@ const main = async () => {
129
131
  if (state.PAGEFIND_ENABLED) {
130
132
  await runPagefind()
131
133
  }
134
+ if (state.RSS_ENABLED) {
135
+ await generateRssFeed(pagesContext, rssContent)
136
+ }
132
137
  await runHooks(state.THEME_POST_BUILD_HOOKS, buildContext)
133
138
  await runHooks(state.USER_POST_BUILD_HOOKS, buildContext)
134
139
  const endTime = performance.now()
package/src/mdx.js CHANGED
@@ -28,7 +28,7 @@ import rehypeStarryNight from 'rehype-starry-night'
28
28
  import { createStarryNight } from '@wooorm/starry-night'
29
29
  import remarkGfm from 'remark-gfm'
30
30
  import { HTMLRenderer } from './renderer.js'
31
- import { signal, computed, read, Suspense, nextTick } from 'refui'
31
+ import { Suspense, nextTick } from 'refui'
32
32
  import { createPortal } from 'refui/extras'
33
33
  import { pathToFileURL } from 'url'
34
34
  import { existsSync } from 'fs'
@@ -38,7 +38,7 @@ import { resolveUserMdxConfig, withBase } from './config.js'
38
38
  import { methanolCtx } from './rehype-plugins/methanol-ctx.js'
39
39
  import { linkResolve } from './rehype-plugins/link-resolve.js'
40
40
  import { cached } from './utils.js'
41
- import { resetReframeRenderCount } from './components.js'
41
+ import { resetReframeRenderCount, setReframeHydrationEnabled, getReframeHydrationEnabled } from './components.js'
42
42
 
43
43
  // Workaround for Vite: it doesn't support resolving module/virtual modules in script src in dev mode
44
44
  const resolveRewindInject = cached(() =>
@@ -145,27 +145,8 @@ export const buildPageContext = ({ routePath, path, pageMeta, pagesContext, lazy
145
145
  getSiblings,
146
146
  withBase
147
147
  }
148
- const resolvePagesTree = () =>
149
- pagesContext.getPagesTree ? pagesContext.getPagesTree(routePath) : pagesContext.pagesTree || []
150
- if (lazyPagesTree) {
151
- let cachedTree = null
152
- let hasTree = false
153
- Object.defineProperty(ctx, 'pagesTree', {
154
- enumerable: true,
155
- get() {
156
- if (!hasTree) {
157
- cachedTree = resolvePagesTree()
158
- hasTree = true
159
- }
160
- return cachedTree
161
- },
162
- set(value) {
163
- cachedTree = value
164
- hasTree = true
165
- }
166
- })
167
- } else {
168
- ctx.pagesTree = resolvePagesTree()
148
+ if (!lazyPagesTree) {
149
+ ctx.pagesTree = pagesContext.getPagesTree ? pagesContext.getPagesTree(routePath) : pagesContext.pagesTree || []
169
150
  }
170
151
  return ctx
171
152
  }
@@ -645,5 +626,49 @@ export const renderHtml = async ({ routePath, path, components, pagesContext, pa
645
626
  )
646
627
  })
647
628
 
648
- return HTMLRenderer.serialize(renderResult)
629
+ const result = HTMLRenderer.serialize(renderResult)
630
+
631
+ return result
632
+ }
633
+
634
+ export const renderPageContent = async ({ routePath, path, components, pagesContext, pageMeta }) => {
635
+ const ctx = buildPageContext({
636
+ routePath,
637
+ path,
638
+ pageMeta,
639
+ pagesContext,
640
+ lazyPagesTree: true
641
+ })
642
+
643
+ await compilePageMdx(pageMeta, pagesContext, { ctx })
644
+
645
+ const prevHydration = getReframeHydrationEnabled()
646
+ const prevThemeHydration = state.THEME_ENV?.getHydrationEnabled?.()
647
+ setReframeHydrationEnabled(false)
648
+ state.THEME_ENV?.setHydrationEnabled?.(false)
649
+ const mdxComponent = pageMeta.mdxComponent
650
+ const PageContent = () => mdxComponent()
651
+
652
+ try {
653
+ const renderResult = await new Promise((resolve, reject) => {
654
+ const result = HTMLRenderer.c(
655
+ Suspense,
656
+ {
657
+ onLoad() {
658
+ nextTick(() => resolve(result))
659
+ },
660
+ catch({ error }) {
661
+ reject(error)
662
+ }
663
+ },
664
+ PageContent
665
+ )
666
+ })
667
+ return HTMLRenderer.serialize(renderResult)
668
+ } finally {
669
+ setReframeHydrationEnabled(prevHydration)
670
+ if (prevThemeHydration != null) {
671
+ state.THEME_ENV?.setHydrationEnabled?.(prevThemeHydration)
672
+ }
673
+ }
649
674
  }
@@ -21,7 +21,16 @@
21
21
  import JSON5 from 'json5'
22
22
  import { extractExcerpt } from './text-utils.js'
23
23
 
24
- const OMIT_KEYS = new Set(['content', 'mdxComponent', 'mdxCtx', 'getSiblings'])
24
+ const OMIT_KEYS = new Set([
25
+ 'content',
26
+ 'mdxComponent',
27
+ 'mdxCtx',
28
+ 'getSiblings',
29
+ 'matter',
30
+ 'path',
31
+ 'exclude',
32
+ 'segments'
33
+ ])
25
34
 
26
35
  const sanitizePage = (page) => {
27
36
  const result = {}