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 +1 -1
- package/src/build-system.js +53 -22
- package/src/components.js +2 -0
- package/src/config.js +44 -0
- package/src/dev-server.js +6 -4
- package/src/feed.js +189 -0
- package/src/main.js +6 -1
- package/src/mdx.js +49 -24
- package/src/pages-index.js +10 -1
- package/src/pages.js +105 -111
- package/src/reframe.js +19 -2
- package/src/state.js +31 -3
- package/src/templates/atom-feed.jsx +61 -0
- package/src/{error-page.jsx → templates/error-page.jsx} +2 -2
- package/src/templates/rss-feed.jsx +60 -0
- package/src/workers/build-pool.js +10 -1
- package/src/workers/build-worker.js +30 -23
- package/themes/blog/sources/style.css +44 -4
- package/themes/blog/src/layout-categories.jsx +2 -2
- package/themes/blog/src/layout-collections.jsx +2 -2
- package/themes/blog/src/layout-home.jsx +2 -2
- package/themes/blog/src/page.jsx +55 -6
- package/themes/blog/src/post-utils.js +13 -3
- package/themes/default/sources/style.css +27 -1
- package/themes/default/src/nav-tree.jsx +74 -58
- package/themes/default/src/page.jsx +23 -0
package/package.json
CHANGED
package/src/build-system.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/pages-index.js
CHANGED
|
@@ -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([
|
|
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 = {}
|