fossbook 0.0.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 +246 -0
- package/bin/fossbook.js +119 -0
- package/lib/all_posts.js +32 -0
- package/lib/assets/placeholder.svg +4 -0
- package/lib/deploy.js +202 -0
- package/lib/home.js +59 -0
- package/lib/index.js +79 -0
- package/lib/init.js +253 -0
- package/lib/mod/config.js +98 -0
- package/lib/mod/marked.js +95 -0
- package/lib/mod/page.js +115 -0
- package/lib/mod/page_base.js +97 -0
- package/lib/new.js +48 -0
- package/lib/posts.js +42 -0
- package/lib/server.js +14 -0
- package/lib/tag/index.js +69 -0
- package/lib/tag/tag.js +29 -0
- package/package.json +45 -0
- package/themes/archie/assets/fonts/ComicNeue-Bold.ttf +0 -0
- package/themes/archie/assets/fonts/ComicNeue-BoldItalic.ttf +0 -0
- package/themes/archie/assets/fonts/ComicNeue-Italic.ttf +0 -0
- package/themes/archie/assets/fonts/ComicNeue-Light.ttf +0 -0
- package/themes/archie/assets/fonts/ComicNeue-LightItalic.ttf +0 -0
- package/themes/archie/assets/fonts/ComicNeue-Regular.ttf +0 -0
- package/themes/archie/assets/fonts/LyonDisplay-Bold.otf +0 -0
- package/themes/archie/assets/fonts/OFL.txt +93 -0
- package/themes/archie/assets/fonts/fira-sans-v10-latin-regular.eot +0 -0
- package/themes/archie/assets/fonts/fira-sans-v10-latin-regular.svg +330 -0
- package/themes/archie/assets/fonts/fira-sans-v10-latin-regular.ttf +0 -0
- package/themes/archie/assets/fonts/fira-sans-v10-latin-regular.woff +0 -0
- package/themes/archie/assets/fonts/fira-sans-v10-latin-regular.woff2 +0 -0
- package/themes/archie/assets/fonts/ibm-plex-mono-v6-latin-500italic.eot +0 -0
- package/themes/archie/assets/fonts/ibm-plex-mono-v6-latin-500italic.svg +365 -0
- package/themes/archie/assets/fonts/ibm-plex-mono-v6-latin-500italic.ttf +0 -0
- package/themes/archie/assets/fonts/ibm-plex-mono-v6-latin-500italic.woff +0 -0
- package/themes/archie/assets/fonts/ibm-plex-mono-v6-latin-500italic.woff2 +0 -0
- package/themes/archie/assets/fonts/roboto-mono-v12-latin-regular.eot +0 -0
- package/themes/archie/assets/fonts/roboto-mono-v12-latin-regular.svg +405 -0
- package/themes/archie/assets/fonts/roboto-mono-v12-latin-regular.ttf +0 -0
- package/themes/archie/assets/fonts/roboto-mono-v12-latin-regular.woff +0 -0
- package/themes/archie/assets/fonts/roboto-mono-v12-latin-regular.woff2 +0 -0
- package/themes/archie/assets/images/node-ssg-1.png +0 -0
- package/themes/archie/assets/images/node-ssg-2.png +0 -0
- package/themes/archie/assets/styles/fonts.css +51 -0
- package/themes/archie/assets/styles/highlights.css +75 -0
- package/themes/archie/assets/styles/main.css +402 -0
- package/themes/archie/layouts/all_posts.html +42 -0
- package/themes/archie/layouts/home.html +62 -0
- package/themes/archie/layouts/page.html +50 -0
- package/themes/archie/layouts/partials/footer.html +10 -0
- package/themes/archie/layouts/post.html +82 -0
- package/themes/archie/layouts/tag.html +43 -0
- package/themes/archie/layouts/tag_list.html +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Fossbook
|
|
2
|
+
|
|
3
|
+
A lightweight static blog site generator for GitHub Pages, similar to Hugo but built with Node.js.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Markdown-based** — Write posts in Markdown with YAML front-matter
|
|
8
|
+
- **Pagination** — Automatic home page pagination
|
|
9
|
+
- **Tags** — Tag-based categorization with tag index and per-tag listing pages
|
|
10
|
+
- **Theming** — Bundled Archie theme with support for custom themes
|
|
11
|
+
- **SEO** — Open Graph and Twitter Card meta tags out of the box
|
|
12
|
+
- **GitHub Pages** — Built-in CNAME support for custom domains
|
|
13
|
+
- **Dev server** — Local preview server with Express
|
|
14
|
+
- **Syntax highlighting** — Code block highlighting via highlight.js
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Install globally
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g fossbook
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Create a new site
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
mkdir my-blog && cd my-blog
|
|
28
|
+
fossbook init
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This creates the following structure:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
my-blog/
|
|
35
|
+
├── content/
|
|
36
|
+
│ ├── about.md
|
|
37
|
+
│ └── posts/
|
|
38
|
+
├── static/
|
|
39
|
+
│ └── images/
|
|
40
|
+
├── fossbook.config.js
|
|
41
|
+
└── package.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Create a new post
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
fossbook new "My First Post"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This creates `content/posts/My First Post/index.md` with pre-filled front-matter and an `images/` directory.
|
|
51
|
+
|
|
52
|
+
### Build the site
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
fossbook build
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Preview locally
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
fossbook serve
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Open http://localhost:3000 to view your site.
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
Create a `fossbook.config.js` in your project root:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
module.exports = {
|
|
72
|
+
blogName: "My Blog",
|
|
73
|
+
authorName: "Your Name",
|
|
74
|
+
authorDescription: "A short bio",
|
|
75
|
+
authorWebsite: "https://example.com",
|
|
76
|
+
blogDescription: "A blog about things",
|
|
77
|
+
blogsite: "https://example.com",
|
|
78
|
+
|
|
79
|
+
// Optional
|
|
80
|
+
githubCNAME: "example.com",
|
|
81
|
+
googleAnalyticsID: "",
|
|
82
|
+
authorTwitter: "@you",
|
|
83
|
+
siteTwitter: "@yourblog",
|
|
84
|
+
githubRepository: "https://github.com/you/your-blog",
|
|
85
|
+
image: "https://example.com/default-image.png",
|
|
86
|
+
theme: "archie",
|
|
87
|
+
|
|
88
|
+
// Directory overrides (defaults shown)
|
|
89
|
+
content: "./content",
|
|
90
|
+
postsDir: "./content/posts",
|
|
91
|
+
outputDir: "./public",
|
|
92
|
+
staticDir: "./static",
|
|
93
|
+
themesDir: "./themes",
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Content Format
|
|
98
|
+
|
|
99
|
+
### Post front-matter
|
|
100
|
+
|
|
101
|
+
```markdown
|
|
102
|
+
---
|
|
103
|
+
title: My Post Title
|
|
104
|
+
date: 2026-02-17
|
|
105
|
+
description: "A brief summary of the post"
|
|
106
|
+
image: "feature.png"
|
|
107
|
+
tags: "JavaScript, Node.js, Static Site"
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
Your Markdown content here...
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Directory structure
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
content/posts/My Post Title/
|
|
117
|
+
├── index.md
|
|
118
|
+
└── images/
|
|
119
|
+
└── feature.png
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## CLI Reference
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Usage: fossbook <command> [options]
|
|
126
|
+
|
|
127
|
+
Commands:
|
|
128
|
+
build Build the static site
|
|
129
|
+
serve Build and start a local dev server
|
|
130
|
+
new <title> Create a new post
|
|
131
|
+
init Create a new fossbook site project
|
|
132
|
+
|
|
133
|
+
Options:
|
|
134
|
+
-c, --config Path to config file (default: ./fossbook.config.js)
|
|
135
|
+
-o, --output Output directory (default: ./public)
|
|
136
|
+
-p, --port Dev server port (default: 3000)
|
|
137
|
+
-v, --version Show version number
|
|
138
|
+
-h, --help Show help
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Theming
|
|
142
|
+
|
|
143
|
+
Fossbook ships with the Archie theme by default. To use a custom theme:
|
|
144
|
+
|
|
145
|
+
1. Create a `themes/<your-theme>/` directory in your project
|
|
146
|
+
2. Add `layouts/` (HTML templates) and `assets/` (CSS, fonts, images)
|
|
147
|
+
3. Set `theme: "your-theme"` in `fossbook.config.js`
|
|
148
|
+
|
|
149
|
+
Theme resolution order: user project `themes/` → built-in `themes/`.
|
|
150
|
+
|
|
151
|
+
### Required layout files
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
layouts/
|
|
155
|
+
├── home.html # Home page with pagination
|
|
156
|
+
├── post.html # Individual post page
|
|
157
|
+
├── page.html # Static pages (e.g., about)
|
|
158
|
+
├── all_posts.html # All posts listing
|
|
159
|
+
├── tag.html # Per-tag listing
|
|
160
|
+
├── tag_list.html # Tag index page
|
|
161
|
+
└── partials/
|
|
162
|
+
└── footer.html # Footer partial
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Deploying to GitHub Pages
|
|
166
|
+
|
|
167
|
+
Fossbook can automatically deploy your blog to GitHub Pages using GitHub Actions.
|
|
168
|
+
|
|
169
|
+
### Prerequisites: Install GitHub CLI (`gh`)
|
|
170
|
+
|
|
171
|
+
The `fossbook deploy` command uses the GitHub CLI to create repositories and monitor deployments.
|
|
172
|
+
|
|
173
|
+
**Linux (Debian/Ubuntu):**
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
sudo apt install gh
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**macOS:**
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
brew install gh
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Windows:**
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
winget install GitHub.cli
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Then authenticate with your GitHub account:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
gh auth login
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Follow the prompts to log in via browser or token.
|
|
198
|
+
|
|
199
|
+
### Initialize with GitHub
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
mkdir my-blog && cd my-blog
|
|
203
|
+
fossbook init --github
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
This will:
|
|
207
|
+
1. Scaffold the site project (config, content directories)
|
|
208
|
+
2. Create a GitHub repository for your blog
|
|
209
|
+
3. Generate `.github/workflows/deploy.yml` for automatic deployments
|
|
210
|
+
4. Push the initial commit to GitHub
|
|
211
|
+
|
|
212
|
+
### Publish a post
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
fossbook new "My New Article"
|
|
216
|
+
# ... edit content/posts/My New Article/index.md ...
|
|
217
|
+
fossbook deploy
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Fossbook will build the site, commit, push to GitHub, wait for the CI/CD pipeline to finish, and display the live URL:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
Building site... done.
|
|
224
|
+
Committing: "Publish: My New Article"
|
|
225
|
+
Pushing to origin/main...
|
|
226
|
+
Waiting for GitHub Pages deployment... ✓
|
|
227
|
+
|
|
228
|
+
✅ Published! View your article at:
|
|
229
|
+
https://username.github.io/my-blog/My%20New%20Article/
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Deploy options:**
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
fossbook deploy --message "Update homepage" # Custom commit message
|
|
236
|
+
fossbook deploy --no-wait # Push without waiting for CI
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
- Generator code: [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
|
|
242
|
+
- Archie theme: [MIT License](https://github.com/athul/archie?tab=MIT-1-ov-file#readme)
|
|
243
|
+
|
|
244
|
+
## Credits
|
|
245
|
+
|
|
246
|
+
Adapted from [kartiknair's blog](https://github.com/kartiknair/blog) and styled using the [Archie theme](https://github.com/athul/archie).
|
package/bin/fossbook.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { loadConfig } = require("../lib/mod/config");
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const command = args[0];
|
|
8
|
+
|
|
9
|
+
function printHelp() {
|
|
10
|
+
console.log(`
|
|
11
|
+
Usage: fossbook <command> [options]
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
build Build the static site
|
|
15
|
+
serve Build and start a local dev server
|
|
16
|
+
new <title> Create a new post
|
|
17
|
+
init Create a new fossbook site project
|
|
18
|
+
deploy Build, commit, push, and monitor deployment
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-c, --config Path to config file (default: ./fossbook.config.js)
|
|
22
|
+
-o, --output Output directory (default: ./public)
|
|
23
|
+
-p, --port Dev server port (default: 3000)
|
|
24
|
+
--clean Remove output directory before build (default: true)
|
|
25
|
+
--github (init) Also create a GitHub repo and push
|
|
26
|
+
-m, --message (deploy) Custom commit message
|
|
27
|
+
--no-wait (deploy) Push without waiting for CI status
|
|
28
|
+
--draft (deploy) Commit locally without pushing
|
|
29
|
+
-v, --version Show version number
|
|
30
|
+
-h, --help Show help
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getOption(flag, alias) {
|
|
35
|
+
const idx = args.indexOf(flag);
|
|
36
|
+
const aliasIdx = alias ? args.indexOf(alias) : -1;
|
|
37
|
+
const foundIdx = idx !== -1 ? idx : aliasIdx;
|
|
38
|
+
if (foundIdx !== -1 && foundIdx + 1 < args.length) {
|
|
39
|
+
return args[foundIdx + 1];
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasFlag(flag, alias) {
|
|
45
|
+
return args.includes(flag) || (alias && args.includes(alias));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (hasFlag("-v", "--version")) {
|
|
49
|
+
const pkg = require("../package.json");
|
|
50
|
+
console.log(`fossbook v${pkg.version}`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (hasFlag("-h", "--help") || !command) {
|
|
55
|
+
printHelp();
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const configPath = getOption("-c", "--config");
|
|
60
|
+
|
|
61
|
+
switch (command) {
|
|
62
|
+
case "build": {
|
|
63
|
+
const config = loadConfig(configPath);
|
|
64
|
+
const outputOverride = getOption("-o", "--output");
|
|
65
|
+
if (outputOverride) config.dev.outdir = outputOverride;
|
|
66
|
+
|
|
67
|
+
const { build } = require("../lib/index");
|
|
68
|
+
build(config);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case "serve": {
|
|
73
|
+
const config = loadConfig(configPath);
|
|
74
|
+
const outputOverride = getOption("-o", "--output");
|
|
75
|
+
if (outputOverride) config.dev.outdir = outputOverride;
|
|
76
|
+
const port = getOption("-p", "--port") || 3000;
|
|
77
|
+
|
|
78
|
+
const { build } = require("../lib/index");
|
|
79
|
+
build(config);
|
|
80
|
+
|
|
81
|
+
const { serve } = require("../lib/server");
|
|
82
|
+
serve(config, Number(port));
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case "new": {
|
|
87
|
+
const title = args[1];
|
|
88
|
+
if (!title) {
|
|
89
|
+
console.error('Error: Please provide a post title. Usage: fossbook new "My Post Title"');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const config = loadConfig(configPath);
|
|
93
|
+
const { createPost } = require("../lib/new");
|
|
94
|
+
createPost(config, title);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case "init": {
|
|
99
|
+
const { initProject } = require("../lib/init");
|
|
100
|
+
initProject({ github: hasFlag("--github") });
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case "deploy": {
|
|
105
|
+
const config = loadConfig(configPath);
|
|
106
|
+
const { deploy } = require("../lib/deploy");
|
|
107
|
+
deploy(config, {
|
|
108
|
+
message: getOption("-m", "--message"),
|
|
109
|
+
noWait: hasFlag("--no-wait"),
|
|
110
|
+
draft: hasFlag("--draft"),
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
console.error(`Unknown command: ${command}`);
|
|
117
|
+
printHelp();
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
package/lib/all_posts.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const PageBase = require("./mod/page_base");
|
|
4
|
+
|
|
5
|
+
module.exports = class AllPostsPage extends PageBase {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
super(config);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// posts: array of Page objects
|
|
11
|
+
generateContent(posts) {
|
|
12
|
+
const pageTitle = "All posts";
|
|
13
|
+
if (!fs.existsSync(`${this.config.dev.outdir}/all_posts/`))
|
|
14
|
+
fs.mkdirSync(`${this.config.dev.outdir}/all_posts/`);
|
|
15
|
+
|
|
16
|
+
const data = { posts: posts, pageTitle: pageTitle };
|
|
17
|
+
this.url = `${this.config.blogsite}/posts.html`;
|
|
18
|
+
this.title = `${this.config.blogName}: ${data.pageTitle}`;
|
|
19
|
+
this.description = "All posts";
|
|
20
|
+
this.imageURL = this.config.image;
|
|
21
|
+
|
|
22
|
+
const templatePath = path.join(this.config.themePath, "layouts", "all_posts.html");
|
|
23
|
+
fs.writeFileSync(
|
|
24
|
+
`${this.config.dev.outdir}/all_posts/index.html`,
|
|
25
|
+
this.generateHTML(templatePath, data),
|
|
26
|
+
(e) => {
|
|
27
|
+
if (e) throw e;
|
|
28
|
+
console.log(`posts.html was created successfully`);
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="400" viewBox="0 0 800 400">
|
|
2
|
+
<rect width="800" height="400" fill="#f0f0f0"/>
|
|
3
|
+
<text x="400" y="200" font-family="Arial, sans-serif" font-size="32" fill="#999" text-anchor="middle" dominant-baseline="middle">Your Image Here</text>
|
|
4
|
+
</svg>
|
package/lib/deploy.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execSync, exec } = require("child_process");
|
|
4
|
+
const { build } = require("./index");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Deploy the blog: build → commit → push → poll CI → show URL.
|
|
8
|
+
*/
|
|
9
|
+
async function deploy(config, options = {}) {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const deployConfig = config.deploy || {};
|
|
12
|
+
const branch = deployConfig.branch || "main";
|
|
13
|
+
const remote = deployConfig.remote || "origin";
|
|
14
|
+
|
|
15
|
+
// 1. Build the site
|
|
16
|
+
console.log("Building site...");
|
|
17
|
+
build(config);
|
|
18
|
+
console.log("Build complete.\n");
|
|
19
|
+
|
|
20
|
+
// 2. Check git is initialized
|
|
21
|
+
if (!fs.existsSync(path.join(cwd, ".git"))) {
|
|
22
|
+
console.error("Error: Not a git repository. Run 'fossbook init --github' first.");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 3. Stage all changes
|
|
27
|
+
execSync("git add -A", { cwd, stdio: "inherit" });
|
|
28
|
+
|
|
29
|
+
// 4. Check if there are changes to commit
|
|
30
|
+
try {
|
|
31
|
+
execSync("git diff --cached --quiet", { cwd });
|
|
32
|
+
console.log("No changes to commit.");
|
|
33
|
+
return;
|
|
34
|
+
} catch {
|
|
35
|
+
// There are staged changes — continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 5. Determine commit message
|
|
39
|
+
let commitMessage = options.message;
|
|
40
|
+
if (!commitMessage) {
|
|
41
|
+
commitMessage = detectNewPost(config) || "Update blog";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 6. Commit
|
|
45
|
+
console.log(`Committing: "${commitMessage}"`);
|
|
46
|
+
execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
|
|
47
|
+
cwd,
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 7. If --draft, stop here
|
|
52
|
+
if (options.draft) {
|
|
53
|
+
console.log("\nDraft committed locally. Run 'fossbook deploy' again to push.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 8. Push
|
|
58
|
+
console.log(`\nPushing to ${remote}/${branch}...`);
|
|
59
|
+
try {
|
|
60
|
+
execSync(`git push ${remote} ${branch}`, { cwd, stdio: "inherit" });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(`Error: Failed to push to ${remote}/${branch}.`);
|
|
63
|
+
console.error("Make sure the remote exists and you have push access.");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 9. Wait for CI/CD and show URL
|
|
68
|
+
if (options.noWait) {
|
|
69
|
+
console.log("\nPushed! Skipping CI/CD status check (--no-wait).");
|
|
70
|
+
printSiteUrl(config);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await waitForDeployment(cwd, config);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect if a new post was added and return a commit message.
|
|
79
|
+
*/
|
|
80
|
+
function detectNewPost(config) {
|
|
81
|
+
try {
|
|
82
|
+
const diffOutput = execSync("git diff --cached --name-only", {
|
|
83
|
+
encoding: "utf-8",
|
|
84
|
+
});
|
|
85
|
+
const postsDir = config.postsDir || "./content/posts";
|
|
86
|
+
const newPosts = diffOutput
|
|
87
|
+
.split("\n")
|
|
88
|
+
.filter((f) => f.startsWith(postsDir.replace("./", "")) && f.endsWith("index.md"))
|
|
89
|
+
.map((f) => {
|
|
90
|
+
// Extract post title from path like "content/posts/My Post/index.md"
|
|
91
|
+
const parts = f.split("/");
|
|
92
|
+
return parts[parts.length - 2];
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (newPosts.length === 1) {
|
|
96
|
+
return `Publish: ${newPosts[0]}`;
|
|
97
|
+
} else if (newPosts.length > 1) {
|
|
98
|
+
return `Publish ${newPosts.length} new posts`;
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Fall through
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Wait for GitHub Actions deployment to complete, then show the URL.
|
|
108
|
+
*/
|
|
109
|
+
async function waitForDeployment(cwd, config) {
|
|
110
|
+
// Check if gh is available
|
|
111
|
+
const hasGh = isGhAvailable();
|
|
112
|
+
|
|
113
|
+
if (!hasGh) {
|
|
114
|
+
console.log("\nGitHub CLI (gh) not found. Cannot monitor deployment status.");
|
|
115
|
+
console.log("Install it to see live deployment progress: https://cli.github.com/");
|
|
116
|
+
printSiteUrl(config);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log("\nWaiting for GitHub Pages deployment...");
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Wait a moment for the workflow to be triggered
|
|
124
|
+
await sleep(3000);
|
|
125
|
+
|
|
126
|
+
// Get the latest workflow run
|
|
127
|
+
const runJson = execSync(
|
|
128
|
+
'gh run list --workflow=deploy.yml --limit=1 --json databaseId,status,conclusion',
|
|
129
|
+
{ cwd, encoding: "utf-8" }
|
|
130
|
+
);
|
|
131
|
+
const runs = JSON.parse(runJson);
|
|
132
|
+
|
|
133
|
+
if (runs.length === 0) {
|
|
134
|
+
console.log("No workflow runs found. The deployment may not be configured yet.");
|
|
135
|
+
console.log("Enable GitHub Pages: Settings → Pages → Source → GitHub Actions");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const runId = runs[0].databaseId;
|
|
140
|
+
|
|
141
|
+
// Watch the run
|
|
142
|
+
console.log(`Monitoring workflow run #${runId}...`);
|
|
143
|
+
execSync(`gh run watch ${runId}`, { cwd, stdio: "inherit" });
|
|
144
|
+
|
|
145
|
+
// Check result
|
|
146
|
+
const resultJson = execSync(
|
|
147
|
+
`gh run view ${runId} --json conclusion`,
|
|
148
|
+
{ cwd, encoding: "utf-8" }
|
|
149
|
+
);
|
|
150
|
+
const result = JSON.parse(resultJson);
|
|
151
|
+
|
|
152
|
+
if (result.conclusion === "success") {
|
|
153
|
+
console.log("\n✅ Deployment successful!");
|
|
154
|
+
printSiteUrl(config);
|
|
155
|
+
} else {
|
|
156
|
+
console.error(`\n❌ Deployment failed (conclusion: ${result.conclusion}).`);
|
|
157
|
+
console.log(`View details: gh run view ${runId} --log`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.warn("\nCould not monitor deployment status.");
|
|
162
|
+
console.log("Check your deployment at: https://github.com → Actions tab");
|
|
163
|
+
printSiteUrl(config);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Print the site URL based on config.
|
|
169
|
+
*/
|
|
170
|
+
function printSiteUrl(config) {
|
|
171
|
+
const siteUrl = config.blogsite || config.githubCNAME;
|
|
172
|
+
if (siteUrl && siteUrl !== "http://localhost:3000") {
|
|
173
|
+
console.log(`\nView your site at: ${siteUrl}`);
|
|
174
|
+
} else {
|
|
175
|
+
// Try to infer from git remote
|
|
176
|
+
try {
|
|
177
|
+
const remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
178
|
+
const match = remoteUrl.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
179
|
+
if (match) {
|
|
180
|
+
const [owner, repo] = match[1].split("/");
|
|
181
|
+
console.log(`\nView your site at: https://${owner}.github.io/${repo}/`);
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
console.log("\nYour site will be available on GitHub Pages once deployment completes.");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isGhAvailable() {
|
|
190
|
+
try {
|
|
191
|
+
execSync("gh --version", { stdio: "ignore" });
|
|
192
|
+
return true;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sleep(ms) {
|
|
199
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = { deploy };
|
package/lib/home.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const PageBase = require("./mod/page_base");
|
|
4
|
+
|
|
5
|
+
module.exports = class Pagination extends PageBase {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
super(config);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
generateContent(posts) {
|
|
11
|
+
const postsPerPage = 5;
|
|
12
|
+
const numPages = Math.ceil(posts.length / postsPerPage);
|
|
13
|
+
|
|
14
|
+
if (fs.existsSync(`${this.config.dev.outdir}/page`))
|
|
15
|
+
fs.rmSync(`${this.config.dev.outdir}/page`, { recursive: true });
|
|
16
|
+
|
|
17
|
+
fs.mkdirSync(`${this.config.dev.outdir}/page`);
|
|
18
|
+
|
|
19
|
+
// copy content/page/1.html to outdir/page/1.html
|
|
20
|
+
const contentPagePath = path.join(this.config.dev.content, "page", "1.html");
|
|
21
|
+
if (fs.existsSync(contentPagePath))
|
|
22
|
+
fs.copyFileSync(
|
|
23
|
+
contentPagePath,
|
|
24
|
+
`${this.config.dev.outdir}/page/1.html`,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < numPages; i++) {
|
|
28
|
+
const pagePosts = posts.slice(i * postsPerPage, (i + 1) * postsPerPage);
|
|
29
|
+
|
|
30
|
+
let filePath;
|
|
31
|
+
let url;
|
|
32
|
+
if (i === 0) {
|
|
33
|
+
filePath = `${this.config.dev.outdir}/index.html`;
|
|
34
|
+
url = this.config.blogsite;
|
|
35
|
+
} else {
|
|
36
|
+
filePath = `${this.config.dev.outdir}/page/${i + 1}.html`;
|
|
37
|
+
url = `${this.config.blogsite}/page/${i + 1}.html`;
|
|
38
|
+
}
|
|
39
|
+
const prev = i - 1 >= 0 ? i : null;
|
|
40
|
+
const next = i + 1 < numPages ? i + 2 : null;
|
|
41
|
+
|
|
42
|
+
console.log("prev", prev, "next", next);
|
|
43
|
+
this.url = url;
|
|
44
|
+
this.title = `${this.config.blogName}`;
|
|
45
|
+
this.imageURL = this.config.image;
|
|
46
|
+
const data = { posts: pagePosts, prev: prev, next: next };
|
|
47
|
+
|
|
48
|
+
const templatePath = path.join(this.config.themePath, "layouts", "home.html");
|
|
49
|
+
fs.writeFile(
|
|
50
|
+
`${filePath}`,
|
|
51
|
+
this.generateHTML(templatePath, data),
|
|
52
|
+
(e) => {
|
|
53
|
+
if (e) throw e;
|
|
54
|
+
console.log(`page/${i + 1}.html was created successfully`);
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|