diff --git a/API.md b/API.md
index ca0cc99..6d54115 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 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`
+``` 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#L712-L717)
+## `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 `: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-L710)
# 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)
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
diff --git a/README.md b/README.md
index 6f7e434..7ef14e0 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)
+ :serve false
+ :block false))
+ (quickblog/serve (assoc (:blog opts)
+ :out-dir (:out-dir opts))))}
+ }}
+```
+
## Breaking changes
### posts.edn removed
diff --git a/src/quickblog/api.clj b/src/quickblog/api.clj
index d5d7cbf..6ea8913 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 falsey, returns a zero-arity
+ `stop-server!` function that will stop the server when called."
{:org.babashka/cli
{:spec
{:port
@@ -615,24 +616,34 @@
([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))
(defn watch
"Watches posts, templates, and assets for changes. Runs file server using
- `serve`."
+ `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]
+ (let [{:keys [assets-dir assets-out-dir posts-dir templates-dir
+ serve block]
:as opts}
(-> opts
(assoc :watch (format ""
@@ -640,56 +651,67 @@
apply-default-opts
render)]
(reset! posts-cache (:posts opts))
- (serve opts false)
+ (when (not (false? 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 (not (false? block))
+ @(promise)
+ watchers)))))
+
+(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))))