Skip to content

Commit 14eca6b

Browse files
committed
Link to previous and next post
1 parent 4c0c94f commit 14eca6b

File tree

4 files changed

+132
-10
lines changed

4 files changed

+132
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ Instances of quickblog can be seen here:
1717

1818
## Unreleased
1919

20+
- Link to previous and next posts; see "Linking to previous and next posts" in
21+
the README ([@jmglov](https://github.com/jmglov))
2022
- Fix flaky caching tests ([@jmglov](https://github.com/jmglov))
2123
- Fix argument passing in test runner ([@jmglov](https://github.com/jmglov))
2224
- Add `--date` to api/new. ([@jmglov](https://github.com/jmglov))

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,26 @@ Resources for understanding and testing social sharing:
217217
- [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/)
218218
- [Twitter Card Validator](https://cards-dev.twitter.com/validator)
219219

220+
### Linking to previous and next posts
221+
222+
If you set the `:link-prev-next-posts` option to `true`, quickblog adds `prev`
223+
and `next` metadata to each post (where `prev` is the previous post and `next`
224+
is the next post in date order, oldest to newest). You can make use of these by
225+
adding something similar to this to your `post.html` template:
226+
227+
``` html
228+
{% if any prev next %}
229+
<div class="post-prev-next">
230+
{% if prev %}
231+
<div>⏪ <a href="{{prev.file|replace:.md:.html}}">{{prev.title}}</a></div>
232+
{% endif %}
233+
{% if next %}
234+
<div><a href="{{next.file|replace:.md:.html}}">{{next.title}}</a> ⏩</div>
235+
{% endif %}
236+
</div>
237+
{% endif %}
238+
```
239+
220240
## Templates
221241

222242
quickblog uses the following templates in site generation:

src/quickblog/internal.clj

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -273,15 +273,17 @@
273273
(map first)
274274
set))
275275

276-
(defn modified-posts [{:keys [force-render out-dir posts posts-dir
277-
rendering-system-files]}]
276+
(defn modified-posts
277+
[{:keys [force-render out-dir posts cached-posts posts-dir rendering-system-files]}]
278278
(->> posts
279-
(filter (fn [[file _]]
279+
(filter (fn [[file post]]
280280
(let [out-file (fs/file out-dir (html-file file))
281281
post-file (fs/file posts-dir file)]
282282
(or force-render
283283
(rendering-modified? out-file
284-
(cons post-file rendering-system-files))))))
284+
(cons post-file rendering-system-files))
285+
(not= (select-keys post [:prev :next])
286+
(select-keys (cached-posts file) [:prev :next]))))))
285287
(map first)
286288
set))
287289

@@ -290,9 +292,38 @@
290292
(mapcat (partial map (fn [[_ {:keys [tags]}]] tags)))
291293
(apply set/union)))
292294

293-
(defn refresh-cache [{:keys [cached-posts
294-
posts]
295-
:as opts}]
295+
(defn assoc-prev-next
296+
"If the `:link-prev-next-posts` opt is true, adds to each post a :prev key
297+
pointing to the filename of the previous post by date and a :next key pointing
298+
to the filename of the next post by date. Preview posts are skipped unless the
299+
`:include-preview-posts-in-linking` is true."
300+
[{:keys [posts link-prev-next-posts include-preview-posts-in-linking]
301+
:as opts}]
302+
(if link-prev-next-posts
303+
(let [remove-preview-posts (if include-preview-posts-in-linking
304+
identity
305+
#(remove (comp :preview val) %))
306+
post-keys (->> posts
307+
remove-preview-posts
308+
(sort-by (comp :date val))
309+
(mapv first))]
310+
(assoc opts :posts
311+
;; We need to merge the linked posts on top of the original ones
312+
;; so that preview posts are still present even when they're
313+
;; excluded from linking
314+
(merge posts
315+
(->> post-keys
316+
(map-indexed
317+
(fn [i k]
318+
[k (assoc (posts k)
319+
:prev (when (pos? i)
320+
(post-keys (dec i)))
321+
:next (when (< i (dec (count post-keys)))
322+
(post-keys (inc i))))]))
323+
(into {})))))
324+
opts))
325+
326+
(defn refresh-cache [{:keys [cached-posts posts] :as opts}]
296327
;; watch mode manages caching manually, so if cached-posts and posts are
297328
;; already set, use them as is
298329
(let [cached-posts (if cached-posts
@@ -303,6 +334,7 @@
303334
posts
304335
(load-posts opts))
305336
opts (assoc opts :posts posts)
337+
opts (assoc-prev-next opts)
306338
opts (assoc opts
307339
:modified-metadata (modified-metadata opts))]
308340
(assoc opts
@@ -397,12 +429,19 @@
397429
out-dir
398430
page-suffix
399431
page-template
400-
post-template]
432+
post-template
433+
link-prev-next-posts
434+
post-order
435+
posts]
401436
:as opts}
402-
{:keys [file html description image image-alt]
437+
{:keys [file html description image image-alt prev next]
403438
:as post-metadata}]
404439
(let [out-file (fs/file out-dir (html-file file))
405-
post-metadata (merge {:discuss discuss-link :page-suffix page-suffix} (assoc post-metadata :body @html))
440+
post-metadata (merge {:discuss discuss-link :page-suffix page-suffix}
441+
(assoc post-metadata :body @html)
442+
(when link-prev-next-posts
443+
{:next (posts next)
444+
:prev (posts prev)}))
406445
body (selmer/render post-template post-metadata)
407446
author (-> (:twitter-handle post-metadata) (or twitter-handle))
408447
image (when image (if (re-matches #"^https?://.+" image)

test/quickblog/api_test.clj

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,3 +573,64 @@
573573
mtime (str (fs/last-modified-time file))]]
574574
(is (= [filename (mtimes filename)]
575575
[filename mtime]))))))
576+
577+
(deftest link-prev-next-posts
578+
(let [posts (->> (range 1 5)
579+
(map (fn [i]
580+
{:file (format "post%s.md" i)
581+
:title (format "post%s" i)
582+
:date (format "2024-02-0%s" i)
583+
:preview? (= 3 i)})))
584+
write-templates! (fn [templates-dir]
585+
(fs/create-dirs templates-dir)
586+
(spit (fs/file templates-dir "base.html")
587+
"{{body | safe }}")
588+
(spit (fs/file templates-dir "post.html")
589+
(str "{% if prev %}prev: {{prev.title}}{% endif %}"
590+
"\n"
591+
"{% if next %}next: {{next.title}}{% endif %}")))]
592+
593+
(testing "add prev and next posts to post metadata"
594+
(with-dirs [posts-dir
595+
templates-dir
596+
cache-dir
597+
out-dir]
598+
(doseq [post posts]
599+
(write-test-post posts-dir post))
600+
(write-templates! templates-dir)
601+
(api/render {:posts-dir posts-dir
602+
:templates-dir templates-dir
603+
:cache-dir cache-dir
604+
:out-dir out-dir
605+
:link-prev-next-posts true})
606+
(is (= (slurp (fs/file out-dir "post1.html"))
607+
"\nnext: post2"))
608+
(is (= (slurp (fs/file out-dir "post2.html"))
609+
"prev: post1\nnext: post4"))
610+
(is (= (slurp (fs/file out-dir "post3.html"))
611+
"\n"))
612+
(is (= (slurp (fs/file out-dir "post4.html"))
613+
"prev: post2\n"))))
614+
615+
(testing "include preview posts"
616+
(with-dirs [posts-dir
617+
templates-dir
618+
cache-dir
619+
out-dir]
620+
(doseq [post posts]
621+
(write-test-post posts-dir post))
622+
(write-templates! templates-dir)
623+
(api/render {:posts-dir posts-dir
624+
:templates-dir templates-dir
625+
:cache-dir cache-dir
626+
:out-dir out-dir
627+
:link-prev-next-posts true
628+
:include-preview-posts-in-linking true})
629+
(is (= (slurp (fs/file out-dir "post1.html"))
630+
"\nnext: post2"))
631+
(is (= (slurp (fs/file out-dir "post2.html"))
632+
"prev: post1\nnext: post3"))
633+
(is (= (slurp (fs/file out-dir "post3.html"))
634+
"prev: post2\nnext: post4"))
635+
(is (= (slurp (fs/file out-dir "post4.html"))
636+
"prev: post3\n"))))))

0 commit comments

Comments
 (0)