From e77f6e1fd338700ded7cefeb4140f0e516df259c Mon Sep 17 00:00:00 2001 From: Josh Glover Date: Wed, 19 Nov 2025 13:01:49 +0100 Subject: [PATCH 1/6] api/serve returns a stop-server! fn in non-blocking mode --- src/quickblog/api.clj | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/quickblog/api.clj b/src/quickblog/api.clj index d5d7cbf..ccd842a 100644 --- a/src/quickblog/api.clj +++ b/src/quickblog/api.clj @@ -604,7 +604,8 @@ (lib/refresh-templates (apply-default-opts opts))) (defn serve - "Runs file-server on `port`." + "Runs file-server on `port`. If `block?` is false, returns a zero-arity + `stop-server!` function that will stop the server when called." {:org.babashka/cli {:spec {:port @@ -615,10 +616,12 @@ ([opts block?] (let [{:keys [port out-dir]} (merge (get-defaults (meta #'serve)) (apply-default-opts opts)) - serve (requiring-resolve 'babashka.http-server/serve)] - (serve {:port port - :dir out-dir}) - (when block? @(promise))))) + serve (requiring-resolve 'babashka.http-server/serve) + stop-server! (serve {:port port + :dir out-dir})] + (if block? + @(promise) + stop-server!)))) (def ^:private posts-cache (atom nil)) From 2789c0151df834bd8954a46d8898b12875384c92 Mon Sep 17 00:00:00 2001 From: Josh Glover Date: Wed, 19 Nov 2025 13:02:56 +0100 Subject: [PATCH 2/6] api/watch returns a list of watcher in non-blocking mode --- src/quickblog/api.clj | 119 +++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/src/quickblog/api.clj b/src/quickblog/api.clj index ccd842a..92832cd 100644 --- a/src/quickblog/api.clj +++ b/src/quickblog/api.clj @@ -627,7 +627,8 @@ (defn watch "Watches posts, templates, and assets for changes. Runs file server using - `serve`." + `serve` (unless the `:no-serve?` opt is true). If the `:no-block?` opt is true, + returns a list of watchers that can be passed to `unwatch` to stop watching." {:org.babashka/cli {:spec {:port @@ -635,7 +636,8 @@ :ref "" :default 1888}}}} [opts] - (let [{:keys [assets-dir assets-out-dir posts-dir templates-dir] + (let [{:keys [assets-dir assets-out-dir posts-dir templates-dir + no-serve? no-block?] :as opts} (-> opts (assoc :watch (format "" @@ -643,56 +645,67 @@ apply-default-opts render)] (reset! posts-cache (:posts opts)) - (serve opts false) + (when-not no-serve? + (serve opts false)) (let [load-pod (requiring-resolve 'babashka.pods/load-pod)] (load-pod 'org.babashka/fswatcher "0.0.7") - (let [watch (requiring-resolve 'pod.babashka.fswatcher/watch)] - (watch posts-dir - (fn [{:keys [path type]}] - (println "Change detected:" (name type) (str path)) - (when (#{:create :remove :rename :write :write|chmod :chmod} type) - (let [post-filename (-> (fs/file path) fs/file-name)] - ;; skip Emacs backup files and the like - (when (and (str/ends-with? post-filename ".md") - ;; emacs backup file - (not (str/starts-with? post-filename ".#"))) - (println "Re-rendering" post-filename) - (let [post (lib/load-post opts path) - deleted? (not (fs/exists? path)) - posts (cond - deleted? - (dissoc @posts-cache post-filename) - - (:quickblog/error post) - (do - (println (:quickblog/error post)) - (dissoc @posts-cache post-filename)) - - :else - (assoc @posts-cache post-filename post)) - opts (-> opts - (assoc :cached-posts @posts-cache - :posts posts) - render)] - (reset! posts-cache (:posts opts)))))))) - - (watch templates-dir - (fn [{:keys [path type]}] - (println "Template change detected; re-rendering all posts:" - (name type) (str path)) - (let [opts (-> opts - (dissoc :cached-posts :posts) - render)] - (reset! posts-cache (:posts opts))))) - - (when (fs/exists? assets-dir) - (watch assets-dir - (fn [{:keys [path type]}] - (println "Asset change detected:" - (name type) (str path)) - (when (contains? #{:remove :rename} type) - (let [file (fs/file assets-out-dir (fs/file-name path))] - (println "Removing deleted asset:" (str file)) - (fs/delete-if-exists file))) - (lib/copy-tree-modified assets-dir assets-out-dir))))))) - @(promise)) + (let [watch (requiring-resolve 'pod.babashka.fswatcher/watch) + watchers + [(watch posts-dir + (fn [{:keys [path type]}] + (println "Change detected:" (name type) (str path)) + (when (#{:create :remove :rename :write :write|chmod :chmod} type) + (let [post-filename (-> (fs/file path) fs/file-name)] + ;; skip Emacs backup files and the like + (when (and (str/ends-with? post-filename ".md") + ;; emacs backup file + (not (str/starts-with? post-filename ".#"))) + (println "Re-rendering" post-filename) + (let [post (lib/load-post opts path) + deleted? (not (fs/exists? path)) + posts (cond + deleted? + (dissoc @posts-cache post-filename) + + (:quickblog/error post) + (do + (println (:quickblog/error post)) + (dissoc @posts-cache post-filename)) + + :else + (assoc @posts-cache post-filename post)) + opts (-> opts + (assoc :cached-posts @posts-cache + :posts posts) + render)] + (reset! posts-cache (:posts opts)))))))) + + (watch templates-dir + (fn [{:keys [path type]}] + (println "Template change detected; re-rendering all posts:" + (name type) (str path)) + (let [opts (-> opts + (dissoc :cached-posts :posts) + render)] + (reset! posts-cache (:posts opts))))) + + (when (fs/exists? assets-dir) + (watch assets-dir + (fn [{:keys [path type]}] + (println "Asset change detected:" + (name type) (str path)) + (when (contains? #{:remove :rename} type) + (let [file (fs/file assets-out-dir (fs/file-name path))] + (println "Removing deleted asset:" (str file)) + (fs/delete-if-exists file))) + (lib/copy-tree-modified assets-dir assets-out-dir))))]] + (if no-block? + watchers + @(promise)))))) + +(defn unwatch + "Stops each watcher in the list of `watchers`." + [watchers] + (let [unwatch (requiring-resolve 'pod.babashka.fswatcher/unwatch)] + (doseq [watcher (remove nil? watchers)] + (unwatch watcher)))) From 0b682df01593de635e5775f2043bfc7f66d2e3c3 Mon Sep 17 00:00:00 2001 From: Josh Glover Date: Wed, 19 Nov 2025 13:27:57 +0100 Subject: [PATCH 3/6] Add alternate content root example to README --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index 6f7e434..1d96de7 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,40 @@ Write a blog post here! any changes to the new post template will cause all of your existing posts to be re-rendered, which is probably not what you want!** +## Serving an alternate content root + +If your website contains a blog not at the content root of the webserver (for +example, https://example.com/blog), you may want `bb quickblog watch` to watch +the blog directory whilst serving the blog directory's parent as the content +root. Assuming that your website has a `bb.edn`, you can add a task similar to +the following to accomplish this: + +``` clojure +{:deps {io.github.borkdude/quickblog {:git/sha "LATEST-SHA-HERE"}} + :tasks + {:requires ([quickblog.api :as quickblog]) + :init (def opts + {:out-dir "public" + ;; ... + :blog {:blog-title "Some cool blog" + ;; ... + :assets-dir "blog/assets" + :out-dir "public/blog" + :posts-dir "blog/posts" + :templates-dir "blog/templates"}}) + + ;; ... + + watch {:doc "Watch blog for changes" + :task (do + (quickblog/watch (assoc (:blog opts) + :no-serve? true + :no-block? true)) + (quickblog/serve (assoc (:blog opts) + :out-dir (:out-dir opts))))} + }} +``` + ## Breaking changes ### posts.edn removed From 359470cb062185f5c2d0b280bb8468ba8d59f898 Mon Sep 17 00:00:00 2001 From: Josh Glover Date: Wed, 19 Nov 2025 13:28:16 +0100 Subject: [PATCH 4/6] Regenerate API docs --- API.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 13 deletions(-) diff --git a/API.md b/API.md index ca0cc99..5ee04da 100644 --- a/API.md +++ b/API.md @@ -12,7 +12,14 @@ Removes cache and output directories -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L493-L499) +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L580-L586) +## `debug` +``` clojure + +(debug & xs) +``` + +[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L277-L279) ## `migrate` ``` clojure @@ -21,7 +28,7 @@ Removes cache and output directories Migrates from `posts.edn` to post-local metadata -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L501-L512) +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L588-L599) ## `new` ``` clojure @@ -30,7 +37,7 @@ Migrates from `posts.edn` to post-local metadata Creates new `file` in posts dir. -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L466-L491) +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L524-L578) ## `quickblog` ``` clojure @@ -39,7 +46,7 @@ Creates new `file` in posts dir. Alias for `render` -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L457-L460) +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L515-L518) ## `refresh-templates` ``` clojure @@ -48,7 +55,7 @@ Alias for `render` Updates to latest default templates -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L514-L517) +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L601-L604) ## `render` ``` clojure @@ -57,16 +64,34 @@ Updates to latest default templates Renders posts declared in `posts.edn` to `out-dir`. -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L421-L455) +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L475-L513) ## `serve` ``` clojure (serve opts) +(serve opts block?) +``` + + +Runs file-server on `port`. If `block?` is false, returns a zero-arity + `stop-server!` function that will stop the server when called. +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L606-L624) +## `unwatch` +``` clojure + +(unwatch watchers) ``` -Runs file-server on `port`. -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L519-L532) +Stops each watcher in the list of `watchers`. +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L706-L711) +## `update-cache-dir` +``` clojure + +(update-cache-dir opts) +``` + +[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L190-L198) ## `watch` ``` clojure @@ -75,8 +100,9 @@ Runs file-server on `port`. Watches posts, templates, and assets for changes. Runs file server using - `serve`. -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L536-L601) + `serve` (unless the `:no-serve?` opt is true). If the `:no-block?` opt is true, + returns a list of watchers that can be passed to `unwatch` to stop watching. +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L628-L704) # quickblog.cli @@ -89,7 +115,7 @@ Watches posts, templates, and assets for changes. Runs file server using (-main & args) ``` -[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/cli.clj#L143-L144) +[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/cli.clj#L144-L145) ## `dispatch` ``` clojure @@ -97,7 +123,7 @@ Watches posts, templates, and assets for changes. Runs file server using (dispatch default-opts & args) ``` -[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/cli.clj#L127-L133) +[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/cli.clj#L128-L134) ## `run` ``` clojure @@ -106,4 +132,65 @@ Watches posts, templates, and assets for changes. Runs file server using Meant to be called using `clj -M:quickblog`; see Quickstart > Clojure in README -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/cli.clj#L135-L141) +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/cli.clj#L136-L142) +# quickblog.internal.frontmatter + + + + + +## `flatten-metadata` +``` clojure + +(flatten-metadata metadata) +``` + + +Given a list of maps which contain a single key/value, flatten them all into + a single map with all the leading spaces removed. If an empty list is provided + then return nil. +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/internal/frontmatter.clj#L20-L39) +## `parse-edn-metadata-headers` +``` clojure + +(parse-edn-metadata-headers lines-seq) +``` + +[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/internal/frontmatter.clj#L67-L77) +## `parse-metadata-headers` +``` clojure + +(parse-metadata-headers lines-seq) +``` + + +Given a sequence of lines from a markdown document, attempt to parse a + metadata header if it exists. Accepts wiki, yaml, and edn formats. + Returns the parsed headers number of lines the metadata spans +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/internal/frontmatter.clj#L79-L95) +## `parse-metadata-line` +``` clojure + +(parse-metadata-line line) +``` + + +Given a line of metadata header text return either a list containing a parsed + and normalizd key and the original text of the value, or if no header is found + (this is a continuation or new value from a pervious header key) simply + return the text. If a blank or invalid line is found return nil. +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/internal/frontmatter.clj#L6-L18) +## `parse-wiki-metadata-headers` +``` clojure + +(parse-wiki-metadata-headers lines-seq) +``` + +[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/internal/frontmatter.clj#L42-L49) +## `parse-yaml-metadata-headers` +``` clojure + +(parse-yaml-metadata-headers lines-seq) +``` + +[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/internal/frontmatter.clj#L53-L65) From 9ace1bd5f0adf8ff2dee63b82f300b08d8b38c1f Mon Sep 17 00:00:00 2001 From: Josh Glover Date: Wed, 19 Nov 2025 13:28:28 +0100 Subject: [PATCH 5/6] Add alternate content root to CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22abd15..f223a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Instances of quickblog can be seen [here](https://github.com/borkdude/quickblog?tab=readme-ov-file#blogs-using-quickblog). +## Unreleased + +- Add support for a blog contained within another website; see [Serving an alternate content root](README.md#serving-an-alternate-content-root) in README. ([@jmglov](https://github.com/jmglov)) + ## 0.4.7 (2025-06-12) - Switch to [Nextjournal Markdown](https://github.com/nextjournal/markdown) for markdown rendering From 9ff9423b57eef5d49d74ed885206d7a37e87c5fe Mon Sep 17 00:00:00 2001 From: Josh Glover Date: Sat, 29 Nov 2025 16:03:13 +0100 Subject: [PATCH 6/6] api/watch: use `:block` and `:serve` instead of `:no-block?` and `:no-serve?` --- API.md | 8 ++++---- README.md | 4 ++-- src/quickblog/api.clj | 22 ++++++++++++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/API.md b/API.md index 5ee04da..6d54115 100644 --- a/API.md +++ b/API.md @@ -73,7 +73,7 @@ Renders posts declared in `posts.edn` to `out-dir`. ``` -Runs file-server on `port`. If `block?` is false, returns a zero-arity +Runs file-server on `port`. If `block?` is falsey, returns a zero-arity `stop-server!` function that will stop the server when called.
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L606-L624) ## `unwatch` @@ -84,7 +84,7 @@ Runs file-server on `port`. If `block?` is false, returns a zero-arity Stops each watcher in the list of `watchers`. -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L706-L711) +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L712-L717) ## `update-cache-dir` ``` clojure @@ -100,9 +100,9 @@ Stops each watcher in the list of `watchers`. Watches posts, templates, and assets for changes. Runs file server using - `serve` (unless the `:no-serve?` opt is true). If the `:no-block?` opt is true, + `serve` (unless the `:serve` opt is `false`). If the `:block` opt is `false`, returns a list of watchers that can be passed to `unwatch` to stop watching. -
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L628-L704) +
[source](https://github.com/borkdude/quickblog/blob/main/src/quickblog/api.clj#L628-L710) # quickblog.cli diff --git a/README.md b/README.md index 1d96de7..7ef14e0 100644 --- a/README.md +++ b/README.md @@ -341,8 +341,8 @@ the following to accomplish this: watch {:doc "Watch blog for changes" :task (do (quickblog/watch (assoc (:blog opts) - :no-serve? true - :no-block? true)) + :serve false + :block false)) (quickblog/serve (assoc (:blog opts) :out-dir (:out-dir opts))))} }} diff --git a/src/quickblog/api.clj b/src/quickblog/api.clj index 92832cd..6ea8913 100644 --- a/src/quickblog/api.clj +++ b/src/quickblog/api.clj @@ -604,7 +604,7 @@ (lib/refresh-templates (apply-default-opts opts))) (defn serve - "Runs file-server on `port`. If `block?` is false, returns a zero-arity + "Runs file-server on `port`. If `block?` is falsey, returns a zero-arity `stop-server!` function that will stop the server when called." {:org.babashka/cli {:spec @@ -627,17 +627,23 @@ (defn watch "Watches posts, templates, and assets for changes. Runs file server using - `serve` (unless the `:no-serve?` opt is true). If the `:no-block?` opt is true, + `serve` (unless the `:serve` opt is `false`). If the `:block` opt is `false`, returns a list of watchers that can be passed to `unwatch` to stop watching." {:org.babashka/cli {:spec {:port {:desc "Port for HTTP server to listen on" :ref "" - :default 1888}}}} + :default 1888} + :serve + {:desc "Start a webserver" + :default true} + :block + {:desc "Block until interrupted" + :default true}}}} [opts] (let [{:keys [assets-dir assets-out-dir posts-dir templates-dir - no-serve? no-block?] + serve block] :as opts} (-> opts (assoc :watch (format "" @@ -645,7 +651,7 @@ apply-default-opts render)] (reset! posts-cache (:posts opts)) - (when-not no-serve? + (when (not (false? serve)) (serve opts false)) (let [load-pod (requiring-resolve 'babashka.pods/load-pod)] (load-pod 'org.babashka/fswatcher "0.0.7") @@ -699,9 +705,9 @@ (println "Removing deleted asset:" (str file)) (fs/delete-if-exists file))) (lib/copy-tree-modified assets-dir assets-out-dir))))]] - (if no-block? - watchers - @(promise)))))) + (if (not (false? block)) + @(promise) + watchers))))) (defn unwatch "Stops each watcher in the list of `watchers`."