@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 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.0",
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) => response.json())
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 <main className="shell"><p>{status}</p></main>
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
- 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")
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}>Save</button>
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
- 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`)
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(JSON.stringify({ ok: true }))
33
- })
34
- return
35
- }
20
+ res.end(body)
21
+ return
22
+ }
36
23
 
37
- res.statusCode = 405
38
- res.end("Method not allowed")
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
  }