@codemeall/create-word-pages 0.1.0 → 0.1.2
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 +116 -0
- package/package.json +2 -2
- package/template/wizard/src/main.tsx +84 -11
- package/template/wizard/src/styles.css +88 -2
- package/template/wizard/vite.config.ts +40 -21
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Word Pages
|
|
2
|
+
|
|
3
|
+
Word Pages turns an Obsidian-authored Markdown vault into a Quartz-powered portfolio, blog, and knowledge-base site that can be deployed to GitHub Pages.
|
|
4
|
+
|
|
5
|
+
It is designed for people who want to manage content locally in Obsidian, keep source files in GitHub, and publish a static public site without paid sync or hosted storage.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Requirements:
|
|
10
|
+
|
|
11
|
+
- Node.js 22 or newer
|
|
12
|
+
- npm 10.9 or newer
|
|
13
|
+
- A GitHub account
|
|
14
|
+
- Obsidian, optional but recommended
|
|
15
|
+
|
|
16
|
+
Create a new site:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @codemeall/create-word-pages my-site
|
|
20
|
+
cd my-site
|
|
21
|
+
npm install
|
|
22
|
+
npm run wizard
|
|
23
|
+
npm run preview
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Open the `content/` folder in Obsidian and start editing.
|
|
27
|
+
|
|
28
|
+
## How Publishing Works
|
|
29
|
+
|
|
30
|
+
Word Pages uses `content/` as the Obsidian vault. Markdown files render only when they include:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
---
|
|
34
|
+
title: "My page"
|
|
35
|
+
type: note
|
|
36
|
+
publish: true
|
|
37
|
+
---
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Supported content types:
|
|
41
|
+
|
|
42
|
+
- `page`: portfolio/home/about pages
|
|
43
|
+
- `post`: blog posts under `/posts/`
|
|
44
|
+
- `note`: knowledge-base notes under `/notes/`
|
|
45
|
+
|
|
46
|
+
The generated site is built by Quartz and can be deployed by the included GitHub Actions workflow.
|
|
47
|
+
|
|
48
|
+
## GitHub Pages Setup
|
|
49
|
+
|
|
50
|
+
1. Create a new GitHub repository.
|
|
51
|
+
2. Push the generated site folder to that repository.
|
|
52
|
+
3. In GitHub, open `Settings -> Pages`.
|
|
53
|
+
4. Set the source to `GitHub Actions`.
|
|
54
|
+
5. Push to `main`.
|
|
55
|
+
|
|
56
|
+
The included workflow builds and deploys the site automatically.
|
|
57
|
+
|
|
58
|
+
For project pages, run the wizard and set your GitHub username and repository name so the site URL is configured correctly.
|
|
59
|
+
|
|
60
|
+
## Privacy Rule
|
|
61
|
+
|
|
62
|
+
`publish: true` means “render this file into the public website.”
|
|
63
|
+
|
|
64
|
+
It does not make the Markdown source private. If the GitHub repository is public, committed Markdown files may be visible on GitHub even when they are not rendered.
|
|
65
|
+
|
|
66
|
+
For private drafts, use a private repository or keep drafts outside the published repo.
|
|
67
|
+
|
|
68
|
+
## Import Existing Jekyll Posts
|
|
69
|
+
|
|
70
|
+
If you have a Jekyll blog with `_posts`, import it like this:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm run import:jekyll -- /path/to/site/_posts
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The importer copies posts into `content/posts`, adds `type: post`, adds `publish: true`, and preserves common frontmatter such as title, date, tags, category, and description.
|
|
77
|
+
|
|
78
|
+
## Common Commands
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm run wizard
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Configure site identity, GitHub Pages URL details, and visibility guidance.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm run preview
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Build and serve the site locally.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm run build
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Build the static site for production.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm run prepare:content
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Stage only publishable Markdown into the Quartz build input.
|
|
103
|
+
|
|
104
|
+
## Client Handoff Checklist
|
|
105
|
+
|
|
106
|
+
- Create the site with `npx @codemeall/create-word-pages client-site`.
|
|
107
|
+
- Run the wizard and set the correct GitHub username and repository name.
|
|
108
|
+
- Open `content/` in Obsidian.
|
|
109
|
+
- Replace the sample home, about, post, and note files.
|
|
110
|
+
- Commit and push to GitHub.
|
|
111
|
+
- Enable GitHub Pages with GitHub Actions.
|
|
112
|
+
- Confirm the first deployment succeeds.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemeall/create-word-pages",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Scaffold a Word Pages Obsidian-to-GitHub-Pages starter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
"npm": ">=10.9"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT"
|
|
21
|
-
}
|
|
21
|
+
}
|
|
@@ -2,6 +2,8 @@ import React, { useEffect, useMemo, useState } from "react"
|
|
|
2
2
|
import { createRoot } from "react-dom/client"
|
|
3
3
|
import "./styles.css"
|
|
4
4
|
|
|
5
|
+
type SaveState = "idle" | "dirty" | "saving" | "saved" | "failed"
|
|
6
|
+
|
|
5
7
|
type WordPagesConfig = {
|
|
6
8
|
site: {
|
|
7
9
|
title: string
|
|
@@ -34,15 +36,26 @@ function displayPagesUrl(baseUrl: string) {
|
|
|
34
36
|
function App() {
|
|
35
37
|
const [config, setConfig] = useState<WordPagesConfig | null>(null)
|
|
36
38
|
const [status, setStatus] = useState("Loading")
|
|
39
|
+
const [saveState, setSaveState] = useState<SaveState>("idle")
|
|
40
|
+
const [message, setMessage] = useState("Load the wizard, edit settings, then save.")
|
|
41
|
+
const [lastSavedAt, setLastSavedAt] = useState("")
|
|
37
42
|
|
|
38
43
|
useEffect(() => {
|
|
39
44
|
fetch("/api/config")
|
|
40
|
-
.then((response) =>
|
|
45
|
+
.then((response) => {
|
|
46
|
+
if (!response.ok) throw new Error("Could not load word-pages.config.json")
|
|
47
|
+
return response.json()
|
|
48
|
+
})
|
|
41
49
|
.then((data) => {
|
|
42
50
|
setConfig(data)
|
|
43
51
|
setStatus("Ready")
|
|
52
|
+
setMessage("Configuration loaded from word-pages.config.json.")
|
|
53
|
+
})
|
|
54
|
+
.catch((error) => {
|
|
55
|
+
setStatus("Could not load configuration")
|
|
56
|
+
setSaveState("failed")
|
|
57
|
+
setMessage(error instanceof Error ? error.message : "Restart npm run wizard from the site root.")
|
|
44
58
|
})
|
|
45
|
-
.catch(() => setStatus("Could not load configuration"))
|
|
46
59
|
}, [])
|
|
47
60
|
|
|
48
61
|
const pageUrl = useMemo(() => {
|
|
@@ -51,7 +64,16 @@ function App() {
|
|
|
51
64
|
}, [config])
|
|
52
65
|
|
|
53
66
|
if (!config) {
|
|
54
|
-
return
|
|
67
|
+
return (
|
|
68
|
+
<main className="shell">
|
|
69
|
+
<section className={`status-panel ${saveState}`} role="status" aria-live="polite">
|
|
70
|
+
<div>
|
|
71
|
+
<strong>{status}</strong>
|
|
72
|
+
<p>{message}</p>
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
</main>
|
|
76
|
+
)
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
function updateSite<K extends keyof WordPagesConfig["site"]>(key: K, value: WordPagesConfig["site"][K]) {
|
|
@@ -68,16 +90,36 @@ function App() {
|
|
|
68
90
|
: current.site.baseUrl
|
|
69
91
|
}
|
|
70
92
|
})
|
|
93
|
+
setSaveState("dirty")
|
|
94
|
+
setMessage("Unsaved changes. Click Save changes to write word-pages.config.json.")
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
async function save() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
try {
|
|
99
|
+
setStatus("Saving")
|
|
100
|
+
setSaveState("saving")
|
|
101
|
+
setMessage("Saving word-pages.config.json...")
|
|
102
|
+
|
|
103
|
+
const response = await fetch("/api/config", {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
body: JSON.stringify(config)
|
|
107
|
+
})
|
|
108
|
+
const result = await response.json().catch(() => ({ message: "No response body" }))
|
|
109
|
+
if (!response.ok || result.ok === false) {
|
|
110
|
+
throw new Error(result.message ?? "Save failed")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const savedAt = new Date().toLocaleTimeString()
|
|
114
|
+
setStatus("Saved")
|
|
115
|
+
setSaveState("saved")
|
|
116
|
+
setLastSavedAt(savedAt)
|
|
117
|
+
setMessage(`Saved to word-pages.config.json at ${savedAt}. Run npm run preview in another terminal to rebuild the site.`)
|
|
118
|
+
} catch (error) {
|
|
119
|
+
setStatus("Save failed")
|
|
120
|
+
setSaveState("failed")
|
|
121
|
+
setMessage(error instanceof Error ? error.message : "Save failed")
|
|
122
|
+
}
|
|
81
123
|
}
|
|
82
124
|
|
|
83
125
|
return (
|
|
@@ -88,7 +130,17 @@ function App() {
|
|
|
88
130
|
<h1>Configure your Obsidian-powered site.</h1>
|
|
89
131
|
<p className="lede">This wizard writes local configuration only. It does not ask for GitHub credentials and does not upload your vault.</p>
|
|
90
132
|
</div>
|
|
91
|
-
<button onClick={save}>
|
|
133
|
+
<button type="button" onClick={save} disabled={saveState === "saving"}>
|
|
134
|
+
{saveState === "saving" ? "Saving..." : "Save changes"}
|
|
135
|
+
</button>
|
|
136
|
+
</section>
|
|
137
|
+
|
|
138
|
+
<section className={`status-panel ${saveState}`} role="status" aria-live="polite">
|
|
139
|
+
<div>
|
|
140
|
+
<strong>{status}</strong>
|
|
141
|
+
<p>{message}</p>
|
|
142
|
+
</div>
|
|
143
|
+
{lastSavedAt && <span>Last saved {lastSavedAt}</span>}
|
|
92
144
|
</section>
|
|
93
145
|
|
|
94
146
|
<section className="grid">
|
|
@@ -133,6 +185,27 @@ function App() {
|
|
|
133
185
|
<p><strong>Expected Pages URL:</strong> {pageUrl}</p>
|
|
134
186
|
</section>
|
|
135
187
|
|
|
188
|
+
<section className="next-steps">
|
|
189
|
+
<h2>Next steps</h2>
|
|
190
|
+
<div className="steps-grid">
|
|
191
|
+
<div>
|
|
192
|
+
<span>1</span>
|
|
193
|
+
<h3>Edit content</h3>
|
|
194
|
+
<p>Open <code>content/</code> in Obsidian and update pages, posts, and notes.</p>
|
|
195
|
+
</div>
|
|
196
|
+
<div>
|
|
197
|
+
<span>2</span>
|
|
198
|
+
<h3>Preview locally</h3>
|
|
199
|
+
<p>Run <code>npm run preview</code> in a second terminal. Quartz serves the site at its local preview URL.</p>
|
|
200
|
+
</div>
|
|
201
|
+
<div>
|
|
202
|
+
<span>3</span>
|
|
203
|
+
<h3>Publish</h3>
|
|
204
|
+
<p>Commit, push to GitHub, and enable Pages with GitHub Actions.</p>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</section>
|
|
208
|
+
|
|
136
209
|
<footer>
|
|
137
210
|
<span>{status}</span>
|
|
138
211
|
<span>Next: open <code>content/</code> in Obsidian, then run <code>npm run preview</code>.</span>
|
|
@@ -59,6 +59,48 @@ button {
|
|
|
59
59
|
cursor: pointer;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
button:disabled {
|
|
63
|
+
cursor: wait;
|
|
64
|
+
opacity: 0.72;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.status-panel {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: space-between;
|
|
71
|
+
gap: 16px;
|
|
72
|
+
margin: 24px 0 0;
|
|
73
|
+
border: 1px solid #d8d0c0;
|
|
74
|
+
border-left: 5px solid #8a8376;
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
padding: 16px;
|
|
77
|
+
background: #fffdf8;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.status-panel p {
|
|
81
|
+
margin: 4px 0 0;
|
|
82
|
+
color: #5f594f;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.status-panel.saved {
|
|
86
|
+
border-left-color: #28786f;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.status-panel.dirty,
|
|
90
|
+
.status-panel.saving {
|
|
91
|
+
border-left-color: #8d5c2c;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.status-panel.failed {
|
|
95
|
+
border-left-color: #b83232;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.status-panel > span {
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
color: #6f675c;
|
|
101
|
+
font-size: 0.92rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
62
104
|
.grid {
|
|
63
105
|
display: grid;
|
|
64
106
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -105,6 +147,44 @@ textarea {
|
|
|
105
147
|
margin-top: 0;
|
|
106
148
|
}
|
|
107
149
|
|
|
150
|
+
.next-steps {
|
|
151
|
+
margin-top: 24px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.steps-grid {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
157
|
+
gap: 16px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.steps-grid > div {
|
|
161
|
+
border: 1px solid #ded8ca;
|
|
162
|
+
border-radius: 8px;
|
|
163
|
+
padding: 18px;
|
|
164
|
+
background: #fffdf8;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.steps-grid span {
|
|
168
|
+
display: inline-grid;
|
|
169
|
+
width: 28px;
|
|
170
|
+
height: 28px;
|
|
171
|
+
place-items: center;
|
|
172
|
+
border-radius: 50%;
|
|
173
|
+
color: white;
|
|
174
|
+
background: #28786f;
|
|
175
|
+
font-size: 0.9rem;
|
|
176
|
+
font-weight: 800;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.steps-grid h3 {
|
|
180
|
+
margin: 14px 0 8px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.steps-grid p {
|
|
184
|
+
margin: 0;
|
|
185
|
+
color: #5f594f;
|
|
186
|
+
}
|
|
187
|
+
|
|
108
188
|
code {
|
|
109
189
|
border-radius: 4px;
|
|
110
190
|
padding: 2px 5px;
|
|
@@ -121,12 +201,18 @@ footer {
|
|
|
121
201
|
|
|
122
202
|
@media (max-width: 720px) {
|
|
123
203
|
.hero,
|
|
124
|
-
footer
|
|
204
|
+
footer,
|
|
205
|
+
.status-panel {
|
|
125
206
|
align-items: stretch;
|
|
126
207
|
flex-direction: column;
|
|
127
208
|
}
|
|
128
209
|
|
|
129
|
-
.grid
|
|
210
|
+
.grid,
|
|
211
|
+
.steps-grid {
|
|
130
212
|
grid-template-columns: 1fr;
|
|
131
213
|
}
|
|
214
|
+
|
|
215
|
+
.status-panel > span {
|
|
216
|
+
white-space: normal;
|
|
217
|
+
}
|
|
132
218
|
}
|
|
@@ -13,29 +13,48 @@ export default defineConfig({
|
|
|
13
13
|
name: "word-pages-config-api",
|
|
14
14
|
configureServer(server) {
|
|
15
15
|
server.middlewares.use("/api/config", async (req, res) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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`)
|
|
16
|
+
try {
|
|
17
|
+
if (req.method === "GET") {
|
|
18
|
+
const body = await readFile(configPath, "utf8")
|
|
31
19
|
res.setHeader("Content-Type", "application/json")
|
|
32
|
-
res.end(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
20
|
+
res.end(body)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
if (req.method === "POST") {
|
|
25
|
+
let raw = ""
|
|
26
|
+
req.on("data", (chunk) => {
|
|
27
|
+
raw += chunk
|
|
28
|
+
})
|
|
29
|
+
req.on("end", async () => {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(raw)
|
|
32
|
+
await writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`)
|
|
33
|
+
res.setHeader("Content-Type", "application/json")
|
|
34
|
+
res.end(JSON.stringify({ ok: true, path: configPath }))
|
|
35
|
+
} catch (error) {
|
|
36
|
+
res.statusCode = 500
|
|
37
|
+
res.setHeader("Content-Type", "application/json")
|
|
38
|
+
res.end(JSON.stringify({
|
|
39
|
+
ok: false,
|
|
40
|
+
message: error instanceof Error ? error.message : "Could not save configuration"
|
|
41
|
+
}))
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
res.statusCode = 405
|
|
48
|
+
res.setHeader("Content-Type", "application/json")
|
|
49
|
+
res.end(JSON.stringify({ ok: false, message: "Method not allowed" }))
|
|
50
|
+
} catch (error) {
|
|
51
|
+
res.statusCode = 500
|
|
52
|
+
res.setHeader("Content-Type", "application/json")
|
|
53
|
+
res.end(JSON.stringify({
|
|
54
|
+
ok: false,
|
|
55
|
+
message: error instanceof Error ? error.message : "Configuration API failed"
|
|
56
|
+
}))
|
|
57
|
+
}
|
|
39
58
|
})
|
|
40
59
|
}
|
|
41
60
|
}
|