diff options
Diffstat (limited to 'internal/web/templates')
| -rw-r--r-- | internal/web/templates/blog/page.html | 3 | ||||
| -rw-r--r-- | internal/web/templates/blog/post.html | 3 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/design.html | 76 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/page_form.html | 2 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/post_form.html | 2 | ||||
| -rw-r--r-- | internal/web/templates/partials/editor.html | 11 | ||||
| -rw-r--r-- | internal/web/templates/partials/imagelib.html | 72 | ||||
| -rw-r--r-- | internal/web/templates/partials/imagepick.html | 13 | ||||
| -rw-r--r-- | internal/web/templates/partials/mdhelp.html | 2 | ||||
| -rw-r--r-- | internal/web/templates/partials/postlist.html | 6 |
10 files changed, 115 insertions, 75 deletions
diff --git a/internal/web/templates/blog/page.html b/internal/web/templates/blog/page.html index 99403ef..6f833f8 100644 --- a/internal/web/templates/blog/page.html +++ b/internal/web/templates/blog/page.html @@ -2,5 +2,6 @@ {{define "content"}} {{if not .Data.page.IsHome}}<h1 class="page-title">{{.Data.page.Title}}</h1>{{end}} {{if .Data.page.IntroHTML}}<div class="intro">{{html .Data.page.IntroHTML}}</div>{{end}} -{{if or .Data.posts .Data.page.IntroHTML}}{{template "postlist" .}}{{else}}<p class="muted">{{t "Nothing here yet."}}</p>{{end}} +{{if or .Data.posts .Data.page.IntroHTML .Data.page.OutroHTML}}{{template "postlist" .}}{{else}}<p class="muted">{{t "Nothing here yet."}}</p>{{end}} +{{if .Data.page.OutroHTML}}<div class="outro">{{html .Data.page.OutroHTML}}</div>{{end}} {{end}} diff --git a/internal/web/templates/blog/post.html b/internal/web/templates/blog/post.html index a3b0375..10d4c3a 100644 --- a/internal/web/templates/blog/post.html +++ b/internal/web/templates/blog/post.html @@ -4,6 +4,9 @@ <h1 class="post-title">{{.Data.post.Title}}</h1> <p class="post-date">{{if .Data.theme.ShowDates}}{{postdate .Data.theme.DateFormat .Data.post.CreatedAt}} · {{end}}<a href="{{if .Data.page.IsHome}}/{{else}}/{{.Data.page.Slug}}{{end}}">{{.Data.page.Title}}</a></p> {{template "posttags" .Data.post.Tags}} + {{$img := .Data.post.Image}}{{$place := .Data.theme.PostImage}} + {{if and $img (eq $place "top")}}<p class="post-image"><img src="/media/{{$img}}" alt=""></p>{{end}} <div class="post-body">{{html .Data.post.BodyHTML}}</div> + {{if and $img (eq $place "bottom")}}<p class="post-image"><img src="/media/{{$img}}" alt=""></p>{{end}} </article> {{end}} diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html index 261eb69..71597e8 100644 --- a/internal/web/templates/dashboard/design.html +++ b/internal/web/templates/dashboard/design.html @@ -181,6 +181,18 @@ {{range .Data.perPage}}<option value="{{.}}"{{if eq . $t.PostsPerPage}} selected{{end}}>{{.}}</option>{{end}} </select></label> </div> + <div class="row"> + <label>{{t "On listings show"}}<br><select name="list_style"> + <option value="full"{{if eq $t.ListStyle "full"}} selected{{end}}>{{t "The whole post"}}</option> + <option value="summary"{{if eq $t.ListStyle "summary"}} selected{{end}}>{{t "A summary and a “Read more” link"}}</option> + </select></label> + <label>{{t "Featured image on the post page"}}<br><select name="post_image"> + <option value="top"{{if eq $t.PostImage "top"}} selected{{end}}>{{t "Above the text"}}</option> + <option value="bottom"{{if eq $t.PostImage "bottom"}} selected{{end}}>{{t "Below the text"}}</option> + <option value="list"{{if eq $t.PostImage "list"}} selected{{end}}>{{t "Only on listings"}}</option> + </select></label> + </div> + <p class="muted small">{{t "A summary is the post up to a line with <!--more-->, or its first paragraphs. A post's featured image is picked on the post form and shown as a thumbnail on listings."}}</p> <h3>{{t "Around the posts"}}</h3> <p class="muted small">{{t "Your own HTML before and after the posts — an intro box, a newsletter form, a badge…"}}</p> {{template "arealist" (dict "area" "above" "root" .)}} @@ -260,19 +272,8 @@ </details> </div> -{{/* The library panel: filled by the script from /files?kind=image as JSON, one page at a time. */}} -<div class="picker" id="picker" hidden> - <div class="picker-box"> - <div class="picker-head"> - <input type="search" placeholder="{{t "Filter by name…"}}" aria-label="{{t "Filter by name…"}}"> - <span class="muted small pcount">{{tf "%d images" .Data.imageCount}}</span> - <button type="button" class="secondary small" data-pick="close">{{t "Close"}}</button> - </div> - <div class="picks"></div> - <p class="muted small pnone" hidden>{{t "No images match."}}</p> - <div class="pager"><button type="button" class="mini" data-pick="prev">← {{t "Newer"}}</button> <span class="pnum"></span> <button type="button" class="mini" data-pick="next">{{t "Older"}} →</button></div> - </div> -</div> +{{/* The library panel behind the image pickers (partials/imagelib.html). */}} +{{template "imagelib" (dict "sub" $sub "count" .Data.imageCount)}} <script> (function () { @@ -303,7 +304,7 @@ return row; } form.addEventListener("click", function (e) { - var b = e.target.closest("[data-act],[data-menu],[data-pick],.preset"); + var b = e.target.closest("[data-act],[data-menu],.preset"); // the image pickers are imagelib's if (!b) return; var row = b.closest(".modrow"), list = row && row.parentNode; switch (b.getAttribute("data-act")) { @@ -336,7 +337,6 @@ q("[name=link_hover_custom]").checked = q("[name=nav_hover_custom]").checked = false; touch(); return; } - pickAction(b); }); form.addEventListener("change", function (e) { var t = e.target; @@ -379,52 +379,6 @@ q("[name=at]").value = section(); // so the page comes back to the section in view }); - // ---- the image library panel: one shared panel, fetched page by page - var panel = document.getElementById("picker"), pick = null, page = 1, filter = "", timer; - if (!window.fetch) { qa(".imagepick .jsonly").forEach(function (el) { el.hidden = true; }); qa(".imagepick select").forEach(function (el) { el.className = ""; }); return; } - function pickAction(b) { - var act = b.getAttribute("data-pick"), ip = b.closest(".imagepick"); - if (act === "open") { pick = ip; panel.hidden = false; q("input", panel).value = filter = ""; load(1); q("input", panel).focus(); } - else if (act === "clear") { choose(ip, "none", ""); } - } - panel.addEventListener("click", function (e) { - var b = e.target.closest("[data-pick],.pick"); - if (!b) return; - if (b.className.indexOf("pick") === 0) { choose(pick, b.getAttribute("data-id"), b.getAttribute("data-name")); panel.hidden = true; return; } - switch (b.getAttribute("data-pick")) { - case "close": panel.hidden = true; break; - case "prev": load(page - 1); break; - case "next": load(page + 1); break; - } - }); - panel.addEventListener("keydown", function (e) { if (e.key === "Escape") panel.hidden = true; }); - q("input", panel).addEventListener("input", function () { clearTimeout(timer); timer = setTimeout(function () { filter = q("input", panel).value.trim(); load(1); }, 250); }); - function load(n) { - var url = "/b/" + form.getAttribute("data-sub") + "/files?kind=image&p=" + n + (filter ? "&q=" + encodeURIComponent(filter) : ""); - fetch(url, { headers: { Accept: "application/json" }, credentials: "same-origin" }).then(function (r) { return r.json(); }).then(function (d) { - page = d.page; - var grid = q(".picks", panel); grid.innerHTML = ""; - (d.files || []).forEach(function (f) { - var b = document.createElement("button"), img = document.createElement("img"), s = document.createElement("span"); - b.type = "button"; b.className = "pick"; b.setAttribute("data-id", f.id); b.setAttribute("data-name", f.filename); - img.loading = "lazy"; img.src = "/b/" + form.getAttribute("data-sub") + "/media/" + f.id; img.alt = ""; - s.className = "name"; s.textContent = f.filename; - b.appendChild(img); b.appendChild(s); grid.appendChild(b); - }); - q(".pnone", panel).hidden = !!(d.files && d.files.length); - q(".pnum", panel).textContent = d.last > 1 ? d.page + " / " + d.last : ""; - q("[data-pick=prev]", panel).disabled = d.page <= 1; q("[data-pick=next]", panel).disabled = d.page >= d.last; - }); - } - function choose(ip, id, name) { // the select stays the value the form sends - var sel = q("select", ip), pv = q(".preview", ip), found = false; - for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value === id) found = true; - if (!found) { var o = document.createElement("option"); o.value = id; o.textContent = name; sel.appendChild(o); } - sel.value = id; - pv.innerHTML = id === "none" ? '<span class="swatch">×</span>' : ""; - if (id !== "none") { var img = document.createElement("img"); img.src = "/b/" + form.getAttribute("data-sub") + "/media/" + id; img.alt = ""; pv.appendChild(img); } - touch(); - } })(); </script> {{end}} diff --git a/internal/web/templates/dashboard/page_form.html b/internal/web/templates/dashboard/page_form.html index 4754c91..18b0a86 100644 --- a/internal/web/templates/dashboard/page_form.html +++ b/internal/web/templates/dashboard/page_form.html @@ -9,6 +9,8 @@ <input name="slug" value="{{.Data.page.Slug}}" pattern="[a-z0-9]([a-z0-9-]*[a-z0-9])?" maxlength="80"></label> <label for="ed-intro">{{t "Intro text"}} <span class="muted">({{t "shown above the posts; optional"}})</span></label> {{template "editor" (dict "name" "intro" "value" .Data.page.IntroMD "format" .Data.page.Format "rows" 8 "tall" false "upload" (print "/b/" .Blog.Subdomain "/files/upload") "preview" (print "/b/" .Blog.Subdomain "/preview") "csrf" .CSRF)}} + <label for="ed-outro">{{t "Closing text"}} <span class="muted">({{t "shown after the posts, in the same format; optional"}})</span></label> + {{template "editor" (dict "name" "outro" "value" .Data.page.OutroMD "format" .Data.page.Format "formatof" "intro" "rows" 8 "tall" false "upload" (print "/b/" .Blog.Subdomain "/files/upload") "preview" (print "/b/" .Blog.Subdomain "/preview") "csrf" .CSRF)}} <label class="check"><input type="checkbox" name="show_in_nav"{{if .Data.page.ShowInNav}} checked{{end}}> {{t "Show in menu"}}</label> <p class="actionrow"> <button type="submit">{{t "Save"}}</button> diff --git a/internal/web/templates/dashboard/post_form.html b/internal/web/templates/dashboard/post_form.html index 70cddcc..9bd7624 100644 --- a/internal/web/templates/dashboard/post_form.html +++ b/internal/web/templates/dashboard/post_form.html @@ -39,6 +39,7 @@ </script> <label for="ed-body">{{t "Content"}}</label> {{template "editor" (dict "name" "body" "value" .Data.post.BodyMD "format" .Data.post.Format "rows" 24 "tall" true "upload" (print "/b/" .Blog.Subdomain "/files/upload") "preview" (print "/b/" .Blog.Subdomain "/preview") "csrf" .CSRF)}} + {{template "imagepick" (dict "name" "image" "current" .Data.post.Image "sub" .Blog.Subdomain "images" .Data.images "names" .Data.imageNames "label" (t "Featured image") "hint" (t "Optional. A thumbnail on listings, and on the post page where the Design tab puts it."))}} <label class="check"><input type="checkbox" name="published"{{if .Data.post.Published}} checked{{end}}> {{t "Visible on the blog"}}</label> <p class="actionrow"> <button type="submit">{{t "Save"}}</button> @@ -47,4 +48,5 @@ </p> </form> </div> +{{template "imagelib" (dict "sub" .Blog.Subdomain "count" .Data.imageCount)}} {{end}} diff --git a/internal/web/templates/partials/editor.html b/internal/web/templates/partials/editor.html index a0f8c7f..cfc5642 100644 --- a/internal/web/templates/partials/editor.html +++ b/internal/web/templates/partials/editor.html @@ -1,10 +1,13 @@ +{{/* A second editor in the same form (a page's outro) shares the first one's + Format row: pass "formatof" with that editor's name and no radios are + rendered, the script watches the other row instead. */}} {{define "editor"}}<div class="editor{{if eq .format "html"}} html{{end}}" id="ed-{{.name}}-wrap"> -<p class="ed-format" id="ed-{{.name}}-format"> +{{if not .formatof}}<p class="ed-format" id="ed-{{.name}}-format"> <span class="muted">{{t "Format"}}:</span> <label><input type="radio" name="format" value="markdown"{{if ne .format "html"}} checked{{end}}> Markdown</label> <label><input type="radio" name="format" value="html"{{if eq .format "html"}} checked{{end}}> HTML</label> <span class="muted small ed-htmlnote">{{t "Put on your blog exactly as written — embeds and scripts all work. An unclosed tag can break the page around it, so check your blog after saving."}}</span> -</p> +</p>{{end}} <div class="ed-toolbar" id="ed-{{.name}}-bar" hidden> <span class="ed-group ed-md"> <button type="button" data-cmd="bold" title="{{t "Bold"}} (Ctrl+B)"><svg viewBox="0 0 16 16"><path d="M4 2h5a3.5 3.5 0 0 1 2.4 6 3.75 3.75 0 0 1-2.2 6.8H4zM6 4v3.5h3a1.75 1.75 0 0 0 0-3.5zm0 5.5V13h3.2a1.75 1.75 0 0 0 0-3.5z"/></svg></button> @@ -41,7 +44,7 @@ <div class="ed-preview" id="ed-{{.name}}-preview" hidden></div> <iframe class="ed-preview-frame" id="ed-{{.name}}-frame" sandbox title="{{t "Preview"}}" hidden></iframe> <div class="editor-tools"> - <label class="upload">{{t "Insert file"}} <input type="file" name="inline_file" id="ed-{{.name}}-file"></label> + <label class="upload">{{t "Insert file"}} <input type="file" name="{{.name}}_file" id="ed-{{.name}}-file"></label> <span class="upload-status" id="ed-{{.name}}-status"></span> <span class="muted small">{{t "…or paste / drop a file into the text. Images are shown, other files linked."}}</span> <noscript><span class="muted small">{{t "The file is added at the end of the text when you save."}}</span></noscript> @@ -52,7 +55,7 @@ (function () { var id = "ed-{{.name}}", ta = document.getElementById(id), inp = document.getElementById(id + "-file"), st = document.getElementById(id + "-status"), wrap = document.getElementById(id + "-wrap"), bar = document.getElementById(id + "-bar"), lk = document.getElementById(id + "-link"), pv = document.getElementById(id + "-preview"), - frame = document.getElementById(id + "-frame"), fmt = document.getElementById(id + "-format"); + frame = document.getElementById(id + "-frame"), fmt = document.getElementById({{if .formatof}}"ed-{{.formatof}}-format"{{else}}id + "-format"{{end}}); if (!window.fetch || !window.FormData || !ta || !inp) return; // old browser: plain textarea, the file is uploaded on save bar.hidden = false; diff --git a/internal/web/templates/partials/imagelib.html b/internal/web/templates/partials/imagelib.html new file mode 100644 index 0000000..bc94f2b --- /dev/null +++ b/internal/web/templates/partials/imagelib.html @@ -0,0 +1,72 @@ +{{/* The image library panel behind every imagepick's "Choose from library…": + one per form, fetched page by page from the Files page's JSON. Its script + is self-contained: a choice goes into the imagepick's select and fires a + change event there, so the form sees it like any other edit. Without + fetch the buttons hide and the select stands in. .sub is the blog, + .count how many images the library has. */}} +{{define "imagelib"}}<div class="picker" id="picker" data-sub="{{.sub}}" hidden> + <div class="picker-box"> + <div class="picker-head"> + <input type="search" placeholder="{{t "Filter by name…"}}" aria-label="{{t "Filter by name…"}}"> + <span class="muted small pcount">{{tf "%d images" .count}}</span> + <button type="button" class="secondary small" data-pick="close">{{t "Close"}}</button> + </div> + <div class="picks"></div> + <p class="muted small pnone" hidden>{{t "No images match."}}</p> + <div class="pager"><button type="button" class="mini" data-pick="prev">← {{t "Newer"}}</button> <span class="pnum"></span> <button type="button" class="mini" data-pick="next">{{t "Older"}} →</button></div> + </div> +</div> +<script> +(function () { + var panel = document.getElementById("picker"), sub = panel.getAttribute("data-sub"), pick = null, page = 1, filter = "", timer; + function q(sel, el) { return (el || document).querySelector(sel); } + function qa(sel, el) { return Array.prototype.slice.call((el || document).querySelectorAll(sel)); } + if (!window.fetch || !panel.closest) return; // the select stands in + qa(".imagepick").forEach(function (ip) { ip.className += " js"; }); + document.addEventListener("click", function (e) { + var b = e.target.closest("[data-pick]"), ip = b && b.closest(".imagepick"); + if (!ip) return; + if (b.getAttribute("data-pick") === "open") { pick = ip; panel.hidden = false; q("input", panel).value = filter = ""; load(1); q("input", panel).focus(); } + else if (b.getAttribute("data-pick") === "clear") { choose(ip, "none", ""); } + }); + panel.addEventListener("click", function (e) { + var b = e.target.closest("[data-pick],.pick"); + if (!b) return; + if (b.className.indexOf("pick") === 0) { choose(pick, b.getAttribute("data-id"), b.getAttribute("data-name")); panel.hidden = true; return; } + switch (b.getAttribute("data-pick")) { + case "close": panel.hidden = true; break; + case "prev": load(page - 1); break; + case "next": load(page + 1); break; + } + }); + panel.addEventListener("keydown", function (e) { if (e.key === "Escape") panel.hidden = true; }); + q("input", panel).addEventListener("input", function () { clearTimeout(timer); timer = setTimeout(function () { filter = q("input", panel).value.trim(); load(1); }, 250); }); + function load(n) { + var url = "/b/" + sub + "/files?kind=image&p=" + n + (filter ? "&q=" + encodeURIComponent(filter) : ""); + fetch(url, { headers: { Accept: "application/json" }, credentials: "same-origin" }).then(function (r) { return r.json(); }).then(function (d) { + page = d.page; + var grid = q(".picks", panel); grid.innerHTML = ""; + (d.files || []).forEach(function (f) { + var b = document.createElement("button"), img = document.createElement("img"), s = document.createElement("span"); + b.type = "button"; b.className = "pick"; b.setAttribute("data-id", f.id); b.setAttribute("data-name", f.filename); + img.loading = "lazy"; img.src = "/b/" + sub + "/media/" + f.id; img.alt = ""; + s.className = "name"; s.textContent = f.filename; + b.appendChild(img); b.appendChild(s); grid.appendChild(b); + }); + q(".pnone", panel).hidden = !!(d.files && d.files.length); + q(".pnum", panel).textContent = d.last > 1 ? d.page + " / " + d.last : ""; + q("[data-pick=prev]", panel).disabled = d.page <= 1; q("[data-pick=next]", panel).disabled = d.page >= d.last; + }); + } + function choose(ip, id, name) { // the select stays the value the form sends + var sel = q("select", ip), pv = q(".preview", ip), found = false; + for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value === id) found = true; + if (!found) { var o = document.createElement("option"); o.value = id; o.textContent = name; sel.appendChild(o); } + sel.value = id; + pv.innerHTML = id === "none" ? '<span class="swatch">×</span>' : ""; + if (id !== "none") { var img = document.createElement("img"); img.src = "/b/" + sub + "/media/" + id; img.alt = ""; pv.appendChild(img); } + var ev; try { ev = new Event("change", { bubbles: true }); } catch (_) { ev = document.createEvent("Event"); ev.initEvent("change", true, false); } + sel.dispatchEvent(ev); + } +})(); +</script>{{end}} diff --git a/internal/web/templates/partials/imagepick.html b/internal/web/templates/partials/imagepick.html index 47c2146..ed9368c 100644 --- a/internal/web/templates/partials/imagepick.html +++ b/internal/web/templates/partials/imagepick.html @@ -1,19 +1,20 @@ -{{/* One image field of the design form. The select lists the recent images - (plus the chosen one) so it works without the script; with it the select is - hidden and "Choose from library…" opens the shared panel at the end of - design.html, which fetches the library page by page. */}} +{{/* One image field of a form (the design page, a post's featured image). + The select lists the recent images (plus the chosen one) so it works + without the script; with it the select is hidden and "Choose from + library…" opens the form's imagelib panel, which fetches the library + page by page and marks the pickers with .js. */}} {{define "imagepick"}}{{$cur := .current}}<div class="imagepick" data-name="{{.name}}"> <label>{{.label}}</label> {{if .hint}}<p class="muted small">{{.hint}}</p>{{end}} <div class="pickrow"> <span class="preview">{{if $cur}}<img src="/b/{{.sub}}/media/{{$cur}}" alt="">{{else}}<span class="swatch">×</span>{{end}}</span> <span class="pickctl"> - <select name="{{.name}}" class="nojs"> + <select name="{{.name}}" class="ip-nojs"> <option value="none"{{if not $cur}} selected{{end}}>{{t "None"}}</option> {{range .images}}<option value="{{.ID}}"{{if eq .ID.String $cur}} selected{{end}}>{{.Filename}}</option>{{end}} {{if and $cur (not (index .names $cur))}}<option value="{{$cur}}" selected>{{t "Current image"}}</option>{{end}} </select> - <span class="jsonly"><button type="button" class="secondary small" data-pick="open">{{t "Choose from library…"}}</button> <button type="button" class="secondary small" data-pick="clear">{{t "None"}}</button></span> + <span class="ip-jsonly"><button type="button" class="secondary small" data-pick="open">{{t "Choose from library…"}}</button> <button type="button" class="secondary small" data-pick="clear">{{t "None"}}</button></span> <label class="upload">{{t "…or upload a new one"}}<br><input type="file" name="{{.name}}_file" accept="{{if .accept}}{{.accept}}{{else}}image/*{{end}}"></label> </span> </div> diff --git a/internal/web/templates/partials/mdhelp.html b/internal/web/templates/partials/mdhelp.html index 5489510..cbf9b02 100644 --- a/internal/web/templates/partials/mdhelp.html +++ b/internal/web/templates/partials/mdhelp.html @@ -5,5 +5,5 @@ <tr><td><code>[{{t "link text"}}](https://example.org)</code></td><td><code></code>, <code>[{{t "file name"}}](/media/…)</code> — <em>{{t "Insert file"}}</em> {{t "writes this for you"}}</td></tr> <tr><td><code>- {{t "list item"}}</code> / <code>1. {{t "numbered"}}</code></td><td><code>> {{t "quote"}}</code>, <code>`{{t "code"}}`</code>, <code>---</code> {{t "for a line"}}</td></tr> </table> - <p class="muted small">{{t "Blank line = new paragraph. Press Enter once for a line break."}}</p> + <p class="muted small">{{t "Blank line = new paragraph. Press Enter once for a line break."}} {{t "A line with"}} <code><!--more--></code> {{t "marks where the summary ends on listings that show one."}}</p> </details>{{end}} diff --git a/internal/web/templates/partials/postlist.html b/internal/web/templates/partials/postlist.html index 01e1037..b1e43c6 100644 --- a/internal/web/templates/partials/postlist.html +++ b/internal/web/templates/partials/postlist.html @@ -3,11 +3,13 @@ own path, which ?p=N is appended to). */}} {{define "postlist"}} {{range .Data.posts}} -<article class="post"> +<article class="post{{if .Image}} has-thumb{{end}}"> + {{if .Image}}<a class="post-thumb" href="/{{.PageSlug}}/{{.Slug}}"><img src="/media/{{.Image}}" alt=""></a>{{end}} <h2 class="post-title"><a href="/{{.PageSlug}}/{{.Slug}}">{{.Title}}</a></h2> {{if $.Data.theme.ShowDates}}<p class="post-date">{{postdate $.Data.theme.DateFormat .CreatedAt}}</p>{{end}} {{template "posttags" .Tags}} - <div class="post-body">{{html .BodyHTML}}</div> + <div class="post-body">{{html .Excerpt}}</div> + {{if .HasMore}}<p class="read-more"><a href="/{{.PageSlug}}/{{.Slug}}">{{t "Read more"}} →</a></p>{{end}} </article> {{end}} {{if gt .Data.lastPage 1}} |
