mirror of
https://github.com/facundoolano/jorge.git
synced 2024-12-25 21:58:28 +01:00
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:
parent
fd87093621
commit
c02e1fc91e
12 changed files with 180 additions and 38 deletions
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 👋
|
|
@ -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]].
|
|
|
@ -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]].
|
|
|
@ -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]].
|
|
|
@ -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~.
|
|
@ -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
|
|
@ -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>
|
||||||
|
|
48
site/site.go
48
site/site.go
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Compare(a["path"].(string), b["path"].(string))
|
||||||
}
|
}
|
||||||
slices.SortFunc(site.posts, Compare)
|
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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue