fossbook 0.2.13 → 0.2.15

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 CHANGED
@@ -85,10 +85,17 @@ This also creates `index.ko.md` and `index.ja.md` in the same post directory. Ru
85
85
  fossbook build
86
86
  ```
87
87
 
88
+ Posts with `draft: true` in their front matter are excluded from generated
89
+ post pages, home pages, archives, and tags. To include them in a local build:
90
+
91
+ ```bash
92
+ fossbook build --include-drafts
93
+ ```
94
+
88
95
  ### Preview locally
89
96
 
90
97
  ```bash
91
- fossbook serve
98
+ fossbook serve --include-drafts
92
99
  ```
93
100
 
94
101
  Open http://localhost:3000 to view your site.
@@ -204,11 +211,16 @@ date: 2026-02-17
204
211
  description: "A brief summary of the post"
205
212
  image: "feature.png"
206
213
  tags: "JavaScript, Node.js, Static Site"
214
+ draft: false
207
215
  ---
208
216
 
209
217
  Your Markdown content here...
210
218
  ```
211
219
 
220
+ Set `draft: true` while a post is in progress. Normal builds and deployments
221
+ exclude it. Use `fossbook serve --include-drafts` to preview drafts locally,
222
+ then remove the field or set it to `false` before publishing.
223
+
212
224
  ### Directory structure
213
225
 
214
226
  ```
@@ -300,6 +312,9 @@ module.exports = {
300
312
  previousPageLabel: "앞으로",
301
313
  nextPageLabel: "뒤로",
302
314
  transcriptLabel: "말글 보이기",
315
+ copyLinkLabel: "링크 복사",
316
+ linkCopiedLabel: "링크를 복사했습니다",
317
+ copyLinkErrorLabel: "링크를 복사하지 못했습니다",
303
318
  };
304
319
  ```
305
320
 
package/bin/fossbook.js CHANGED
@@ -24,6 +24,7 @@ Options:
24
24
  --clean Remove output directory before build (default: true)
25
25
  --github (init) Also create a GitHub repo and push
26
26
  --lang (new) Comma-separated translation languages, e.g. ko,ja
27
+ --include-drafts (build, serve) Include posts with draft: true
27
28
  -m, --message (deploy) Custom commit message
28
29
  --no-wait (deploy) Push without waiting for CI status
29
30
  --draft (deploy) Commit locally without pushing
@@ -64,6 +65,7 @@ switch (command) {
64
65
  const config = loadConfig(configPath);
65
66
  const outputOverride = getOption("-o", "--output");
66
67
  if (outputOverride) config.dev.outdir = outputOverride;
68
+ config.includeDrafts = hasFlag("--include-drafts");
67
69
 
68
70
  const { build } = require("../lib/index");
69
71
  build(config);
@@ -74,6 +76,7 @@ switch (command) {
74
76
  const config = loadConfig(configPath);
75
77
  const outputOverride = getOption("-o", "--output");
76
78
  if (outputOverride) config.dev.outdir = outputOverride;
79
+ config.includeDrafts = hasFlag("--include-drafts");
77
80
  const port = getOption("-p", "--port") || 3000;
78
81
 
79
82
  const { build } = require("../lib/index");
package/lib/deploy.js CHANGED
@@ -14,6 +14,7 @@ async function deploy(config, options = {}) {
14
14
 
15
15
  // 1. Build the site
16
16
  console.log("Building site...");
17
+ config.includeDrafts = false;
17
18
  build(config);
18
19
  console.log("Build complete.\n");
19
20
 
package/lib/index.js CHANGED
@@ -38,7 +38,12 @@ function createPostTranslationIndex(languageConfigs) {
38
38
  const sourceFile = config.language === config.defaultLanguage
39
39
  ? "index.md"
40
40
  : `index.${config.language}.md`;
41
- if (!fs.existsSync(path.join(postsDir, entry.name, sourceFile))) continue;
41
+ const sourcePath = path.join(postsDir, entry.name, sourceFile);
42
+ if (!fs.existsSync(sourcePath)) continue;
43
+
44
+ const post = new Page(config);
45
+ post.readSource(sourcePath, entry.name);
46
+ if (post.draft && !config.includeDrafts) continue;
42
47
 
43
48
  const prefix = config.postsPath ? `${config.postsPath}/` : "";
44
49
  translations.push({
@@ -163,6 +168,9 @@ function build(config) {
163
168
  fs.mkdirSync(config.dev.outdir, { recursive: true });
164
169
 
165
170
  const languageConfigs = config.languageConfigs || [config];
171
+ languageConfigs.forEach((languageConfig) => {
172
+ languageConfig.includeDrafts = config.includeDrafts === true;
173
+ });
166
174
  const postTranslationIndex = createPostTranslationIndex(languageConfigs);
167
175
  const aboutTranslations = createAboutTranslations(languageConfigs);
168
176
  const homeTranslations = createHomeTranslations(languageConfigs);
package/lib/init.js CHANGED
@@ -130,7 +130,7 @@ function initProject(options = {}) {
130
130
  // fossbook.config.ko.js, and so on. Menu labels can also be localized with
131
131
  // homeLabel, allPostsLabel, aboutLabel, tagsLabel, allTagsLabel,
132
132
  // postedOnLabel, readMoreLabel, previousPageLabel, nextPageLabel,
133
- // and transcriptLabel.
133
+ // transcriptLabel, copyLinkLabel, linkCopiedLabel, and copyLinkErrorLabel.
134
134
 
135
135
  // Comment system (optional)
136
136
  // comments: { provider: "utterances", repo: "user/repo", issueTerm: "pathname", theme: "github-light" },
package/lib/mod/page.js CHANGED
@@ -39,6 +39,7 @@ module.exports = class Page extends PageBase {
39
39
  this.url = `${this.config.blogsite}/${this.path}/`;
40
40
  this.image = content.attributes.image;
41
41
  this.description = content.attributes.description;
42
+ this.draft = content.attributes.draft === true;
42
43
  // default image if no imageURL is specified
43
44
  this.imageURL = this.config.image;
44
45
 
package/lib/posts.js CHANGED
@@ -26,6 +26,7 @@ module.exports = class Posts {
26
26
  }
27
27
  const post = new Page(this.config);
28
28
  post.readSource(indexPath, postPath);
29
+ if (post.draft && !this.config.includeDrafts) return;
29
30
  // Optionally nest posts under a URL prefix (e.g. "posts" -> /posts/<slug>/).
30
31
  // post.slug stays the source folder name so image lookups remain correct.
31
32
  const prefix = this.config.postsPath ? `${this.config.postsPath}/` : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fossbook",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "A Markdown-based authoring and web-publishing tool for comics, illustrated stories, and articles",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -69,12 +69,38 @@
69
69
  width: var(--dialogue-width, 100%);
70
70
  }
71
71
 
72
- .transcript-control {
72
+ .post-controls {
73
+ align-items: center;
73
74
  display: flex;
75
+ flex-wrap: wrap;
76
+ gap: 1rem;
74
77
  justify-content: flex-end;
75
78
  margin: .75rem 0 1.5rem;
76
79
  }
77
80
 
81
+ .copy-link-button {
82
+ background: transparent;
83
+ border: 1px solid var(--bordercl);
84
+ border-radius: 4px;
85
+ color: inherit;
86
+ cursor: pointer;
87
+ font: inherit;
88
+ font-size: .78rem;
89
+ line-height: 1.2;
90
+ min-height: 1.75rem;
91
+ padding: .15rem .4rem;
92
+ }
93
+
94
+ .copy-link-button:hover {
95
+ background: var(--hovercolor);
96
+ color: #fff;
97
+ }
98
+
99
+ .copy-link-button:focus-visible {
100
+ outline: 2px solid var(--hovercolor);
101
+ outline-offset: 2px;
102
+ }
103
+
78
104
  .transcript-toggle {
79
105
  align-items: center;
80
106
  display: inline-flex;
@@ -124,7 +150,16 @@
124
150
  }
125
151
 
126
152
  .transcripts-hidden .image-dialogue {
127
- display: none;
153
+ border: 0;
154
+ clip: rect(0 0 0 0);
155
+ clip-path: inset(50%);
156
+ height: 1px;
157
+ margin: -1px;
158
+ overflow: hidden;
159
+ padding: 0;
160
+ position: absolute !important;
161
+ white-space: nowrap;
162
+ width: 1px;
128
163
  }
129
164
 
130
165
 
@@ -152,12 +187,20 @@
152
187
  }
153
188
 
154
189
  @media print {
155
- .transcript-control {
190
+ .post-controls {
156
191
  display: none;
157
192
  }
158
193
 
159
194
  .transcripts-hidden .image-dialogue {
160
195
  display: flex;
196
+ clip: auto;
197
+ clip-path: none;
198
+ height: auto;
199
+ margin: 0 auto;
200
+ overflow: visible;
201
+ position: static !important;
202
+ white-space: normal;
203
+ width: var(--dialogue-width, 100%);
161
204
  }
162
205
  }
163
206
 
@@ -29,9 +29,10 @@
29
29
 
30
30
  ${page.twitterCard("summary")}
31
31
  ${page.body.includes('class="blockquote-container image-dialogue"') ? `<script>
32
+ document.documentElement.classList.add("transcripts-hidden");
32
33
  try {
33
- if (localStorage.getItem("fossbook-comic-transcript") === "hidden") {
34
- document.documentElement.classList.add("transcripts-hidden");
34
+ if (localStorage.getItem("fossbook-comic-transcript") === "visible") {
35
+ document.documentElement.classList.remove("transcripts-hidden");
35
36
  }
36
37
  } catch (error) {}
37
38
  </script>` : ""}
@@ -56,12 +57,15 @@
56
57
  <div class="meta">${page.config.postedOnLabel || "Posted on"} ${page.formatDate(new Date(page.date))}</div>
57
58
  ${page.languageSwitcher()}
58
59
  </div>
59
- ${page.body.includes('class="blockquote-container image-dialogue"') ? `<div class="transcript-control">
60
- <div class="transcript-toggle">
60
+ <div class="post-controls">
61
+ ${page.body.includes('class="blockquote-container image-dialogue"') ? `<div class="transcript-toggle">
61
62
  <label for="comic-transcript-toggle">${page.config.transcriptLabel || "Display dialogue text"}</label>
62
- <input id="comic-transcript-toggle" type="checkbox" role="switch" aria-controls="comic-transcript" checked>
63
- </div>
64
- </div>` : ""}
63
+ <input id="comic-transcript-toggle" type="checkbox" role="switch" aria-controls="comic-transcript">
64
+ </div>` : ""}
65
+ <button class="copy-link-button" type="button">
66
+ <span aria-live="polite">${page.config.copyLinkLabel || "Copy link"}</span>
67
+ </button>
68
+ </div>
65
69
  <section class="body" id="comic-transcript">
66
70
  ${page.body}
67
71
  </section>
@@ -100,6 +104,40 @@
100
104
  import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
101
105
  mermaid.initialize({ startOnLoad: true });
102
106
  </script>` : ""}
107
+ <script>
108
+ const copyLinkButton = document.querySelector(".copy-link-button");
109
+ const copyLinkLabel = copyLinkButton.querySelector("span");
110
+ const defaultCopyLinkLabel = copyLinkLabel.textContent;
111
+ let copyLinkResetTimer;
112
+ const copyEpisodeLink = async () => {
113
+ const canonicalLink = document.querySelector('link[rel="canonical"]');
114
+ const episodeUrl = canonicalLink ? canonicalLink.href : window.location.href;
115
+ try {
116
+ if (navigator.clipboard && navigator.clipboard.writeText) {
117
+ await navigator.clipboard.writeText(episodeUrl);
118
+ } else {
119
+ const textArea = document.createElement("textarea");
120
+ textArea.value = episodeUrl;
121
+ textArea.setAttribute("readonly", "");
122
+ textArea.style.position = "fixed";
123
+ textArea.style.opacity = "0";
124
+ document.body.appendChild(textArea);
125
+ textArea.select();
126
+ const copied = document.execCommand("copy");
127
+ textArea.remove();
128
+ if (!copied) throw new Error("Copy command failed");
129
+ }
130
+ copyLinkLabel.textContent = ${JSON.stringify(page.config.linkCopiedLabel || "Link copied")};
131
+ } catch (error) {
132
+ copyLinkLabel.textContent = ${JSON.stringify(page.config.copyLinkErrorLabel || "Could not copy link")};
133
+ }
134
+ clearTimeout(copyLinkResetTimer);
135
+ copyLinkResetTimer = setTimeout(() => {
136
+ copyLinkLabel.textContent = defaultCopyLinkLabel;
137
+ }, 2000);
138
+ };
139
+ copyLinkButton.addEventListener("click", copyEpisodeLink);
140
+ </script>
103
141
  ${page.body.includes('class="blockquote-container image-dialogue"') ? `<script>
104
142
  const transcriptToggle = document.querySelector("#comic-transcript-toggle");
105
143
  const transcriptStorageKey = "fossbook-comic-transcript";