@codemeall/create-word-pages 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-word-pages.js +76 -0
- package/package.json +21 -0
- package/template/.github/workflows/pages.yml +51 -0
- package/template/README.md +54 -0
- package/template/content/notes/publishing-model.md +19 -0
- package/template/content/pages/about.md +14 -0
- package/template/content/pages/home.md +16 -0
- package/template/content/posts/launching-word-pages.md +16 -0
- package/template/content/templates/post.md +12 -0
- package/template/package.json +31 -0
- package/template/quartz.config.ts +78 -0
- package/template/scripts/frontmatter.mjs +98 -0
- package/template/scripts/import-jekyll-posts.mjs +55 -0
- package/template/scripts/install-quartz.mjs +63 -0
- package/template/scripts/prepare-quartz-content.mjs +80 -0
- package/template/scripts/run-quartz.mjs +20 -0
- package/template/wizard/index.html +12 -0
- package/template/wizard/src/main.tsx +144 -0
- package/template/wizard/src/styles.css +132 -0
- package/template/wizard/vite.config.ts +43 -0
- package/template/word-pages.config.json +18 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { cp, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const packageRoot = path.resolve(__dirname, "..")
|
|
9
|
+
const templateRoot = path.join(packageRoot, "template")
|
|
10
|
+
|
|
11
|
+
const rawTarget = process.argv[2] ?? "word-pages-site"
|
|
12
|
+
const targetDir = path.resolve(process.cwd(), rawTarget)
|
|
13
|
+
const projectName = path.basename(targetDir)
|
|
14
|
+
|
|
15
|
+
async function ensureEmptyOrCreate(dir) {
|
|
16
|
+
await mkdir(dir, { recursive: true })
|
|
17
|
+
const entries = await readdir(dir)
|
|
18
|
+
if (entries.length > 0) {
|
|
19
|
+
throw new Error(`Target directory is not empty: ${dir}`)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function replacePlaceholders(filePath) {
|
|
24
|
+
const info = await stat(filePath)
|
|
25
|
+
if (!info.isFile()) return
|
|
26
|
+
|
|
27
|
+
const textExtensions = new Set([
|
|
28
|
+
".css",
|
|
29
|
+
".html",
|
|
30
|
+
".js",
|
|
31
|
+
".json",
|
|
32
|
+
".md",
|
|
33
|
+
".mjs",
|
|
34
|
+
".ts",
|
|
35
|
+
".tsx",
|
|
36
|
+
".txt",
|
|
37
|
+
".yml"
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
if (!textExtensions.has(path.extname(filePath))) return
|
|
41
|
+
|
|
42
|
+
const source = await readFile(filePath, "utf8")
|
|
43
|
+
const updated = source.replaceAll("__WORD_PAGES_PROJECT_NAME__", projectName)
|
|
44
|
+
if (updated !== source) {
|
|
45
|
+
await writeFile(filePath, updated)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function walk(dir, visitor) {
|
|
50
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const fullPath = path.join(dir, entry.name)
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
await walk(fullPath, visitor)
|
|
55
|
+
} else {
|
|
56
|
+
await visitor(fullPath)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await ensureEmptyOrCreate(targetDir)
|
|
63
|
+
await cp(templateRoot, targetDir, { recursive: true })
|
|
64
|
+
await walk(targetDir, replacePlaceholders)
|
|
65
|
+
|
|
66
|
+
console.log(`Created Word Pages starter in ${targetDir}`)
|
|
67
|
+
console.log("")
|
|
68
|
+
console.log("Next steps:")
|
|
69
|
+
console.log(` cd ${path.relative(process.cwd(), targetDir) || "."}`)
|
|
70
|
+
console.log(" npm install")
|
|
71
|
+
console.log(" npm run wizard")
|
|
72
|
+
console.log(" npm run preview")
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(error instanceof Error ? error.message : error)
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codemeall/create-word-pages",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a Word Pages Obsidian-to-GitHub-Pages starter.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-word-pages": "./bin/create-word-pages.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22",
|
|
18
|
+
"npm": ">=10.9"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT"
|
|
21
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Deploy Word Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pages: write
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: pages
|
|
16
|
+
cancel-in-progress: false
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Setup Node
|
|
26
|
+
uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: 22
|
|
29
|
+
cache: npm
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: npm ci
|
|
33
|
+
|
|
34
|
+
- name: Build
|
|
35
|
+
run: npm run build
|
|
36
|
+
|
|
37
|
+
- name: Upload artifact
|
|
38
|
+
uses: actions/upload-pages-artifact@v3
|
|
39
|
+
with:
|
|
40
|
+
path: .word-pages/quartz/public
|
|
41
|
+
|
|
42
|
+
deploy:
|
|
43
|
+
environment:
|
|
44
|
+
name: github-pages
|
|
45
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
needs: build
|
|
48
|
+
steps:
|
|
49
|
+
- name: Deploy
|
|
50
|
+
id: deployment
|
|
51
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Word Pages Starter
|
|
2
|
+
|
|
3
|
+
Word Pages publishes an Obsidian-authored Markdown vault to GitHub Pages with a Quartz-powered reading experience.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run wizard
|
|
10
|
+
npm run preview
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Open `content/` as an Obsidian vault. Write pages, posts, and notes there. A Markdown file is rendered only when it has `publish: true` in frontmatter.
|
|
14
|
+
|
|
15
|
+
Important: `publish: true` controls whether the generated site renders a page. It does not make committed source Markdown private. If this repository is public, committed files can be visible on GitHub even when they are not rendered.
|
|
16
|
+
|
|
17
|
+
## Content Contract
|
|
18
|
+
|
|
19
|
+
Use frontmatter to classify content:
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
---
|
|
23
|
+
title: "My post"
|
|
24
|
+
type: post
|
|
25
|
+
publish: true
|
|
26
|
+
date: 2026-06-06
|
|
27
|
+
tags:
|
|
28
|
+
- writing
|
|
29
|
+
---
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Supported types:
|
|
33
|
+
|
|
34
|
+
- `page`: routed to `/` when `slug: index` or `slug: home`; otherwise routed to `/:slug/`.
|
|
35
|
+
- `post`: routed to `/posts/:slug/`.
|
|
36
|
+
- `note`: routed to `/notes/:slug/`.
|
|
37
|
+
|
|
38
|
+
Wikilinks, backlinks, graph view, tags, search, and dark/light mode are provided by Quartz.
|
|
39
|
+
|
|
40
|
+
## GitHub Pages
|
|
41
|
+
|
|
42
|
+
The included workflow builds the site with GitHub Actions and publishes the generated `public/` folder. For project Pages, set your GitHub username and repository name in the wizard so the Quartz `baseUrl` is correct. Quartz expects this value without protocol or slashes, for example `octocat.github.io/my-site`.
|
|
43
|
+
|
|
44
|
+
Custom domains are supported by GitHub Pages, but Word Pages v1 only documents that workflow and does not automate DNS setup.
|
|
45
|
+
|
|
46
|
+
## Import Jekyll Posts
|
|
47
|
+
|
|
48
|
+
To import an existing Jekyll `_posts` folder:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm run import:jekyll -- /path/to/site/_posts
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The importer copies Markdown files into `content/posts`, preserves common frontmatter, adds `type: post`, and adds `publish: true`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Publishing Model"
|
|
3
|
+
type: note
|
|
4
|
+
slug: publishing-model
|
|
5
|
+
publish: true
|
|
6
|
+
tags:
|
|
7
|
+
- docs
|
|
8
|
+
- github-pages
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Publishing Model
|
|
12
|
+
|
|
13
|
+
Word Pages stages publishable notes into Quartz routes before building the static site.
|
|
14
|
+
|
|
15
|
+
- `page` content becomes top-level pages.
|
|
16
|
+
- `post` content becomes `/posts/:slug/`.
|
|
17
|
+
- `note` content becomes `/notes/:slug/`.
|
|
18
|
+
|
|
19
|
+
Only files with `publish: true` are rendered. Source privacy still depends on repository visibility.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "About"
|
|
3
|
+
type: page
|
|
4
|
+
slug: about
|
|
5
|
+
publish: true
|
|
6
|
+
tags:
|
|
7
|
+
- portfolio
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# About
|
|
11
|
+
|
|
12
|
+
Add your profile, services, contact links, or agency positioning here.
|
|
13
|
+
|
|
14
|
+
Word Pages keeps the public site static and lets GitHub Pages host it from your repository.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Home"
|
|
3
|
+
type: page
|
|
4
|
+
slug: index
|
|
5
|
+
publish: true
|
|
6
|
+
tags:
|
|
7
|
+
- portfolio
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Word Pages
|
|
11
|
+
|
|
12
|
+
This is your public home page. Open the `content/` folder in Obsidian and edit this note.
|
|
13
|
+
|
|
14
|
+
Use this space for a calm portfolio introduction, featured writing, client knowledge base links, or a personal digital garden.
|
|
15
|
+
|
|
16
|
+
Start with [[notes/publishing-model|the publishing model]] or read the sample [[posts/launching-word-pages|launch post]].
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Launching Word Pages"
|
|
3
|
+
type: post
|
|
4
|
+
slug: launching-word-pages
|
|
5
|
+
publish: true
|
|
6
|
+
date: 2026-06-06
|
|
7
|
+
tags:
|
|
8
|
+
- writing
|
|
9
|
+
- word-pages
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Launching Word Pages
|
|
13
|
+
|
|
14
|
+
This sample post shows the blog side of your Obsidian vault.
|
|
15
|
+
|
|
16
|
+
Posts are routed under `/posts/` and can link to notes like [[notes/publishing-model|Publishing Model]].
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__WORD_PAGES_PROJECT_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"wizard": "vite --host 127.0.0.1 --config wizard/vite.config.ts",
|
|
8
|
+
"prepare:content": "node scripts/prepare-quartz-content.mjs",
|
|
9
|
+
"import:jekyll": "node scripts/import-jekyll-posts.mjs",
|
|
10
|
+
"quartz:install": "node scripts/install-quartz.mjs",
|
|
11
|
+
"build": "npm run prepare:content && npm run quartz:install && node scripts/run-quartz.mjs build -d ../quartz-content -o public",
|
|
12
|
+
"preview": "npm run prepare:content && npm run quartz:install && node scripts/run-quartz.mjs build -d ../quartz-content --serve",
|
|
13
|
+
"test": "node --test"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
17
|
+
"vite": "^7.0.0",
|
|
18
|
+
"react": "^19.0.0",
|
|
19
|
+
"react-dom": "^19.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "^5.8.0"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22",
|
|
26
|
+
"npm": ">=10.9"
|
|
27
|
+
},
|
|
28
|
+
"wordPages": {
|
|
29
|
+
"quartzRepository": "https://github.com/jackyzha0/quartz.git"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { QuartzConfig } from "./quartz/cfg"
|
|
2
|
+
import * as Plugin from "./quartz/plugins"
|
|
3
|
+
|
|
4
|
+
const config: QuartzConfig = {
|
|
5
|
+
configuration: {
|
|
6
|
+
pageTitle: __WORD_PAGES_TITLE__,
|
|
7
|
+
enableSPA: true,
|
|
8
|
+
enablePopovers: true,
|
|
9
|
+
analytics: null,
|
|
10
|
+
locale: "en-US",
|
|
11
|
+
baseUrl: __WORD_PAGES_BASE_URL__,
|
|
12
|
+
ignorePatterns: ["private", "templates", ".obsidian"],
|
|
13
|
+
defaultDateType: "modified",
|
|
14
|
+
theme: {
|
|
15
|
+
fontOrigin: "googleFonts",
|
|
16
|
+
cdnCaching: true,
|
|
17
|
+
typography: {
|
|
18
|
+
header: "Schibsted Grotesk",
|
|
19
|
+
body: "Source Sans 3",
|
|
20
|
+
code: "IBM Plex Mono"
|
|
21
|
+
},
|
|
22
|
+
colors: {
|
|
23
|
+
lightMode: {
|
|
24
|
+
light: "#faf8f3",
|
|
25
|
+
lightgray: "#e5e0d5",
|
|
26
|
+
gray: "#8a8376",
|
|
27
|
+
darkgray: "#3e3a33",
|
|
28
|
+
dark: "#171613",
|
|
29
|
+
secondary: "#28786f",
|
|
30
|
+
tertiary: "#8d5c2c",
|
|
31
|
+
highlight: "rgba(40, 120, 111, 0.14)",
|
|
32
|
+
textHighlight: "#fff0a8"
|
|
33
|
+
},
|
|
34
|
+
darkMode: {
|
|
35
|
+
light: "#181816",
|
|
36
|
+
lightgray: "#2a2a26",
|
|
37
|
+
gray: "#aaa394",
|
|
38
|
+
darkgray: "#ddd8cb",
|
|
39
|
+
dark: "#f8f3e7",
|
|
40
|
+
secondary: "#73c6b6",
|
|
41
|
+
tertiary: "#d39b63",
|
|
42
|
+
highlight: "rgba(115, 198, 182, 0.16)",
|
|
43
|
+
textHighlight: "#5e4d18"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
plugins: {
|
|
49
|
+
transformers: [
|
|
50
|
+
Plugin.FrontMatter(),
|
|
51
|
+
Plugin.CreatedModifiedDate({ priority: ["frontmatter", "filesystem"] }),
|
|
52
|
+
Plugin.SyntaxHighlighting(),
|
|
53
|
+
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
|
54
|
+
Plugin.GitHubFlavoredMarkdown(),
|
|
55
|
+
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
|
56
|
+
Plugin.Description(),
|
|
57
|
+
Plugin.Latex({ renderEngine: "katex" })
|
|
58
|
+
],
|
|
59
|
+
filters: [
|
|
60
|
+
Plugin.ExplicitPublish()
|
|
61
|
+
],
|
|
62
|
+
emitters: [
|
|
63
|
+
Plugin.AliasRedirects(),
|
|
64
|
+
Plugin.ComponentResources(),
|
|
65
|
+
Plugin.ContentPage(),
|
|
66
|
+
Plugin.FolderPage(),
|
|
67
|
+
Plugin.TagPage(),
|
|
68
|
+
Plugin.ContentIndex({
|
|
69
|
+
enableSiteMap: true,
|
|
70
|
+
enableRSS: true
|
|
71
|
+
}),
|
|
72
|
+
Plugin.Assets(),
|
|
73
|
+
Plugin.Static()
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default config
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export function parseFrontmatter(source) {
|
|
2
|
+
if (!source.startsWith("---\n")) {
|
|
3
|
+
return { data: {}, content: source }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const end = source.indexOf("\n---", 4)
|
|
7
|
+
if (end === -1) {
|
|
8
|
+
return { data: {}, content: source }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const yaml = source.slice(4, end)
|
|
12
|
+
const content = source.slice(end + 4).replace(/^\n/, "")
|
|
13
|
+
return { data: parseYaml(yaml), content }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function stringifyFrontmatter(content, data) {
|
|
17
|
+
return `---\n${stringifyYaml(data)}---\n\n${content.trimStart()}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseYaml(yaml) {
|
|
21
|
+
const data = {}
|
|
22
|
+
const lines = yaml.split("\n")
|
|
23
|
+
let index = 0
|
|
24
|
+
|
|
25
|
+
while (index < lines.length) {
|
|
26
|
+
const line = lines[index]
|
|
27
|
+
if (!line.trim()) {
|
|
28
|
+
index += 1
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)
|
|
33
|
+
if (!match) {
|
|
34
|
+
index += 1
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [, key, rawValue] = match
|
|
39
|
+
if (rawValue === "") {
|
|
40
|
+
const values = []
|
|
41
|
+
let cursor = index + 1
|
|
42
|
+
while (cursor < lines.length) {
|
|
43
|
+
const item = lines[cursor].match(/^\s*-\s*(.*)$/)
|
|
44
|
+
if (!item) break
|
|
45
|
+
values.push(parseScalar(item[1]))
|
|
46
|
+
cursor += 1
|
|
47
|
+
}
|
|
48
|
+
data[key] = values
|
|
49
|
+
index = cursor
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
data[key] = parseScalar(rawValue)
|
|
54
|
+
index += 1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return data
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseScalar(raw) {
|
|
61
|
+
const value = raw.trim()
|
|
62
|
+
if (value === "true") return true
|
|
63
|
+
if (value === "false") return false
|
|
64
|
+
if (value === "[]") return []
|
|
65
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
66
|
+
return value
|
|
67
|
+
.slice(1, -1)
|
|
68
|
+
.split(",")
|
|
69
|
+
.map((item) => parseScalar(item))
|
|
70
|
+
.filter((item) => item !== "")
|
|
71
|
+
}
|
|
72
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
73
|
+
return value.slice(1, -1)
|
|
74
|
+
}
|
|
75
|
+
return value
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stringifyYaml(data) {
|
|
79
|
+
return Object.entries(data)
|
|
80
|
+
.filter(([, value]) => value !== undefined)
|
|
81
|
+
.map(([key, value]) => {
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
if (value.length === 0) return `${key}: []\n`
|
|
84
|
+
return `${key}:\n${value.map((item) => ` - ${formatScalar(item)}`).join("\n")}\n`
|
|
85
|
+
}
|
|
86
|
+
return `${key}: ${formatScalar(value)}\n`
|
|
87
|
+
})
|
|
88
|
+
.join("")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatScalar(value) {
|
|
92
|
+
if (typeof value === "boolean") return value ? "true" : "false"
|
|
93
|
+
const text = String(value)
|
|
94
|
+
if (!text || /[:#\n{}[\],&*?|<>=!%@`]/.test(text)) {
|
|
95
|
+
return JSON.stringify(text)
|
|
96
|
+
}
|
|
97
|
+
return text
|
|
98
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, readFile, writeFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { parseFrontmatter, stringifyFrontmatter } from "./frontmatter.mjs"
|
|
4
|
+
|
|
5
|
+
const source = process.argv[2]
|
|
6
|
+
if (!source) {
|
|
7
|
+
console.error("Usage: npm run import:jekyll -- /path/to/site/_posts")
|
|
8
|
+
process.exit(1)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const root = process.cwd()
|
|
12
|
+
const destination = path.join(root, "content", "posts")
|
|
13
|
+
await mkdir(destination, { recursive: true })
|
|
14
|
+
|
|
15
|
+
function slugFromJekyllName(fileName) {
|
|
16
|
+
return fileName
|
|
17
|
+
.replace(/^\d{4}-\d{2}-\d{2}-/, "")
|
|
18
|
+
.replace(/\.md$/i, "")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const files = (await readdir(source)).filter((file) => file.endsWith(".md"))
|
|
22
|
+
let imported = 0
|
|
23
|
+
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const fullPath = path.join(source, file)
|
|
26
|
+
const raw = await readFile(fullPath, "utf8")
|
|
27
|
+
const parsed = parseFrontmatter(raw)
|
|
28
|
+
const slug = parsed.data.slug ?? slugFromJekyllName(file)
|
|
29
|
+
|
|
30
|
+
const data = {
|
|
31
|
+
...parsed.data,
|
|
32
|
+
type: "post",
|
|
33
|
+
publish: true,
|
|
34
|
+
slug,
|
|
35
|
+
title: parsed.data.title ?? slug.replaceAll("-", " "),
|
|
36
|
+
date: parsed.data.date ?? file.slice(0, 10)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
delete data.layout
|
|
40
|
+
delete data.permalink
|
|
41
|
+
|
|
42
|
+
const outputPath = path.join(destination, `${slug}.md`)
|
|
43
|
+
await writeFile(outputPath, stringifyFrontmatter(parsed.content.trimStart(), data))
|
|
44
|
+
imported += 1
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const assetSource = path.resolve(source, "..", "assets")
|
|
48
|
+
try {
|
|
49
|
+
await cp(assetSource, path.join(root, "content", "assets"), { recursive: true, force: true })
|
|
50
|
+
console.log("Copied assets into content/assets.")
|
|
51
|
+
} catch {
|
|
52
|
+
console.log("No sibling assets folder copied.")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(`Imported ${imported} Jekyll posts into content/posts.`)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises"
|
|
2
|
+
import { spawn } from "node:child_process"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
const root = process.cwd()
|
|
6
|
+
const quartzDir = path.join(root, ".word-pages", "quartz")
|
|
7
|
+
const npmCache = path.join(root, ".word-pages", "npm-cache")
|
|
8
|
+
const quartzRepo = "https://github.com/jackyzha0/quartz.git"
|
|
9
|
+
|
|
10
|
+
function run(command, args, options = {}) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const child = spawn(command, args, {
|
|
13
|
+
stdio: "inherit",
|
|
14
|
+
...options
|
|
15
|
+
})
|
|
16
|
+
child.on("exit", (code) => {
|
|
17
|
+
if (code === 0) resolve()
|
|
18
|
+
else reject(new Error(`${command} ${args.join(" ")} exited with ${code}`))
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function exists(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
await access(filePath)
|
|
26
|
+
return true
|
|
27
|
+
} catch {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!await exists(path.join(quartzDir, "package.json"))) {
|
|
33
|
+
await mkdir(path.dirname(quartzDir), { recursive: true })
|
|
34
|
+
await run("git", ["clone", "--depth", "1", quartzRepo, quartzDir])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!await exists(path.join(quartzDir, "node_modules"))) {
|
|
38
|
+
await mkdir(npmCache, { recursive: true })
|
|
39
|
+
await run("npm", ["install"], {
|
|
40
|
+
cwd: quartzDir,
|
|
41
|
+
env: {
|
|
42
|
+
...process.env,
|
|
43
|
+
npm_config_cache: npmCache
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const config = JSON.parse(await readFile(path.join(root, "word-pages.config.json"), "utf8"))
|
|
49
|
+
const configTemplate = await readFile(path.join(root, "quartz.config.ts"), "utf8")
|
|
50
|
+
const renderedConfig = configTemplate
|
|
51
|
+
.replaceAll("__WORD_PAGES_TITLE__", JSON.stringify(config.site.title))
|
|
52
|
+
.replaceAll("__WORD_PAGES_BASE_URL__", JSON.stringify(config.site.baseUrl))
|
|
53
|
+
|
|
54
|
+
await writeFile(path.join(quartzDir, "quartz.config.ts"), renderedConfig)
|
|
55
|
+
await run("npm", ["exec", "quartz", "--", "plugin", "install", "--from-config"], {
|
|
56
|
+
cwd: quartzDir,
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
npm_config_cache: npmCache
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
console.log(`Quartz is ready at ${quartzDir}.`)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { parseFrontmatter, stringifyFrontmatter } from "./frontmatter.mjs"
|
|
4
|
+
|
|
5
|
+
const root = process.cwd()
|
|
6
|
+
const config = JSON.parse(await readFile(path.join(root, "word-pages.config.json"), "utf8"))
|
|
7
|
+
const sourceRoot = path.join(root, config.content.source)
|
|
8
|
+
const stagedRoot = path.join(root, config.content.staged)
|
|
9
|
+
|
|
10
|
+
const allowedTypes = new Set(["page", "post", "note"])
|
|
11
|
+
|
|
12
|
+
function slugify(value) {
|
|
13
|
+
return String(value ?? "")
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/['"]/g, "")
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, "")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function collectMarkdown(dir) {
|
|
22
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
23
|
+
const files = []
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const fullPath = path.join(dir, entry.name)
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
if (entry.name === ".obsidian") continue
|
|
28
|
+
files.push(...await collectMarkdown(fullPath))
|
|
29
|
+
} else if (entry.name.endsWith(".md")) {
|
|
30
|
+
files.push(fullPath)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return files
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function routeFor(filePath, data) {
|
|
37
|
+
const type = String(data.type ?? "").toLowerCase()
|
|
38
|
+
if (!allowedTypes.has(type)) {
|
|
39
|
+
throw new Error(`${filePath} has unsupported type "${data.type}". Use page, post, or note.`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fallbackSlug = slugify(data.title ?? path.basename(filePath, ".md"))
|
|
43
|
+
const slug = slugify(data.slug ?? fallbackSlug)
|
|
44
|
+
if (!slug) throw new Error(`${filePath} needs a title or slug.`)
|
|
45
|
+
|
|
46
|
+
if (type === "post") return path.join("posts", `${slug}.md`)
|
|
47
|
+
if (type === "note") return path.join("notes", `${slug}.md`)
|
|
48
|
+
if (slug === "index" || slug === "home") return "index.md"
|
|
49
|
+
return `${slug}.md`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await rm(stagedRoot, { recursive: true, force: true })
|
|
53
|
+
await mkdir(stagedRoot, { recursive: true })
|
|
54
|
+
|
|
55
|
+
const files = await collectMarkdown(sourceRoot)
|
|
56
|
+
let published = 0
|
|
57
|
+
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const raw = await readFile(file, "utf8")
|
|
60
|
+
const parsed = parseFrontmatter(raw)
|
|
61
|
+
if (parsed.data.publish !== true) continue
|
|
62
|
+
|
|
63
|
+
const relativeOutput = routeFor(file, parsed.data)
|
|
64
|
+
const outputPath = path.join(stagedRoot, relativeOutput)
|
|
65
|
+
await mkdir(path.dirname(outputPath), { recursive: true })
|
|
66
|
+
|
|
67
|
+
const data = {
|
|
68
|
+
...parsed.data,
|
|
69
|
+
title: parsed.data.title ?? path.basename(file, ".md")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await writeFile(outputPath, stringifyFrontmatter(parsed.content.trimStart(), data))
|
|
73
|
+
published += 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (published === 0) {
|
|
77
|
+
throw new Error("No publishable Markdown files found. Add publish: true to at least one content file.")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`Prepared ${published} publishable Markdown files for Quartz.`)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
const root = process.cwd()
|
|
5
|
+
const quartzDir = path.join(root, ".word-pages", "quartz")
|
|
6
|
+
const npmCache = path.join(root, ".word-pages", "npm-cache")
|
|
7
|
+
const args = process.argv.slice(2)
|
|
8
|
+
|
|
9
|
+
const child = spawn("npm", ["exec", "quartz", "--", ...args], {
|
|
10
|
+
cwd: quartzDir,
|
|
11
|
+
stdio: "inherit",
|
|
12
|
+
env: {
|
|
13
|
+
...process.env,
|
|
14
|
+
npm_config_cache: npmCache
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
child.on("exit", (code) => {
|
|
19
|
+
process.exit(code ?? 1)
|
|
20
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Word Pages Wizard</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from "react"
|
|
2
|
+
import { createRoot } from "react-dom/client"
|
|
3
|
+
import "./styles.css"
|
|
4
|
+
|
|
5
|
+
type WordPagesConfig = {
|
|
6
|
+
site: {
|
|
7
|
+
title: string
|
|
8
|
+
description: string
|
|
9
|
+
author: string
|
|
10
|
+
githubUsername: string
|
|
11
|
+
repositoryName: string
|
|
12
|
+
visibility: "ask" | "public" | "private"
|
|
13
|
+
baseUrl: string
|
|
14
|
+
}
|
|
15
|
+
theme: {
|
|
16
|
+
preset: string
|
|
17
|
+
}
|
|
18
|
+
content: {
|
|
19
|
+
source: string
|
|
20
|
+
staged: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function deriveBaseUrl(username: string, repositoryName: string) {
|
|
25
|
+
if (!username || !repositoryName) return ""
|
|
26
|
+
if (repositoryName.toLowerCase() === `${username.toLowerCase()}.github.io`) return `${username}.github.io`
|
|
27
|
+
return `${username}.github.io/${repositoryName}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function displayPagesUrl(baseUrl: string) {
|
|
31
|
+
return baseUrl ? `https://${baseUrl}/` : "Configure GitHub details to preview the Pages URL."
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function App() {
|
|
35
|
+
const [config, setConfig] = useState<WordPagesConfig | null>(null)
|
|
36
|
+
const [status, setStatus] = useState("Loading")
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
fetch("/api/config")
|
|
40
|
+
.then((response) => response.json())
|
|
41
|
+
.then((data) => {
|
|
42
|
+
setConfig(data)
|
|
43
|
+
setStatus("Ready")
|
|
44
|
+
})
|
|
45
|
+
.catch(() => setStatus("Could not load configuration"))
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const pageUrl = useMemo(() => {
|
|
49
|
+
if (!config?.site.githubUsername || !config.site.repositoryName) return "Configure GitHub details to preview the Pages URL."
|
|
50
|
+
return displayPagesUrl(deriveBaseUrl(config.site.githubUsername, config.site.repositoryName))
|
|
51
|
+
}, [config])
|
|
52
|
+
|
|
53
|
+
if (!config) {
|
|
54
|
+
return <main className="shell"><p>{status}</p></main>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function updateSite<K extends keyof WordPagesConfig["site"]>(key: K, value: WordPagesConfig["site"][K]) {
|
|
58
|
+
setConfig((current) => current && {
|
|
59
|
+
...current,
|
|
60
|
+
site: {
|
|
61
|
+
...current.site,
|
|
62
|
+
[key]: value,
|
|
63
|
+
baseUrl: key === "githubUsername" || key === "repositoryName"
|
|
64
|
+
? deriveBaseUrl(
|
|
65
|
+
key === "githubUsername" ? String(value) : current.site.githubUsername,
|
|
66
|
+
key === "repositoryName" ? String(value) : current.site.repositoryName
|
|
67
|
+
)
|
|
68
|
+
: current.site.baseUrl
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function save() {
|
|
74
|
+
setStatus("Saving")
|
|
75
|
+
const response = await fetch("/api/config", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
body: JSON.stringify(config)
|
|
79
|
+
})
|
|
80
|
+
setStatus(response.ok ? "Saved" : "Save failed")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<main className="shell">
|
|
85
|
+
<section className="hero">
|
|
86
|
+
<div>
|
|
87
|
+
<p className="eyebrow">Word Pages setup</p>
|
|
88
|
+
<h1>Configure your Obsidian-powered site.</h1>
|
|
89
|
+
<p className="lede">This wizard writes local configuration only. It does not ask for GitHub credentials and does not upload your vault.</p>
|
|
90
|
+
</div>
|
|
91
|
+
<button onClick={save}>Save</button>
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<section className="grid">
|
|
95
|
+
<label>
|
|
96
|
+
Site title
|
|
97
|
+
<input value={config.site.title} onChange={(event) => updateSite("title", event.target.value)} />
|
|
98
|
+
</label>
|
|
99
|
+
<label>
|
|
100
|
+
Author or organization
|
|
101
|
+
<input value={config.site.author} onChange={(event) => updateSite("author", event.target.value)} />
|
|
102
|
+
</label>
|
|
103
|
+
<label className="wide">
|
|
104
|
+
Description
|
|
105
|
+
<textarea value={config.site.description} onChange={(event) => updateSite("description", event.target.value)} />
|
|
106
|
+
</label>
|
|
107
|
+
<label>
|
|
108
|
+
GitHub username
|
|
109
|
+
<input value={config.site.githubUsername} onChange={(event) => updateSite("githubUsername", event.target.value)} />
|
|
110
|
+
</label>
|
|
111
|
+
<label>
|
|
112
|
+
Repository name
|
|
113
|
+
<input value={config.site.repositoryName} onChange={(event) => updateSite("repositoryName", event.target.value)} />
|
|
114
|
+
</label>
|
|
115
|
+
<label>
|
|
116
|
+
Repository visibility
|
|
117
|
+
<select value={config.site.visibility} onChange={(event) => updateSite("visibility", event.target.value as WordPagesConfig["site"]["visibility"])}>
|
|
118
|
+
<option value="ask">Decide before publishing</option>
|
|
119
|
+
<option value="public">Public</option>
|
|
120
|
+
<option value="private">Private</option>
|
|
121
|
+
</select>
|
|
122
|
+
</label>
|
|
123
|
+
<label>
|
|
124
|
+
GitHub Pages base URL
|
|
125
|
+
<input value={config.site.baseUrl} onChange={(event) => updateSite("baseUrl", event.target.value)} />
|
|
126
|
+
</label>
|
|
127
|
+
</section>
|
|
128
|
+
|
|
129
|
+
<section className="notice">
|
|
130
|
+
<h2>Publishing boundary</h2>
|
|
131
|
+
<p><strong>Rendered site:</strong> only Markdown with <code>publish: true</code> is staged for Quartz.</p>
|
|
132
|
+
<p><strong>Repository source:</strong> if this repo is public, committed Markdown may be visible on GitHub even when it is not rendered.</p>
|
|
133
|
+
<p><strong>Expected Pages URL:</strong> {pageUrl}</p>
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
<footer>
|
|
137
|
+
<span>{status}</span>
|
|
138
|
+
<span>Next: open <code>content/</code> in Obsidian, then run <code>npm run preview</code>.</span>
|
|
139
|
+
</footer>
|
|
140
|
+
</main>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
createRoot(document.getElementById("root")!).render(<App />)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color: #1d1b16;
|
|
3
|
+
background: #f9f6ee;
|
|
4
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.shell {
|
|
16
|
+
width: min(1040px, calc(100vw - 32px));
|
|
17
|
+
margin: 0 auto;
|
|
18
|
+
padding: 40px 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.hero {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: end;
|
|
24
|
+
justify-content: space-between;
|
|
25
|
+
gap: 24px;
|
|
26
|
+
padding-bottom: 28px;
|
|
27
|
+
border-bottom: 1px solid #ded8ca;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.eyebrow {
|
|
31
|
+
margin: 0 0 10px;
|
|
32
|
+
color: #28786f;
|
|
33
|
+
font-size: 0.82rem;
|
|
34
|
+
font-weight: 700;
|
|
35
|
+
text-transform: uppercase;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
h1 {
|
|
39
|
+
max-width: 720px;
|
|
40
|
+
margin: 0;
|
|
41
|
+
font-size: clamp(2rem, 4vw, 3.7rem);
|
|
42
|
+
line-height: 1.02;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.lede {
|
|
46
|
+
max-width: 650px;
|
|
47
|
+
color: #5f594f;
|
|
48
|
+
font-size: 1.05rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
button {
|
|
52
|
+
min-height: 44px;
|
|
53
|
+
border: 0;
|
|
54
|
+
border-radius: 6px;
|
|
55
|
+
padding: 0 20px;
|
|
56
|
+
color: white;
|
|
57
|
+
background: #28786f;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.grid {
|
|
63
|
+
display: grid;
|
|
64
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
65
|
+
gap: 18px;
|
|
66
|
+
margin: 28px 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
label {
|
|
70
|
+
display: grid;
|
|
71
|
+
gap: 8px;
|
|
72
|
+
color: #413d35;
|
|
73
|
+
font-weight: 700;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.wide {
|
|
77
|
+
grid-column: 1 / -1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
input,
|
|
81
|
+
select,
|
|
82
|
+
textarea {
|
|
83
|
+
width: 100%;
|
|
84
|
+
border: 1px solid #cfc7b7;
|
|
85
|
+
border-radius: 6px;
|
|
86
|
+
padding: 12px;
|
|
87
|
+
color: #1d1b16;
|
|
88
|
+
background: #fffdf8;
|
|
89
|
+
font: inherit;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
textarea {
|
|
93
|
+
min-height: 104px;
|
|
94
|
+
resize: vertical;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.notice {
|
|
98
|
+
border: 1px solid #ded8ca;
|
|
99
|
+
border-radius: 8px;
|
|
100
|
+
padding: 20px;
|
|
101
|
+
background: #fffdf8;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.notice h2 {
|
|
105
|
+
margin-top: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
code {
|
|
109
|
+
border-radius: 4px;
|
|
110
|
+
padding: 2px 5px;
|
|
111
|
+
background: #ece6d8;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
footer {
|
|
115
|
+
display: flex;
|
|
116
|
+
justify-content: space-between;
|
|
117
|
+
gap: 20px;
|
|
118
|
+
margin-top: 24px;
|
|
119
|
+
color: #6f675c;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@media (max-width: 720px) {
|
|
123
|
+
.hero,
|
|
124
|
+
footer {
|
|
125
|
+
align-items: stretch;
|
|
126
|
+
flex-direction: column;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.grid {
|
|
130
|
+
grid-template-columns: 1fr;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { defineConfig } from "vite"
|
|
2
|
+
import react from "@vitejs/plugin-react"
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
|
|
6
|
+
const configPath = path.resolve(process.cwd(), "word-pages.config.json")
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
root: "wizard",
|
|
10
|
+
plugins: [
|
|
11
|
+
react(),
|
|
12
|
+
{
|
|
13
|
+
name: "word-pages-config-api",
|
|
14
|
+
configureServer(server) {
|
|
15
|
+
server.middlewares.use("/api/config", async (req, res) => {
|
|
16
|
+
if (req.method === "GET") {
|
|
17
|
+
const body = await readFile(configPath, "utf8")
|
|
18
|
+
res.setHeader("Content-Type", "application/json")
|
|
19
|
+
res.end(body)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (req.method === "POST") {
|
|
24
|
+
let raw = ""
|
|
25
|
+
req.on("data", (chunk) => {
|
|
26
|
+
raw += chunk
|
|
27
|
+
})
|
|
28
|
+
req.on("end", async () => {
|
|
29
|
+
const parsed = JSON.parse(raw)
|
|
30
|
+
await writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`)
|
|
31
|
+
res.setHeader("Content-Type", "application/json")
|
|
32
|
+
res.end(JSON.stringify({ ok: true }))
|
|
33
|
+
})
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
res.statusCode = 405
|
|
38
|
+
res.end("Method not allowed")
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"site": {
|
|
3
|
+
"title": "Word Pages",
|
|
4
|
+
"description": "A public knowledge base, blog, and portfolio managed from Obsidian.",
|
|
5
|
+
"author": "Your Name",
|
|
6
|
+
"githubUsername": "",
|
|
7
|
+
"repositoryName": "__WORD_PAGES_PROJECT_NAME__",
|
|
8
|
+
"visibility": "ask",
|
|
9
|
+
"baseUrl": "/"
|
|
10
|
+
},
|
|
11
|
+
"theme": {
|
|
12
|
+
"preset": "clean-garden"
|
|
13
|
+
},
|
|
14
|
+
"content": {
|
|
15
|
+
"source": "content",
|
|
16
|
+
"staged": ".word-pages/quartz-content"
|
|
17
|
+
}
|
|
18
|
+
}
|