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))))