diff --git a/README.md b/README.md index bddb575..0d5605a 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,26 @@ Resources for understanding and testing social sharing: - [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/) - [Twitter Card Validator](https://cards-dev.twitter.com/validator) +### Post links + +quickblog adds `prev` and `next` metadata to each post (where `prev` is the +previous post and `next` is the next post in date order, oldest to newest). You +can make use of these by adding something similar to this to your `post.html` +template: + +``` html +{% if any prev next %} +
+{% if prev %} +
{{prev.title}}
+{% endif %} +{% if next %} +
{{next.title}}
+{% endif %} +
+{% endif %} +``` + ## Templates quickblog uses the following templates in site generation: diff --git a/src/quickblog/api.clj b/src/quickblog/api.clj index 6cf563e..824c747 100644 --- a/src/quickblog/api.clj +++ b/src/quickblog/api.clj @@ -630,7 +630,9 @@ :as opts} (-> opts (assoc :watch (format "" - lib/live-reload-script)) + lib/live-reload-script) + ;; Include preview posts in post linking (prev and next) + :include-preview-posts? true) apply-default-opts render)] (reset! posts-cache (:posts opts)) diff --git a/src/quickblog/internal.clj b/src/quickblog/internal.clj index 4c8022b..08596e0 100644 --- a/src/quickblog/internal.clj +++ b/src/quickblog/internal.clj @@ -217,6 +217,45 @@ (println error) true)) +(defn- delink-prev + "Make sure we don't have a chain of previous posts" + [post] + (update post :prev dissoc :prev)) + +(defn link-posts-reduce-f [{:keys [posts prev] :as acc} post] + (let [cur (when prev + (-> prev (assoc :next post) delink-prev)) + post' (if prev + (assoc post :prev prev) + post) + posts' (if cur + (conj posts cur) + posts)] + {:posts posts', :prev post'})) + +(defn link-posts + "For a map of files to posts, sorts them adds to each post a :prev key pointing + to the previous post and a :next key pointing to the next post, where previous + and next are defined by the order imposed by `sort-posts`. Preview posts are + skipped unless `:include-preview-posts?` is set true in `opts`." + [{:keys [include-preview-posts?]} posts] + (->> (let [maybe-remove-previews (if include-preview-posts? identity remove-previews) + ps (->> posts + vals + maybe-remove-previews + sort-posts + reverse + (reduce link-posts-reduce-f {:posts [], :prev nil}))] + (-> ps :posts (conj (-> ps :prev delink-prev)))) + (map (fn [{:keys [file] :as post}] + [file post])) + (into {}) + ;; We need to merge this with the original posts map because that + ;; map might contain preview posts, whereas the linking process + ;; intentionally skips preview posts unless the `include-preview-posts?` + ;; param is true + (merge posts))) + (defn load-posts [{:keys [cache-dir cached-posts posts-dir] :as opts}] (if (fs/exists? posts-dir) (let [cache-file (fs/file cache-dir cache-filename) @@ -225,19 +264,20 @@ (set post-paths) (set (fs/modified-since cache-file post-paths))) _cached-post-paths (set/difference post-paths modified-post-paths)] - (merge (->> cached-posts - (map (fn [[file post]] - [file (assoc post :html (read-cached-post opts file))])) - (into {})) - (->> modified-post-paths - (map (juxt ->filename (partial load-post opts))) - (remove (partial has-error? opts)) - (into {})))) + (->> (merge (->> cached-posts + (map (fn [[file post]] + [file (assoc post :html (read-cached-post opts file))])) + (into {})) + (->> modified-post-paths + (map (juxt ->filename (partial load-post opts))) + (remove (partial has-error? opts)) + (into {}))) + (link-posts opts))) {})) (defn only-metadata [posts] (->> posts - (map (fn [[file post]] [file (dissoc post :html)])) + (map (fn [[file post]] [file (dissoc post :html :prev :next)])) (into {}))) (defn load-cache [{:keys [cache-dir rendering-system-files]}] diff --git a/test/quickblog/api_test.clj b/test/quickblog/api_test.clj index 344bc9e..25eec3d 100644 --- a/test/quickblog/api_test.clj +++ b/test/quickblog/api_test.clj @@ -160,12 +160,14 @@ (testing "with blank page suffix" (with-dirs [posts-dir templates-dir - out-dir] + out-dir + cache-dir] (write-test-post posts-dir {:tags #{"foobar" "tag with spaces"}}) (api/render {:page-suffix "" :posts-dir posts-dir :templates-dir templates-dir - :out-dir out-dir}) + :out-dir out-dir + :cache-dir cache-dir}) (doseq [filename ["test.html" "index.html" "archive.html" (fs/file "tags" "index.html") (fs/file "tags" "tag-with-spaces.html") @@ -298,6 +300,74 @@ :out-dir out-dir}) (is (not (str/includes? (slurp (fs/file out-dir "test.html")) lib/live-reload-script)))))) +(deftest link-posts + (let [posts (->> (range 1 5) + (map (fn [i] + {:file (format "post%s.md" i) + :title (format "post%s" i) + :date (format "2024-02-0%s" i) + :preview? (= 3 i)}))) + write-templates! (fn [templates-dir] + (fs/create-dirs templates-dir) + (spit (fs/file templates-dir "base.html") + "{{body | safe }}") + (spit (fs/file templates-dir "post.html") + (str "{% if prev %}prev: {{prev.title}}{% endif %}" + "\n" + "{% if next %}next: {{next.title}}{% endif %}")))] + + (testing "add prev and next posts to post metadata" + (with-dirs [posts-dir + templates-dir + cache-dir + out-dir] + (doseq [post posts] + (write-test-post posts-dir post)) + (write-templates! templates-dir) + (api/render {:posts-dir posts-dir + :templates-dir templates-dir + :cache-dir cache-dir + :out-dir out-dir}) + (is (= (slurp (fs/file out-dir "post1.html")) + "\nnext: post2")) + (is (= (slurp (fs/file out-dir "post2.html")) + "prev: post1\nnext: post4")) + (is (= (slurp (fs/file out-dir "post3.html")) + "\n")) + (is (= (slurp (fs/file out-dir "post4.html")) + "prev: post2\n")) + (let [cache (lib/load-cache {:cache-dir cache-dir})] + (is (nil? (get-in cache ["post1.md" :prev]))) + (is (= (get-in cache ["post1.md" :next :title "post2"]))) + (is (= (get-in cache ["post2.md" :prev :title "post1"]))) + (is (= (get-in cache ["post2.md" :next :title "post4"]))) + (is (nil? (get-in cache ["post3.md" :prev]))) + (is (nil? (get-in cache ["post3.md" :next]))) + (is (= (get-in cache ["post4.md" :prev :title "post2"]))) + (is (nil? (get-in cache ["post4.md" :next])))))) + + (testing "include preview posts" + (with-dirs [posts-dir + templates-dir + cache-dir + out-dir] + (doseq [post posts] + (write-test-post posts-dir post)) + (write-templates! templates-dir) + (api/render {:posts-dir posts-dir + :templates-dir templates-dir + :cache-dir cache-dir + :out-dir out-dir + :include-preview-posts? true}) + (is (= (slurp (fs/file out-dir "post1.html")) + "\nnext: post2")) + (is (= (slurp (fs/file out-dir "post2.html")) + "prev: post1\nnext: post3")) + (is (= (slurp (fs/file out-dir "post3.html")) + "prev: post2\nnext: post4")) + (is (= (slurp (fs/file out-dir "post4.html")) + "prev: post3\n")))))) + ;; disabled, flaky in CI, cc @jmglov #_(deftest caching (testing "assets" diff --git a/test/quickblog/test_runner.clj b/test/quickblog/test_runner.clj index c5e32d4..d051630 100644 --- a/test/quickblog/test_runner.clj +++ b/test/quickblog/test_runner.clj @@ -1,11 +1,11 @@ (ns quickblog.test-runner - {:org.babashka/cli {:coerce {:dirs :strings - :nses :symbols - :patterns :strings - :vars :symbols - :includes :keywords - :excludes :keywords - :only :symbols}}} + {:org.babashka/cli {:coerce {:dirs [:string] + :nses [:symbol] + :patterns [:string] + :vars [:symbol] + :includes [:keyword] + :excludes [:keyword] + :only :symbol}}} (:refer-clojure :exclude [test]) (:require [clojure.test :as test]