Add page.previous and page.next liquid variables (#22)

* sort pages

* rename tutorial files to enforce an order

* add prev next props to posts

* test previous next

* prevent nested recursive links between posts

* add a next/prev footer

* make prev/next work as expected for both pages and posts

* remove sort by index

* bold current section in nav
This commit is contained in:
Facundo Olano 2024-03-02 16:16:40 -03:00 committed by GitHub
parent fd87093621
commit c02e1fc91e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 180 additions and 38 deletions

View file

@ -1,7 +1,9 @@
<nav> <nav>
<a href="/">{{ site.config.name }}</a> <a href="/">{{ site.config.name }}</a>
<a href="/tutorial/">tutorial</a>
<a href="/blog/">devlog</a> {% assign section = page.path|split:"/" | first %}
<a {% if section == "tutorial" %}style="font-weight:bold"{% endif %} href="/tutorial/">tutorial</a>
<a {% if section == "blog" %}style="font-weight:bold"{% endif %} href="/blog/">devlog</a>
<div class="nav-right hidden-mobile"> <div class="nav-right hidden-mobile">
<a href="https://github.com/facundoolano/jorge">github</a> <a href="https://github.com/facundoolano/jorge">github</a>

View file

@ -12,4 +12,29 @@ layout: base
{% endif %} {% endif %}
</header> </header>
{{ content }} {{ content }}
<br/>
<br/>
{% comment%}
if page.date then it's a post, and we want to invert the prev/next for the controls:
the next arrow should take you to the newer post, which appears fist in the site.posts list.
{%endcomment%}
{% if page.date %}
{% assign previous = page.next %}
{% assign next = page.previous %}
{% else %}
{% assign previous = page.previous %}
{% assign next = page.next %}
{% endif %}
<div class="pagination" style="display:flex">
{% if previous %}
<span><a class="title" href="{{previous.url}}"> {{ previous.subtitle | default:previous.title}}</a></span>
{% endif %}
{% if next %}
<span style="margin-left: auto"><a class="title" href="{{next.url}}">{{ next.subtitle | default:next.title}}</a></p>
{% endif %}
</div>
</div> </div>

View file

@ -41,7 +41,7 @@ $ jorge serve
<h2><a href="/tutorial" class="title" id="tutorial">Tutorial</a></h2> <h2><a href="/tutorial" class="title" id="tutorial">Tutorial</a></h2>
<ol start='0'> <ol start='0'>
{% for page in site.pages|where:"dir", "/tutorial"|sort:"index" %} {% for page in site.pages|where:"dir", "/tutorial" %}
<li> <li>
<a class="title" href="{{ page.url }}">{{ page.title }}{%if page.subtitle%}: {{page.subtitle|downcase}}{%endif%}</a> <a class="title" href="{{ page.url }}">{{ page.title }}{%if page.subtitle%}: {{page.subtitle|downcase}}{%endif%}</a>
</li> </li>

View file

@ -3,7 +3,6 @@ title: Introduction
layout: post layout: post
lang: en lang: en
tags: [tutorial] tags: [tutorial]
index: 0
--- ---
#+OPTIONS: toc:nil num:nil #+OPTIONS: toc:nil num:nil
#+LANGUAGE: en #+LANGUAGE: en
@ -14,10 +13,6 @@ jorge started as a Go learning project, aimed at streamlining my blogging workfl
This tutorial covers the basics of using jorge, from starting a site to deploying it. I tried to keep it accessible, but you may need to consult with [[https://jekyllrb.com/docs/][Jekyll]] or [[https://gohugo.io/documentation/][Hugo]] documentation if you never used a static site generator, want to get the finer-grained details of template syntax, etc. This tutorial covers the basics of using jorge, from starting a site to deploying it. I tried to keep it accessible, but you may need to consult with [[https://jekyllrb.com/docs/][Jekyll]] or [[https://gohugo.io/documentation/][Hugo]] documentation if you never used a static site generator, want to get the finer-grained details of template syntax, etc.
#+HTML: <br>
#+ATTR_HTML: :align right
Next: [[file:installation][install jorge]].
*** Notes *** Notes
[fn:1] Facundo Olano 👋 [fn:1] Facundo Olano 👋

View file

@ -1,9 +1,8 @@
--- ---
title: Installation title: Install jorge
layout: post layout: post
lang: en lang: en
tags: [tutorial] tags: [tutorial]
index: 1
--- ---
#+OPTIONS: toc:nil num:nil #+OPTIONS: toc:nil num:nil
#+LANGUAGE: en #+LANGUAGE: en
@ -48,7 +47,3 @@ Flags:
Run "jorge <command> --help" for more information on a command. Run "jorge <command> --help" for more information on a command.
#+end_src #+end_src
#+HTML: <br>
#+ATTR_HTML: :align right
Next: [[file:jorge-init][start a website]].

View file

@ -4,7 +4,6 @@ subtitle: Start a website
layout: post layout: post
lang: en lang: en
tags: [tutorial] tags: [tutorial]
index: 2
--- ---
#+OPTIONS: toc:nil num:nil #+OPTIONS: toc:nil num:nil
#+LANGUAGE: en #+LANGUAGE: en
@ -60,8 +59,3 @@ Note how jorge assumes that the ~.html~ file extension will be omitted when serv
and that index files represent URL directories (~src/blog/index.html~ will be served as ~/blog/)~. and that index files represent URL directories (~src/blog/index.html~ will be served as ~/blog/)~.
If you prefer to build your site from scratch, you can skip running ~jorge init~ altogether; the rest of the commands only expect a ~src/~ directory to work with. If you prefer to build your site from scratch, you can skip running ~jorge init~ altogether; the rest of the commands only expect a ~src/~ directory to work with.
#+HTML: <br>
#+ATTR_HTML: :align right
Next: [[file:jorge-serve][browse the site locally]].

View file

@ -4,7 +4,6 @@ subtitle: Browse the site locally
layout: post layout: post
lang: en lang: en
tags: [tutorial] tags: [tutorial]
index: 3
--- ---
#+OPTIONS: toc:nil num:nil #+OPTIONS: toc:nil num:nil
#+LANGUAGE: en #+LANGUAGE: en
@ -61,7 +60,3 @@ If you update the code in ~src/index.html~, you should see your browser tab refr
#+end_src #+end_src
The new title should show up on the browser page. The new title should show up on the browser page.
#+HTML: <br>
#+ATTR_HTML: :align right
Next: [[file:jorge-post][add a blog post]].

View file

@ -4,7 +4,6 @@ subtitle: Add a blog post
layout: post layout: post
lang: en lang: en
tags: [tutorial] tags: [tutorial]
index: 4
--- ---
#+OPTIONS: toc:nil num:nil #+OPTIONS: toc:nil num:nil
#+LANGUAGE: en #+LANGUAGE: en
@ -83,10 +82,6 @@ $ jorge post "Another kind of post"
added src/2024-02-23-another-kind-of-post.md added src/2024-02-23-another-kind-of-post.md
#+end_src #+end_src
#+HTML: <br>
#+ATTR_HTML: :align right
Next: [[file:jorge-build][prepare for production]].
*** Notes *** Notes
[fn:1] Both a blog archive and the RSS feed (technically [[https://en.wikipedia.org/wiki/Atom_(web_standard)][Atom]]) are already implemented in the default site generated by ~jorge init~. [fn:1] Both a blog archive and the RSS feed (technically [[https://en.wikipedia.org/wiki/Atom_(web_standard)][Atom]]) are already implemented in the default site generated by ~jorge init~.

View file

@ -4,7 +4,6 @@ subtitle: Prepare for production
layout: post layout: post
lang: en lang: en
tags: [tutorial] tags: [tutorial]
index: 5
--- ---
#+OPTIONS: toc:nil num:nil #+OPTIONS: toc:nil num:nil
#+LANGUAGE: en #+LANGUAGE: en

View file

@ -4,7 +4,7 @@ title: Tutorial
--- ---
<ol start='0'> <ol start='0'>
{% for page in site.pages|where:"dir", "/tutorial"|sort:"index" %} {% for page in site.pages|where:"dir", "/tutorial" %}
<li> <li>
<a class="title" href="{{ page.url }}">{{ page.title }}{%if page.subtitle%}: {{page.subtitle|downcase}}{%endif%}</a> <a class="title" href="{{ page.url }}">{{ page.title }}{%if page.subtitle%}: {{page.subtitle|downcase}}{%endif%}</a>
</li> </li>

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"maps"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -181,21 +182,56 @@ func (site *Site) loadTemplates() error {
return err return err
} }
// sort posts by reverse chronological order // sort by reverse chronological order when date is present
Compare := func(a map[string]interface{}, b map[string]interface{}) int { // otherwise by path alphabetical
return b["date"].(time.Time).Compare(a["date"].(time.Time)) CompareTemplates := func(a map[string]interface{}, b map[string]interface{}) int {
if bdate, ok := b["date"]; ok {
if adate, ok := a["date"]; ok {
return bdate.(time.Time).Compare(adate.(time.Time))
} }
slices.SortFunc(site.posts, Compare) }
return strings.Compare(a["path"].(string), b["path"].(string))
}
slices.SortFunc(site.posts, CompareTemplates)
slices.SortFunc(site.pages, CompareTemplates)
for _, posts := range site.tags { for _, posts := range site.tags {
slices.SortFunc(posts, Compare) slices.SortFunc(posts, CompareTemplates)
} }
// populate previous and next in template index
site.addPrevNext(site.pages)
site.addPrevNext(site.posts)
return nil return nil
} }
func (site *Site) addPrevNext(posts []map[string]interface{}) {
for i, post := range posts {
path := filepath.Join(site.Config.RootDir, post["src_path"].(string))
// only consider them part of the same collection if they share the directory
if i > 0 && post["dir"] == posts[i-1]["dir"] {
// make a copy of the map, without prev/next (to avoid weird recursion)
previous := maps.Clone(posts[i-1])
delete(previous, "previous")
delete(previous, "next")
site.templates[path].Metadata["previous"] = previous
}
if i < len(posts)-1 && post["dir"] == posts[i+1]["dir"] {
next := maps.Clone(posts[i+1])
delete(next, "previous")
delete(next, "next")
site.templates[path].Metadata["next"] = next
}
}
}
func (site *Site) Build() error { func (site *Site) Build() error {
// clear previous target contents // clear previous target contents
os.RemoveAll(site.Config.TargetDir) os.RemoveAll(site.Config.TargetDir)
os.Mkdir(site.Config.SrcDir, DIR_RWE_MODE) os.Mkdir(site.Config.
SrcDir, DIR_RWE_MODE)
wg, files := spawnBuildWorkers(site) wg, files := spawnBuildWorkers(site)
defer wg.Wait() defer wg.Wait()

View file

@ -117,6 +117,112 @@ title: about
} }
func TestPreviousNext(t *testing.T) {
config := newProject()
defer os.RemoveAll(config.RootDir)
// prev next distinguish between series in different dirs
// add two different blog dirs and two different page dirs (tutorials)
blog1 := filepath.Join(config.SrcDir, "blog1")
blog2 := filepath.Join(config.SrcDir, "blog2")
tutorial1 := filepath.Join(config.SrcDir, "tutorial1")
tutorial2 := filepath.Join(config.SrcDir, "tutorial2")
os.Mkdir(blog1, DIR_RWE_MODE)
os.Mkdir(blog2, DIR_RWE_MODE)
os.Mkdir(tutorial1, DIR_RWE_MODE)
os.Mkdir(tutorial2, DIR_RWE_MODE)
newFile(blog1, "p1-1.html", `---
date: 2024-01-01
---`)
newFile(blog1, "p1-2.html", `---
date: 2024-01-02
---`)
newFile(blog1, "p1-3.html", `---
date: 2024-01-03
---`)
newFile(blog2, "p2-1.html", `---
date: 2024-02-01
---`)
newFile(blog2, "p2-2.html", `---
date: 2024-02-02
---`)
newFile(blog2, "p2-3.html", `---
date: 2024-02-03
---`)
newFile(tutorial1, "1-first-part.html", `---
---`)
newFile(tutorial1, "2-another-entry.html", `---
---`)
newFile(tutorial1, "3-goodbye.html", `---
---`)
newFile(tutorial2, "index.html", `---
---`)
newFile(tutorial2, "the-end.html", `---
---`)
newFile(tutorial2, "another-entry.html", `---
---`)
site, err := Load(*config)
// helper method to map a filename to its prev next keys (if any)
getPrevNext := func(dir string, filename string) (interface{}, interface{}) {
path := filepath.Join(dir, filename)
templ := site.templates[path]
return templ.Metadata["previous"], templ.Metadata["next"]
}
// tests for posts (sorted reverse chronologically, most recent one first)
assertEqual(t, err, nil)
prev, next := getPrevNext(blog1, "p1-3.html")
assertEqual(t, prev, nil)
assertEqual(t, next.(map[string]interface{})["url"], "/blog1/p1-2")
prev, next = getPrevNext(blog1, "p1-2.html")
assertEqual(t, prev.(map[string]interface{})["url"], "/blog1/p1-3")
assertEqual(t, next.(map[string]interface{})["url"], "/blog1/p1-1")
prev, next = getPrevNext(blog1, "p1-1.html")
assertEqual(t, prev.(map[string]interface{})["url"], "/blog1/p1-2")
assertEqual(t, next, nil)
assertEqual(t, err, nil)
prev, next = getPrevNext(blog2, "p2-3.html")
assertEqual(t, prev, nil)
assertEqual(t, next.(map[string]interface{})["url"], "/blog2/p2-2")
prev, next = getPrevNext(blog2, "p2-2.html")
assertEqual(t, prev.(map[string]interface{})["url"], "/blog2/p2-3")
assertEqual(t, next.(map[string]interface{})["url"], "/blog2/p2-1")
prev, next = getPrevNext(blog2, "p2-1.html")
assertEqual(t, prev.(map[string]interface{})["url"], "/blog2/p2-2")
assertEqual(t, next, nil)
// test for pages based on filename
prev, next = getPrevNext(tutorial1, "1-first-part.html")
assertEqual(t, prev, nil)
assertEqual(t, next.(map[string]interface{})["url"], "/tutorial1/2-another-entry")
prev, next = getPrevNext(tutorial1, "2-another-entry.html")
assertEqual(t, prev.(map[string]interface{})["url"], "/tutorial1/1-first-part")
assertEqual(t, next.(map[string]interface{})["url"], "/tutorial1/3-goodbye")
prev, next = getPrevNext(tutorial1, "3-goodbye.html")
assertEqual(t, prev.(map[string]interface{})["url"], "/tutorial1/2-another-entry")
assertEqual(t, next, nil)
// ensure alphabetical and index skipped
prev, next = getPrevNext(tutorial2, "another-entry.html")
assertEqual(t, prev, nil)
assertEqual(t, next.(map[string]interface{})["url"], "/tutorial2/the-end")
prev, next = getPrevNext(tutorial2, "the-end.html")
assertEqual(t, prev.(map[string]interface{})["url"], "/tutorial2/another-entry")
assertEqual(t, next, nil)
}
func TestRenderArchive(t *testing.T) { func TestRenderArchive(t *testing.T) {
config := newProject() config := newProject()
defer os.RemoveAll(config.RootDir) defer os.RemoveAll(config.RootDir)