diff --git a/CHANGELOG.md b/CHANGELOG.md index 745cb35..3276597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Instances of quickblog can be seen here: ## Unreleased +- Link to previous and next posts; see "Linking to previous and next posts" in + the README ([@jmglov](https://github.com/jmglov)) - Fix flaky caching tests ([@jmglov](https://github.com/jmglov)) - Fix argument passing in test runner ([@jmglov](https://github.com/jmglov)) - Add `--date` to api/new. ([@jmglov](https://github.com/jmglov)) diff --git a/README.md b/README.md index 48a8638..4cd63e8 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,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) +### Linking to previous and next posts + +If you set the `:link-prev-next-posts` option to `true`, 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/internal.clj b/src/quickblog/internal.clj index cb6ac3b..457b277 100644 --- a/src/quickblog/internal.clj +++ b/src/quickblog/internal.clj @@ -273,15 +273,17 @@ (map first) set)) -(defn modified-posts [{:keys [force-render out-dir posts posts-dir - rendering-system-files]}] +(defn modified-posts + [{:keys [force-render out-dir posts cached-posts posts-dir rendering-system-files]}] (->> posts - (filter (fn [[file _]] + (filter (fn [[file post]] (let [out-file (fs/file out-dir (html-file file)) post-file (fs/file posts-dir file)] (or force-render (rendering-modified? out-file - (cons post-file rendering-system-files)))))) + (cons post-file rendering-system-files)) + (not= (select-keys post [:prev :next]) + (select-keys (cached-posts file) [:prev :next])))))) (map first) set)) @@ -290,9 +292,38 @@ (mapcat (partial map (fn [[_ {:keys [tags]}]] tags))) (apply set/union))) -(defn refresh-cache [{:keys [cached-posts - posts] - :as opts}] +(defn assoc-prev-next + "If the `:link-prev-next-posts` opt is true, adds to each post a :prev key + pointing to the filename of the previous post by date and a :next key pointing + to the filename of the next post by date. Preview posts are skipped unless the + `:include-preview-posts-in-linking` is true." + [{:keys [posts link-prev-next-posts include-preview-posts-in-linking] + :as opts}] + (if link-prev-next-posts + (let [remove-preview-posts (if include-preview-posts-in-linking + identity + #(remove (comp :preview val) %)) + post-keys (->> posts + remove-preview-posts + (sort-by (comp :date val)) + (mapv first))] + (assoc opts :posts + ;; We need to merge the linked posts on top of the original ones + ;; so that preview posts are still present even when they're + ;; excluded from linking + (merge posts + (->> post-keys + (map-indexed + (fn [i k] + [k (assoc (posts k) + :prev (when (pos? i) + (post-keys (dec i))) + :next (when (< i (dec (count post-keys))) + (post-keys (inc i))))])) + (into {}))))) + opts)) + +(defn refresh-cache [{:keys [cached-posts posts] :as opts}] ;; watch mode manages caching manually, so if cached-posts and posts are ;; already set, use them as is (let [cached-posts (if cached-posts @@ -303,6 +334,7 @@ posts (load-posts opts)) opts (assoc opts :posts posts) + opts (assoc-prev-next opts) opts (assoc opts :modified-metadata (modified-metadata opts))] (assoc opts @@ -397,12 +429,19 @@ out-dir page-suffix page-template - post-template] + post-template + link-prev-next-posts + post-order + posts] :as opts} - {:keys [file html description image image-alt] + {:keys [file html description image image-alt prev next] :as post-metadata}] (let [out-file (fs/file out-dir (html-file file)) - post-metadata (merge {:discuss discuss-link :page-suffix page-suffix} (assoc post-metadata :body @html)) + post-metadata (merge {:discuss discuss-link :page-suffix page-suffix} + (assoc post-metadata :body @html) + (when link-prev-next-posts + {:next (posts next) + :prev (posts prev)})) body (selmer/render post-template post-metadata) author (-> (:twitter-handle post-metadata) (or twitter-handle)) image (when image (if (re-matches #"^https?://.+" image) diff --git a/test/quickblog/api_test.clj b/test/quickblog/api_test.clj index cd38817..eeb3f76 100644 --- a/test/quickblog/api_test.clj +++ b/test/quickblog/api_test.clj @@ -573,3 +573,64 @@ mtime (str (fs/last-modified-time file))]] (is (= [filename (mtimes filename)] [filename mtime])))))) + +(deftest link-prev-next-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 + :link-prev-next-posts true}) + (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")))) + + (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 + :link-prev-next-posts true + :include-preview-posts-in-linking 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"))))))