methanol 0.0.18 → 0.0.20
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.md +4 -0
- package/package.json +2 -2
- package/src/build-system.js +76 -19
- package/src/components.js +2 -0
- package/src/config.js +41 -0
- package/src/dev-server.js +6 -4
- package/src/feed.js +198 -0
- package/src/main.js +6 -1
- package/src/mdx.js +48 -24
- package/src/pages.js +105 -111
- package/src/reframe.js +19 -2
- package/src/state.js +22 -0
- 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 +51 -21
- package/themes/README.md +52 -0
- package/themes/blog/README.md +34 -14
- 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/README.md +26 -0
- package/themes/default/components/ThemeColorSwitch.client.jsx +13 -0
- package/themes/default/sources/style.css +27 -1
- package/themes/default/sources/theme-prepare.js +13 -0
- package/themes/default/src/nav-tree.jsx +70 -55
- package/themes/default/src/page.jsx +25 -0
|
@@ -28,6 +28,7 @@ let initPromise = null
|
|
|
28
28
|
let pages = []
|
|
29
29
|
let pagesContext = null
|
|
30
30
|
let components = null
|
|
31
|
+
let mdxPageIds = new Set()
|
|
31
32
|
|
|
32
33
|
const ensureInit = async () => {
|
|
33
34
|
if (initPromise) return initPromise
|
|
@@ -112,31 +113,27 @@ const logPageError = (phase, page, error) => {
|
|
|
112
113
|
const handleSetPages = async (message) => {
|
|
113
114
|
const { pages: nextPages, excludedRoutes = [], excludedDirs = [] } = message || {}
|
|
114
115
|
pages = Array.isArray(nextPages) ? nextPages : []
|
|
116
|
+
mdxPageIds = new Set()
|
|
115
117
|
await rebuildPagesContext(new Set(excludedRoutes), new Set(excludedDirs))
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
const handleSyncUpdates = async (message) => {
|
|
119
|
-
const { updates = [],
|
|
120
|
-
if (Array.isArray(titles)) {
|
|
121
|
-
for (let i = 0; i < titles.length; i += 1) {
|
|
122
|
-
const page = pages[i]
|
|
123
|
-
if (!page) continue
|
|
124
|
-
if (titles[i] !== undefined) {
|
|
125
|
-
page.title = titles[i]
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
121
|
+
const { updates = [], excludedRoutes = null, excludedDirs = null } = message || {}
|
|
129
122
|
for (const update of updates) {
|
|
130
123
|
const page = pages[update.id]
|
|
131
124
|
if (!page) continue
|
|
132
125
|
if (update.title !== undefined) page.title = update.title
|
|
133
|
-
if (update.toc !== undefined) page.toc = update.toc
|
|
134
126
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
if (!pagesContext || excludedRoutes || excludedDirs) {
|
|
128
|
+
await rebuildPagesContext(
|
|
129
|
+
excludedRoutes ? new Set(excludedRoutes) : pagesContext?.excludedRoutes || new Set(),
|
|
130
|
+
excludedDirs ? new Set(excludedDirs) : pagesContext?.excludedDirs || new Set()
|
|
131
|
+
)
|
|
132
|
+
} else {
|
|
133
|
+
pagesContext.refreshPagesTree?.(true)
|
|
134
|
+
}
|
|
135
|
+
for (const id of mdxPageIds) {
|
|
136
|
+
const page = pages[id]
|
|
140
137
|
if (!page?.mdxCtx) continue
|
|
141
138
|
refreshMdxCtx(page)
|
|
142
139
|
}
|
|
@@ -159,6 +156,7 @@ const handleCompile = async (message) => {
|
|
|
159
156
|
lazyPagesTree: true,
|
|
160
157
|
refreshPagesTree: false
|
|
161
158
|
})
|
|
159
|
+
mdxPageIds.add(id)
|
|
162
160
|
updates.push({ id, title: page.title, toc: page.toc || null })
|
|
163
161
|
} catch (error) {
|
|
164
162
|
logPageError('MDX compile', page, error)
|
|
@@ -173,7 +171,6 @@ const handleCompile = async (message) => {
|
|
|
173
171
|
const handleRender = async (message) => {
|
|
174
172
|
const { ids = [], stage } = message || {}
|
|
175
173
|
const { renderHtml } = await import('../mdx.js')
|
|
176
|
-
const results = []
|
|
177
174
|
let completed = 0
|
|
178
175
|
for (const id of ids) {
|
|
179
176
|
const page = pages[id]
|
|
@@ -190,7 +187,7 @@ const handleRender = async (message) => {
|
|
|
190
187
|
pagesContext,
|
|
191
188
|
pageMeta: page
|
|
192
189
|
})
|
|
193
|
-
|
|
190
|
+
parentPort?.postMessage({ type: 'result', stage, result: { id, html } })
|
|
194
191
|
} catch (error) {
|
|
195
192
|
logPageError('MDX render', page, error)
|
|
196
193
|
throw error
|
|
@@ -198,7 +195,35 @@ const handleRender = async (message) => {
|
|
|
198
195
|
completed += 1
|
|
199
196
|
parentPort?.postMessage({ type: 'progress', stage, completed })
|
|
200
197
|
}
|
|
201
|
-
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const handleRss = async (message) => {
|
|
201
|
+
const { ids = [], stage } = message || {}
|
|
202
|
+
const { renderPageContent } = await import('../mdx.js')
|
|
203
|
+
let completed = 0
|
|
204
|
+
for (const id of ids) {
|
|
205
|
+
const page = pages[id]
|
|
206
|
+
if (!page) {
|
|
207
|
+
completed += 1
|
|
208
|
+
parentPort?.postMessage({ type: 'progress', stage, completed })
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const content = await renderPageContent({
|
|
213
|
+
routePath: page.routePath,
|
|
214
|
+
path: page.path,
|
|
215
|
+
components,
|
|
216
|
+
pagesContext,
|
|
217
|
+
pageMeta: page
|
|
218
|
+
})
|
|
219
|
+
parentPort?.postMessage({ type: 'result', stage, result: { id, content } })
|
|
220
|
+
} catch (error) {
|
|
221
|
+
logPageError('RSS render', page, error)
|
|
222
|
+
throw error
|
|
223
|
+
}
|
|
224
|
+
completed += 1
|
|
225
|
+
parentPort?.postMessage({ type: 'progress', stage, completed })
|
|
226
|
+
}
|
|
202
227
|
}
|
|
203
228
|
|
|
204
229
|
parentPort?.on('message', async (message) => {
|
|
@@ -221,8 +246,13 @@ parentPort?.on('message', async (message) => {
|
|
|
221
246
|
return
|
|
222
247
|
}
|
|
223
248
|
if (type === 'render') {
|
|
224
|
-
|
|
225
|
-
parentPort?.postMessage({ type: 'done', stage
|
|
249
|
+
await handleRender(message)
|
|
250
|
+
parentPort?.postMessage({ type: 'done', stage })
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
if (type === 'rss') {
|
|
254
|
+
await handleRss(message)
|
|
255
|
+
parentPort?.postMessage({ type: 'done', stage })
|
|
226
256
|
return
|
|
227
257
|
}
|
|
228
258
|
} catch (error) {
|
package/themes/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Themes
|
|
2
|
+
|
|
3
|
+
Methanol ships with built-in themes under `themes/`.
|
|
4
|
+
|
|
5
|
+
## Built-in themes
|
|
6
|
+
|
|
7
|
+
- `default`: Documentation-style theme (sidebar + ToC).
|
|
8
|
+
- `blog`: Blog theme (post list, tags/categories UI).
|
|
9
|
+
|
|
10
|
+
## Using a theme
|
|
11
|
+
|
|
12
|
+
CLI:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
methanol build --theme blog
|
|
16
|
+
methanol dev --theme default
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Config (`methanol.config.*`):
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
export default () => ({
|
|
23
|
+
theme: 'blog'
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Using a local theme (in your project)
|
|
28
|
+
|
|
29
|
+
Theme name resolution only applies when `theme` is a string (built-in or `methanol-theme-xxx` from `node_modules`).
|
|
30
|
+
If your theme lives inside your project, import it and pass the theme object/factory:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import createTheme from './themes/my-theme/index.js'
|
|
34
|
+
|
|
35
|
+
export default () => ({
|
|
36
|
+
theme: createTheme()
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Publishing a theme
|
|
41
|
+
|
|
42
|
+
If you publish a theme as an npm package named `methanol-theme-xxx`, users can enable it via `--theme xxx` or `theme: 'xxx'`.
|
|
43
|
+
|
|
44
|
+
## Theme structure (convention)
|
|
45
|
+
|
|
46
|
+
- `index.js`: entrypoint that exports a theme object or a factory function (recommended).
|
|
47
|
+
- `src/`: theme runtime/template modules (e.g. `src/page.jsx`).
|
|
48
|
+
- `components/`: theme components (used by MDX).
|
|
49
|
+
- `pages/`: theme-provided pages (e.g. `_404.mdx`, `offline.mdx`).
|
|
50
|
+
- `public/`: theme static assets (merged with user `public/`).
|
|
51
|
+
- `sources/`: extra source mappings exposed via `theme.sources`.
|
|
52
|
+
|
package/themes/blog/README.md
CHANGED
|
@@ -1,26 +1,46 @@
|
|
|
1
1
|
# Blog Theme
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A blog theme for Methanol.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
|
-
|
|
7
|
-
- Post list
|
|
8
|
-
-
|
|
9
|
-
-
|
|
6
|
+
|
|
7
|
+
- Post list and post pages
|
|
8
|
+
- Category/collection views (theme-provided client UI)
|
|
9
|
+
- Responsive layout
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
CLI:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
methanol build --theme blog
|
|
17
|
+
methanol dev --theme blog
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Config (`methanol.config.*`):
|
|
14
21
|
|
|
15
22
|
```js
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// ...
|
|
20
|
-
}
|
|
23
|
+
export default () => ({
|
|
24
|
+
theme: 'blog'
|
|
25
|
+
})
|
|
21
26
|
```
|
|
22
27
|
|
|
23
28
|
## Structure
|
|
24
|
-
|
|
25
|
-
- `
|
|
26
|
-
- `pages/`:
|
|
29
|
+
|
|
30
|
+
- `src/page.jsx`: main layout template
|
|
31
|
+
- `pages/`: theme pages (including special pages like `_404.mdx` and `offline.mdx` when present)
|
|
32
|
+
- `components/`: theme components used by MDX
|
|
33
|
+
- `public/`: theme static assets
|
|
34
|
+
- `sources/`: theme source mappings (used by `theme.sources`)
|
|
35
|
+
|
|
36
|
+
## Local development
|
|
37
|
+
|
|
38
|
+
If you want to use the theme from a local folder (instead of built-in name / npm package), import it in config:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import createBlogTheme from './themes/blog/index.js'
|
|
42
|
+
|
|
43
|
+
export default () => ({
|
|
44
|
+
theme: createBlogTheme()
|
|
45
|
+
})
|
|
46
|
+
```
|
|
@@ -98,9 +98,31 @@ a {
|
|
|
98
98
|
flex-shrink: 0;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
.header-actions .rss-link {
|
|
102
|
+
color: var(--text-muted);
|
|
103
|
+
text-decoration: none;
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
width: 2.25rem;
|
|
108
|
+
height: 2.25rem;
|
|
109
|
+
border-radius: 999px;
|
|
110
|
+
transition: all 0.2s;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.header-actions .rss-link:hover {
|
|
114
|
+
color: var(--text);
|
|
115
|
+
background: var(--bg-soft);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.rss-label {
|
|
119
|
+
display: none;
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
.blog-nav {
|
|
102
123
|
display: flex;
|
|
103
124
|
gap: 1.25rem;
|
|
125
|
+
align-items: center;
|
|
104
126
|
}
|
|
105
127
|
|
|
106
128
|
.blog-nav a {
|
|
@@ -551,11 +573,29 @@ h6:hover .heading-anchor, h6:active .heading-anchor {
|
|
|
551
573
|
visibility: visible;
|
|
552
574
|
}
|
|
553
575
|
|
|
554
|
-
|
|
555
|
-
|
|
576
|
+
.nav-toggle:checked ~ .nav-toggle-label {
|
|
577
|
+
color: var(--primary);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.header-actions .rss-link {
|
|
581
|
+
width: 100%;
|
|
582
|
+
height: auto;
|
|
583
|
+
border-radius: 0;
|
|
584
|
+
justify-content: flex-start;
|
|
585
|
+
padding: 0.75rem var(--container-px);
|
|
586
|
+
background: transparent !important;
|
|
587
|
+
font-weight: 500;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.header-actions .rss-link:hover {
|
|
591
|
+
color: var(--primary);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.rss-label {
|
|
595
|
+
display: inline;
|
|
596
|
+
margin-left: 0.5rem;
|
|
597
|
+
}
|
|
556
598
|
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
599
|
@media (max-width: 640px) {
|
|
560
600
|
.blog-header { padding: 1rem 0; }
|
|
561
601
|
.header-container { gap: 1rem; }
|
|
@@ -40,9 +40,9 @@ const renderPostCards = (posts = []) =>
|
|
|
40
40
|
)
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
export const LayoutCategories = ({ PageContent, title, pages, navLinks, components }) => {
|
|
43
|
+
export const LayoutCategories = ({ PageContent, title, pages, navLinks, components, currentRoutePath, hiddenPrefixes }) => {
|
|
44
44
|
const { CategoryView } = components || {}
|
|
45
|
-
const filteredPosts = filterBlogPosts(pages, navLinks)
|
|
45
|
+
const filteredPosts = filterBlogPosts(pages, navLinks, { currentRoutePath, hiddenPrefixes })
|
|
46
46
|
const staticPosts = mapStaticPosts(filteredPosts)
|
|
47
47
|
const categories = collectCategories(filteredPosts)
|
|
48
48
|
const visiblePosts = staticPosts.slice(0, 10)
|
|
@@ -39,9 +39,9 @@ const renderPostCards = (posts = [], collectionTitles = {}) =>
|
|
|
39
39
|
)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
export const LayoutCollections = ({ PageContent, title, pages, pagesByRoute, navLinks, components }) => {
|
|
42
|
+
export const LayoutCollections = ({ PageContent, title, pages, pagesByRoute, navLinks, components, currentRoutePath, hiddenPrefixes }) => {
|
|
43
43
|
const { CollectionView } = components || {}
|
|
44
|
-
const filteredPosts = filterBlogPosts(pages, navLinks)
|
|
44
|
+
const filteredPosts = filterBlogPosts(pages, navLinks, { currentRoutePath, hiddenPrefixes })
|
|
45
45
|
const staticPosts = mapStaticPosts(filteredPosts)
|
|
46
46
|
const collectionTitles = collectCollectionTitles(staticPosts, pagesByRoute)
|
|
47
47
|
const visiblePosts = staticPosts.slice(0, 10)
|
|
@@ -35,9 +35,9 @@ const renderPostCards = (posts = []) =>
|
|
|
35
35
|
)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
export const LayoutHome = ({ PageContent, pages, navLinks, components }) => {
|
|
38
|
+
export const LayoutHome = ({ PageContent, pages, navLinks, components, currentRoutePath, hiddenPrefixes }) => {
|
|
39
39
|
const { PostList } = components || {}
|
|
40
|
-
const filteredPosts = filterBlogPosts(pages, navLinks)
|
|
40
|
+
const filteredPosts = filterBlogPosts(pages, navLinks, { currentRoutePath, hiddenPrefixes })
|
|
41
41
|
const staticPosts = mapStaticPosts(filteredPosts)
|
|
42
42
|
const visiblePosts = staticPosts.slice(0, 10)
|
|
43
43
|
const hasMore = staticPosts.length > visiblePosts.length
|
package/themes/blog/src/page.jsx
CHANGED
|
@@ -24,7 +24,7 @@ import { LayoutCategories } from './layout-categories.jsx'
|
|
|
24
24
|
import { LayoutCollections } from './layout-collections.jsx'
|
|
25
25
|
import { LayoutPost } from './layout-post.jsx'
|
|
26
26
|
|
|
27
|
-
const Header = ({ siteName, navLinks, components, pagefindOptions }) => {
|
|
27
|
+
const Header = ({ siteName, navLinks, components, pagefindOptions, rssHref, feedLabel }) => {
|
|
28
28
|
const { ThemeSearchBox } = components || {}
|
|
29
29
|
return (
|
|
30
30
|
<header class="blog-header">
|
|
@@ -34,7 +34,6 @@ const Header = ({ siteName, navLinks, components, pagefindOptions }) => {
|
|
|
34
34
|
</a>
|
|
35
35
|
<div class="header-actions">
|
|
36
36
|
{ThemeSearchBox && <ThemeSearchBox options={pagefindOptions} />}
|
|
37
|
-
|
|
38
37
|
<input type="checkbox" id="nav-toggle" class="nav-toggle" />
|
|
39
38
|
<label for="nav-toggle" class="nav-toggle-label">
|
|
40
39
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -50,6 +49,25 @@ const Header = ({ siteName, navLinks, components, pagefindOptions }) => {
|
|
|
50
49
|
{link.label}
|
|
51
50
|
</a>
|
|
52
51
|
))}
|
|
52
|
+
{rssHref ? (
|
|
53
|
+
<a class="rss-link" href={rssHref} aria-label={feedLabel} title={feedLabel}>
|
|
54
|
+
<svg
|
|
55
|
+
width="20"
|
|
56
|
+
height="20"
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
stroke-width="2"
|
|
61
|
+
stroke-linecap="round"
|
|
62
|
+
stroke-linejoin="round"
|
|
63
|
+
>
|
|
64
|
+
<path d="M4 11a9 9 0 0 1 9 9"></path>
|
|
65
|
+
<path d="M4 4a16 16 0 0 1 16 16"></path>
|
|
66
|
+
<circle cx="5" cy="19" r="1"></circle>
|
|
67
|
+
</svg>
|
|
68
|
+
<span class="rss-label">{feedLabel}</span>
|
|
69
|
+
</a>
|
|
70
|
+
) : null}
|
|
53
71
|
</nav>
|
|
54
72
|
</div>
|
|
55
73
|
</div>
|
|
@@ -57,11 +75,11 @@ const Header = ({ siteName, navLinks, components, pagefindOptions }) => {
|
|
|
57
75
|
)
|
|
58
76
|
}
|
|
59
77
|
|
|
60
|
-
const Footer = ({ siteName }) => (
|
|
78
|
+
const Footer = ({ siteName, owner }) => (
|
|
61
79
|
<footer class="blog-footer">
|
|
62
80
|
<div class="container">
|
|
63
81
|
<p>
|
|
64
|
-
© {new Date().getFullYear()} {siteName}. Powered by <a href="https://methanol.sudoMaker.com">Methanol</a>.
|
|
82
|
+
© {new Date().getFullYear()} {owner || siteName}. Powered by <a href="https://methanol.sudoMaker.com">Methanol</a>.
|
|
65
83
|
</p>
|
|
66
84
|
</div>
|
|
67
85
|
</footer>
|
|
@@ -70,6 +88,7 @@ const Footer = ({ siteName }) => (
|
|
|
70
88
|
const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx, withBase }) => {
|
|
71
89
|
const page = ctx.page
|
|
72
90
|
const siteName = ctx.site.name || 'Methanol Blog'
|
|
91
|
+
const siteOwner = ctx.site.owner || null
|
|
73
92
|
const title = page.title || siteName
|
|
74
93
|
|
|
75
94
|
const isHome = page.routePath === '/'
|
|
@@ -90,11 +109,27 @@ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx, withBase }) =>
|
|
|
90
109
|
return link
|
|
91
110
|
})
|
|
92
111
|
|
|
112
|
+
const currentRoutePath = page.routePath || '/'
|
|
93
113
|
const isCategoriesPage = page.routePath === '/categories'
|
|
94
114
|
const isCollectionsPage = page.routePath === '/collections'
|
|
115
|
+
const hiddenPrefixes = (ctx.pages || [])
|
|
116
|
+
.filter(
|
|
117
|
+
(entry) =>
|
|
118
|
+
entry.isIndex &&
|
|
119
|
+
entry.hidden &&
|
|
120
|
+
entry.routePath &&
|
|
121
|
+
entry.routePath !== '/' &&
|
|
122
|
+
entry.routePath !== '/404' &&
|
|
123
|
+
entry.routePath !== '/offline'
|
|
124
|
+
)
|
|
125
|
+
.map((entry) => (entry.routePath.endsWith('/') ? entry.routePath : `${entry.routePath}/`))
|
|
95
126
|
|
|
96
127
|
const pagefindEnabled = ctx.site.pagefind?.enabled !== false
|
|
97
128
|
const pagefindOptions = ctx.site.pagefind?.options || null
|
|
129
|
+
const feedInfo = ctx.site.feed
|
|
130
|
+
const rssHref = feedInfo?.enabled ? feedInfo.href : null
|
|
131
|
+
const feedType = feedInfo?.atom ? 'application/atom+xml' : 'application/rss+xml'
|
|
132
|
+
const feedLabel = feedInfo?.atom ? 'Atom' : 'RSS'
|
|
98
133
|
|
|
99
134
|
return (
|
|
100
135
|
<>
|
|
@@ -107,6 +142,7 @@ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx, withBase }) =>
|
|
|
107
142
|
{title} | {siteName}
|
|
108
143
|
</title>
|
|
109
144
|
<link rel="stylesheet" href="/.methanol_theme_blog/style.css" />
|
|
145
|
+
{rssHref ? <link rel="alternate" type={feedType} title={`${siteName} ${feedLabel}`} href={rssHref} /> : null}
|
|
110
146
|
<ExtraHead />
|
|
111
147
|
</head>
|
|
112
148
|
<body>
|
|
@@ -116,10 +152,19 @@ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx, withBase }) =>
|
|
|
116
152
|
navLinks={resolvedNavLinks}
|
|
117
153
|
components={pagefindEnabled ? components : {}}
|
|
118
154
|
pagefindOptions={pagefindOptions}
|
|
155
|
+
rssHref={rssHref}
|
|
156
|
+
feedLabel={feedLabel}
|
|
119
157
|
/>
|
|
120
158
|
<main class="container main-content">
|
|
121
159
|
{isHome ? (
|
|
122
|
-
<LayoutHome
|
|
160
|
+
<LayoutHome
|
|
161
|
+
PageContent={PageContent}
|
|
162
|
+
pages={ctx.pages}
|
|
163
|
+
navLinks={navLinks}
|
|
164
|
+
components={components}
|
|
165
|
+
currentRoutePath={currentRoutePath}
|
|
166
|
+
hiddenPrefixes={hiddenPrefixes}
|
|
167
|
+
/>
|
|
123
168
|
) : isCategoriesPage ? (
|
|
124
169
|
<LayoutCategories
|
|
125
170
|
PageContent={PageContent}
|
|
@@ -127,6 +172,8 @@ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx, withBase }) =>
|
|
|
127
172
|
pages={ctx.pages}
|
|
128
173
|
navLinks={navLinks}
|
|
129
174
|
components={components}
|
|
175
|
+
currentRoutePath={currentRoutePath}
|
|
176
|
+
hiddenPrefixes={hiddenPrefixes}
|
|
130
177
|
/>
|
|
131
178
|
) : isCollectionsPage ? (
|
|
132
179
|
<LayoutCollections
|
|
@@ -136,12 +183,14 @@ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx, withBase }) =>
|
|
|
136
183
|
pagesByRoute={ctx.pagesByRoute}
|
|
137
184
|
navLinks={navLinks}
|
|
138
185
|
components={components}
|
|
186
|
+
currentRoutePath={currentRoutePath}
|
|
187
|
+
hiddenPrefixes={hiddenPrefixes}
|
|
139
188
|
/>
|
|
140
189
|
) : (
|
|
141
190
|
<LayoutPost PageContent={PageContent} title={title} page={page} />
|
|
142
191
|
)}
|
|
143
192
|
</main>
|
|
144
|
-
<Footer siteName={siteName} />
|
|
193
|
+
<Footer siteName={siteName} owner={siteOwner} />
|
|
145
194
|
</div>
|
|
146
195
|
</body>
|
|
147
196
|
</html>
|
|
@@ -29,15 +29,25 @@ const isBlogPost = (page) => {
|
|
|
29
29
|
return true
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export const filterBlogPosts = (pages, navLinks = []) => {
|
|
32
|
+
export const filterBlogPosts = (pages, navLinks = [], options = {}) => {
|
|
33
33
|
const excluded = new Set((navLinks || []).map((link) => link?.href).filter(Boolean))
|
|
34
34
|
const list = (pages || []).filter((page) => isBlogPost(page) && !excluded.has(page.routeHref))
|
|
35
|
-
|
|
35
|
+
const { currentRoutePath, hiddenPrefixes } = options || {}
|
|
36
|
+
const activeRoute = typeof currentRoutePath === 'string' ? currentRoutePath : ''
|
|
37
|
+
const hiddenScopes = Array.isArray(hiddenPrefixes) ? hiddenPrefixes.filter(Boolean) : []
|
|
38
|
+
const filtered = hiddenScopes.length
|
|
39
|
+
? list.filter((page) => {
|
|
40
|
+
const hiddenScope = hiddenScopes.find((prefix) => page.routePath?.startsWith(prefix))
|
|
41
|
+
if (!hiddenScope) return true
|
|
42
|
+
return activeRoute.startsWith(hiddenScope)
|
|
43
|
+
})
|
|
44
|
+
: list
|
|
45
|
+
filtered.sort((a, b) => {
|
|
36
46
|
const dateA = new Date(a.frontmatter?.date || a.stats?.createdAt || 0)
|
|
37
47
|
const dateB = new Date(b.frontmatter?.date || b.stats?.createdAt || 0)
|
|
38
48
|
return dateB - dateA
|
|
39
49
|
})
|
|
40
|
-
return
|
|
50
|
+
return filtered
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
export const getExcerpt = (page) => extractExcerpt(page)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Default Theme
|
|
2
|
+
|
|
3
|
+
The default Methanol theme is designed for documentation sites (sidebar navigation + table of contents).
|
|
4
|
+
|
|
5
|
+
## Enable
|
|
6
|
+
|
|
7
|
+
CLI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
methanol build --theme default
|
|
11
|
+
methanol dev --theme default
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Config:
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
export default () => ({
|
|
18
|
+
theme: 'default'
|
|
19
|
+
})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Notes
|
|
23
|
+
|
|
24
|
+
- User `public/` assets override theme-provided `public/` assets.
|
|
25
|
+
- Theme pages under `pages/` can provide special routes like `_404.mdx` and `offline.mdx`.
|
|
26
|
+
|
|
@@ -75,6 +75,19 @@ export default function () {
|
|
|
75
75
|
localStorage.setItem('methanol-theme', theme.value)
|
|
76
76
|
document.documentElement.classList.toggle('light', theme.value === 'light')
|
|
77
77
|
document.documentElement.classList.toggle('dark', theme.value === 'dark')
|
|
78
|
+
|
|
79
|
+
const metas = document.querySelectorAll('meta[name="theme-color"]')
|
|
80
|
+
let meta = metas[0]
|
|
81
|
+
if (!meta) {
|
|
82
|
+
meta = document.createElement('meta')
|
|
83
|
+
meta.name = 'theme-color'
|
|
84
|
+
document.head.appendChild(meta)
|
|
85
|
+
}
|
|
86
|
+
for (let i = 1; i < metas.length; i++) {
|
|
87
|
+
metas[i].remove()
|
|
88
|
+
}
|
|
89
|
+
meta.content = theme.value === 'light' ? '#ffffff' : '#09090b'
|
|
90
|
+
meta.removeAttribute('media')
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
const CurrentIcon = $(() => {
|
|
@@ -470,6 +470,33 @@ a {
|
|
|
470
470
|
flex-shrink: 0; /* Prevent footer from shrinking */
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
.sidebar-footer .rss-link {
|
|
474
|
+
display: flex;
|
|
475
|
+
align-items: center;
|
|
476
|
+
justify-content: center;
|
|
477
|
+
width: 2.25rem;
|
|
478
|
+
height: 2.25rem;
|
|
479
|
+
background: var(--surface-muted);
|
|
480
|
+
border: 1px solid var(--border);
|
|
481
|
+
border-radius: var(--radius);
|
|
482
|
+
color: var(--text);
|
|
483
|
+
transition: all 0.2s ease;
|
|
484
|
+
flex-shrink: 0;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.sidebar-footer .rss-link:hover {
|
|
488
|
+
background: var(--hover-bg);
|
|
489
|
+
border-color: var(--muted);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.sidebar-footer .rss-link:active {
|
|
493
|
+
transform: scale(0.96);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.sidebar-footer .rss-link svg {
|
|
497
|
+
opacity: 0.8;
|
|
498
|
+
}
|
|
499
|
+
|
|
473
500
|
.lang-switch-wrapper {
|
|
474
501
|
flex: 1;
|
|
475
502
|
position: relative;
|
|
@@ -1825,4 +1852,3 @@ a {
|
|
|
1825
1852
|
size: auto;
|
|
1826
1853
|
}
|
|
1827
1854
|
}
|
|
1828
|
-
|
|
@@ -25,6 +25,19 @@
|
|
|
25
25
|
document.documentElement.classList.toggle('light', theme === 'light')
|
|
26
26
|
document.documentElement.classList.toggle('dark', theme === 'dark')
|
|
27
27
|
|
|
28
|
+
const metas = document.querySelectorAll('meta[name="theme-color"]')
|
|
29
|
+
let meta = metas[0]
|
|
30
|
+
if (!meta) {
|
|
31
|
+
meta = document.createElement('meta')
|
|
32
|
+
meta.name = 'theme-color'
|
|
33
|
+
document.head.appendChild(meta)
|
|
34
|
+
}
|
|
35
|
+
for (let i = 1; i < metas.length; i++) {
|
|
36
|
+
metas[i].remove()
|
|
37
|
+
}
|
|
38
|
+
meta.content = theme === 'light' ? '#ffffff' : '#09090b'
|
|
39
|
+
meta.removeAttribute('media')
|
|
40
|
+
|
|
28
41
|
const savedAccent = localStorage.getItem('methanol-accent')
|
|
29
42
|
if (savedAccent && savedAccent !== 'default') {
|
|
30
43
|
document.documentElement.classList.add('accent-' + savedAccent)
|