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 %}
+
+{% endif %}
+{% if next %}
+
+{% 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"))))))