SEO: custom share template (canonical, per-page OG, JSON-LD, robots), robots.txt, OG banner; generator sets shareOpenGraphURL; ignis module STEP example
This commit is contained in:
@@ -91,6 +91,14 @@ CSS file (Gruvbox, light + dark, theme toggle without reload).
|
||||
`_thumb` child-note convention for small thumbnails.
|
||||
- Built-in share footer navigation and book children list are hidden
|
||||
(`#content-footer .navigation`, `#index`).
|
||||
- **SEO**: a custom share template (`templates/share/page.ejs`, note `share template`,
|
||||
`application/x-ejs`, wired via `~shareTemplate` inheritable on the blog root, with
|
||||
`tree_item`/`prev_next`/`toc_item` partials as children) adds per-page canonical,
|
||||
`og:url`/`og:image`/`og:type`, Twitter cards, JSON-LD (WebSite/BlogPosting/CollectionPage)
|
||||
and `index,follow`. The generator's `ensureSeoMeta()` sets `shareDescription` +
|
||||
`shareOpenGraphURL` on every published note and category. `robots.txt`
|
||||
(`shareAlias=robots.txt`, `shareRaw`) points at the sitemap; `~shareOpenGraphImage`
|
||||
on the blog root sets the default OG image.
|
||||
|
||||
## Limitations / gotchas
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ no external dependencies (except `highlight.js`, vendored in `lib/`).
|
||||
- **Digital Garden**: `state` labels (Seed → Evergreen, Draft, Review) rendered as badges and
|
||||
colour-coded growth tables.
|
||||
- **Search** (client-side over a generated index), **RSS feed** and **sitemap.xml**.
|
||||
- **SEO**: per-page canonical + Open Graph (`og:url`, `og:image`, article/website type),
|
||||
Twitter cards, JSON-LD structured data (WebSite / BlogPosting / CollectionPage),
|
||||
`robots.txt` and `index,follow` meta. Provided by a custom share template
|
||||
(`templates/share/page.ejs`).
|
||||
- **Widget rendering** for Mermaid, Excalidraw (canvas) and MindElixir (mind map) notes, with a
|
||||
collapsible, highlighted "Source" block and copy button.
|
||||
- **3D models**: `.step` / `.stp` / `.stl` files (as file notes or inline attachments) render
|
||||
@@ -63,6 +67,8 @@ no external dependencies (except `highlight.js`, vendored in `lib/`).
|
||||
| `lib/hljs_line_numbers.js` | highlight.js line-numbers plugin | code |
|
||||
| `scripts/*.js` | frontend share snippets (content is pasted **verbatim** into a code note, including the `<script>`/`<style>` wrapper) | code |
|
||||
| `templates/blog_article.html` | article template with promoted labels | text |
|
||||
| `templates/share/*.ejs` | custom share template + partials (canonical, OG, JSON-LD, robots) | code, `application/x-ejs` |
|
||||
| `assets/og-banner.png` | default Open Graph image (1200×630, replace with your own) | file |
|
||||
| `docs/embed-3d-model.md` | how to embed STEP/STL in a note | – |
|
||||
| `examples/*` | sample category logo + menu icon script (ignis8) | code / SVG |
|
||||
| `generated/*` | reference output, **regenerated automatically** by `blog_generator.js` | code / xml |
|
||||
@@ -139,6 +145,10 @@ Under **`.blog`** create one code note per file, using the file name as the note
|
||||
| `sitemap.xml` | `generated/sitemap.xml` | code (regenerated) |
|
||||
| `state_map` | `generated/state_map.js` | `text/html` (regenerated) |
|
||||
| `blog_data` | `generated/blog_data.js` | `text/html` (regenerated) |
|
||||
| `robots.txt` | `generated/robots.txt` | code (static) |
|
||||
| `share template` | `templates/share/page.ejs` | `application/x-ejs` |
|
||||
| `tree_item`, `prev_next`, `toc_item` (children of `share template`) | `templates/share/tree_item.ejs` etc. | `application/x-ejs` |
|
||||
| `og-banner` | `assets/og-banner.png` | image note |
|
||||
|
||||
Also create, under the `Blog` book: `blog_generator` (from `blog_generator.js`,
|
||||
mime `application/javascript;env=backend`) and `blog_publish_manager` (from
|
||||
@@ -159,6 +169,8 @@ mime `application/javascript;env=backend`) and `blog_publish_manager` (from
|
||||
| `iconClass` | `bx bx-edit-alt` | – |
|
||||
| `~shareCss` | → `blog_share_theme.css` | yes |
|
||||
| `~shareFavicon` | → `favicon` | yes |
|
||||
| `~shareTemplate` | → `share template` | yes |
|
||||
| `~shareOpenGraphImage` | → `og-banner` | – |
|
||||
| `~shareHtml` | → `table column hover`, `blog card click`, `garden row click`, `code note highlight`, `code copy & lines`, `state_map`, `title state badge`, `bmc button`, `logo share alias`, `blog_data`, `prev next`, `reading time`, `search header`, `ignis8 menu icon` (optional), `note fallback`, `menu date sort`, `3d model viewer` | yes |
|
||||
| `~shareHtml` | → `hide home title` | **no** (index only) |
|
||||
| `~runOnNoteContentChange`, `~runOnAttributeChange`, `~runOnNoteCreation`, `~runOnNoteTitleChange`, `~runOnAttributeCreation`, `~runOnBranchDeletion`, `~runOnChildNoteCreation` | → `blog_generator` | yes |
|
||||
@@ -176,6 +188,7 @@ mime `application/javascript;env=backend`) and `blog_publish_manager` (from
|
||||
| `highlight.js` | `shareHiddenFromTree` |
|
||||
| `feed.xml` | `shareAlias=feed`, `shareRaw`, `shareHiddenFromTree` |
|
||||
| `sitemap.xml` | `shareAlias=sitemap.xml`, `shareRaw`, `shareHiddenFromTree` |
|
||||
| `robots.txt` | `shareAlias=robots.txt`, `shareRaw`, `shareHiddenFromTree` |
|
||||
| `.blog` (book) | `shareHiddenFromTree`, `viewType=list` |
|
||||
|
||||
**On the Digital Garden category note** (and any other garden category), add:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
+12
-5
@@ -356,19 +356,26 @@ function run() {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureShareDescription() {
|
||||
function ensureSeoMeta() {
|
||||
const seen = new Set();
|
||||
(function walk(note, depth) {
|
||||
if (depth > 20) return;
|
||||
for (const child of note.getChildNotes()) {
|
||||
if (seen.has(child.noteId)) continue;
|
||||
seen.add(child.noteId);
|
||||
if (child.getLabelValue("publish") === "true") {
|
||||
const desc = child.getLabelValue("summary") || SITE_DESCRIPTION;
|
||||
const published = child.getLabelValue("publish") === "true";
|
||||
const isCat = isCategoryNote(child);
|
||||
if (published || isCat) {
|
||||
const desc = child.getLabelValue("summary") || (isCat ? "Articles in " + child.title : SITE_DESCRIPTION);
|
||||
if (child.getLabelValue("shareDescription") !== desc) {
|
||||
child.setLabel("shareDescription", desc);
|
||||
child.save();
|
||||
api.log("blog_generator: set shareDescription on " + child.title);
|
||||
}
|
||||
const url = SHARE_BASE + "/" + encodeURIComponent(child.getLabelValue("shareAlias") || child.noteId);
|
||||
if (child.getLabelValue("shareOpenGraphURL") !== url) {
|
||||
child.setLabel("shareOpenGraphURL", url);
|
||||
child.save();
|
||||
api.log("blog_generator: set shareOpenGraphURL on " + child.title);
|
||||
}
|
||||
}
|
||||
walk(child, depth + 1);
|
||||
@@ -598,7 +605,7 @@ function run() {
|
||||
buildCategories();
|
||||
buildStateMap();
|
||||
buildSitemap();
|
||||
ensureShareDescription();
|
||||
ensureSeoMeta();
|
||||
buildBlogData();
|
||||
removeEmptyCategories();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://YOUR-INSTANCE.example.com/sitemap.xml
|
||||
@@ -0,0 +1,266 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%
|
||||
const hasTree = subRoot.note.hasVisibleChildren();
|
||||
|
||||
// Collect HTML snippets by location
|
||||
const htmlSnippetsByLocation = {};
|
||||
for (const htmlRelation of note.getRelations("shareHtml")) {
|
||||
const htmlNote = htmlRelation.targetNote;
|
||||
if (htmlNote) {
|
||||
let location = htmlNote.getLabelValue("shareHtmlLocation") || "content:end";
|
||||
if (!location.includes(":")) {
|
||||
location = location + ":end";
|
||||
}
|
||||
if (!htmlSnippetsByLocation[location]) {
|
||||
htmlSnippetsByLocation[location] = [];
|
||||
}
|
||||
htmlSnippetsByLocation[location].push(htmlNote.getContent());
|
||||
}
|
||||
}
|
||||
const renderSnippets = (location) => {
|
||||
const snippets = htmlSnippetsByLocation[location];
|
||||
return snippets ? snippets.join("\n") : "";
|
||||
};
|
||||
|
||||
// ---- SEO: per-page URL / canonical / OG image / JSON-LD ----
|
||||
const rootUrl = subRoot.note.getLabelValue("shareOpenGraphURL") || "";
|
||||
const perNoteUrl = note.getLabelValue("shareOpenGraphURL");
|
||||
const pageUrl = perNoteUrl
|
||||
|| (rootUrl && note.noteId !== subRoot.note.noteId ? rootUrl + (note.shareId || note.noteId) : rootUrl);
|
||||
|
||||
let ogImageRel = subRoot.note.getLabelValue("shareOpenGraphImage");
|
||||
if (subRoot.note.hasRelation("shareOpenGraphImage")) {
|
||||
ogImageRel = `api/images/${subRoot.note.getRelation("shareOpenGraphImage").value}/image.png`;
|
||||
}
|
||||
const ogImage = ogImageRel ? rootUrl + ogImageRel : "";
|
||||
|
||||
const jsonLd = [];
|
||||
if (note.noteId === subRoot.note.noteId) {
|
||||
jsonLd.push({ "@context": "https://schema.org", "@type": "WebSite", name: subRoot.note.title, url: pageUrl, description: note.getLabelValue("shareDescription") || "" });
|
||||
} else if (note.getLabelValue("publish") === "true") {
|
||||
jsonLd.push({ "@context": "https://schema.org", "@type": "BlogPosting", headline: note.title, url: pageUrl, datePublished: (note.getLabelValue("date") || note.dateCreated).slice(0, 10), dateModified: (note.utcDateModified || "").slice(0, 10), description: note.getLabelValue("shareDescription") || "", mainEntityOfPage: pageUrl, author: { "@type": "Person", name: subRoot.note.title } });
|
||||
} else {
|
||||
jsonLd.push({ "@context": "https://schema.org", "@type": "CollectionPage", name: note.title, url: pageUrl, description: note.getLabelValue("shareDescription") || "" });
|
||||
}
|
||||
%>
|
||||
<%- renderSnippets("head:start") %>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="shortcut icon" href="<%= faviconUrl %>">
|
||||
<% for (const url of cssToLoad) { %>
|
||||
<link href="<%= url %>" rel="stylesheet">
|
||||
<% } %>
|
||||
<% for (const url of jsToLoad) { %>
|
||||
<script type="module" src="<%= url %>"></script>
|
||||
<% } %>
|
||||
<% if (note.hasLabel("shareDisallowRobotIndexing")) { %>
|
||||
<meta name="robots" content="noindex,follow" />
|
||||
<% } else { %>
|
||||
<meta name="robots" content="index,follow" />
|
||||
<% } %>
|
||||
|
||||
<%
|
||||
const pageTitle = `${note.title}${note.noteId !== subRoot.note.noteId ? ` - ${subRoot.note.title}` : ""}`;
|
||||
|
||||
const openGraphColor = subRoot.note.getLabelValue("shareOpenGraphColor");
|
||||
const openGraphDomain = subRoot.note.getLabelValue("shareOpenGraphDomain");
|
||||
%>
|
||||
<title><%= pageTitle %></title>
|
||||
<% if (pageUrl) { %><link rel="canonical" href="<%= pageUrl %>"><% } %>
|
||||
<script>
|
||||
// Load dark/light theme as soon as possible to avoid color flashes.
|
||||
let theme = localStorage.getItem("theme");
|
||||
if (!theme) {
|
||||
theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light";
|
||||
}
|
||||
document.documentElement.classList.add(`theme-${theme}`);
|
||||
|
||||
window.glob = {
|
||||
isStatic: <%= !!isStatic %>,
|
||||
theme
|
||||
};
|
||||
|
||||
(function() {
|
||||
const leftCollapsed = localStorage.getItem("left-pane-collapsed") === "true";
|
||||
const tocCollapsed = localStorage.getItem("toc-pane-collapsed") === "true";
|
||||
if (leftCollapsed) document.documentElement.classList.add("left-pane-collapsed");
|
||||
if (tocCollapsed) document.documentElement.classList.add("toc-pane-collapsed");
|
||||
})();
|
||||
</script>
|
||||
<!-- HTML Meta Tags -->
|
||||
<meta name="description" content="<%= note.getLabelValue("shareDescription") %>">
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:url" content="<%= pageUrl %>">
|
||||
<meta property="og:type" content="<%= note.getLabelValue("publish") === "true" ? "article" : "website" %>">
|
||||
<meta property="og:site_name" content="<%= subRoot.note.title %>">
|
||||
<meta property="og:title" content="<%= pageTitle %>">
|
||||
<meta property="og:description" content="<%= note.getLabelValue("shareDescription") %>">
|
||||
<meta property="og:image" content="<%= ogImage %>">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="<%= openGraphDomain %>">
|
||||
<meta property="twitter:url" content="<%= pageUrl %>">
|
||||
<meta name="twitter:title" content="<%= pageTitle %>">
|
||||
<meta name="twitter:description" content="<%= note.getLabelValue("shareDescription") %>">
|
||||
<meta name="twitter:image" content="<%= ogImage %>">
|
||||
<meta name="theme-color" content="<%= openGraphColor %>">
|
||||
<script type="application/ld+json"><%= JSON.stringify(jsonLd) %></script>
|
||||
<style id="trilium-icon-packs">
|
||||
<%- iconPackCss %>
|
||||
</style>
|
||||
<%- renderSnippets("head:end") %>
|
||||
</head>
|
||||
<%
|
||||
const logoWidth = subRoot.note.getLabelValue("shareLogoWidth") ?? 53;
|
||||
const logoHeight = subRoot.note.getLabelValue("shareLogoHeight") ?? 40;
|
||||
const mobileLogoHeight = logoHeight && logoWidth ? 32 / (logoWidth / logoHeight) : "";
|
||||
const shareRootLink = subRoot.note.hasLabel("shareRootLink") ? sanitizeUrl(subRoot.note.getLabelValue("shareRootLink") ?? "") : `./${subRoot.note.noteId}`;
|
||||
const headingRe = /(<h[1-6]>)(.+?)(<\/h[1-6]>)/g;
|
||||
const headingMatches = [...content.matchAll(headingRe)];
|
||||
const headingSlugs = utils.slugifyHeadings(headingMatches.map((m) => m[2]));
|
||||
let headingIndex = 0;
|
||||
content = content.replaceAll(headingRe, (...match) => {
|
||||
const slug = headingSlugs[headingIndex++];
|
||||
match[0] = match[0].replace(match[3], `<a id="${slug}" class="toc-anchor" name="${slug}" href="#${slug}">#</a>${match[3]}`);
|
||||
return match[0];
|
||||
});
|
||||
%>
|
||||
<body data-note-id="<%= note.noteId %>" class="type-<%= note.type %>" data-ancestor-note-id="<%= subRoot.note.noteId %>">
|
||||
<%- renderSnippets("body:start") %>
|
||||
<div id="header">
|
||||
<button aria-label="<%= t("share_theme.toggle-navigation") %>" id="left-pane-toggle-button" class="header-button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"></path></svg></button>
|
||||
<a href="<%= shareRootLink %>" id="header-logo">
|
||||
<img src="<%= logoUrl %>" width="32" height="<%= mobileLogoHeight %>" alt="<%= t("share_theme.logo-alt") %>" />
|
||||
<%= subRoot.note.title %>
|
||||
</a>
|
||||
<% if (headingMatches.length > 1) { %>
|
||||
<button aria-label="<%= t("share_theme.toggle-toc") %>" id="toc-pane-toggle-button" class="header-button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M3 9h14V7H3v2zm0 4h14v-2H3v2zm0 4h14v-2H3v2zm16 0h2v-2h-2v2zm0-10V7h2v2h-2zm0 6h2v-2h-2v2zm0 6h2v-2h-2v2z"></path></svg></button>
|
||||
<% } else { %>
|
||||
<div class="header-button-placeholder"></div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div id="split-pane">
|
||||
<div id="left-pane">
|
||||
<div id="navigation">
|
||||
<div id="site-header">
|
||||
<div class="theme-selection">
|
||||
<span id="sitetheme"><%= t("share_theme.site-theme") %></span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" aria-labelledby="sitetheme">
|
||||
<span class="slider"></span>
|
||||
<svg class="dark-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20.742 13.045a8.088 8.088 0 0 1-2.077.271c-2.135 0-4.14-.83-5.646-2.336a8.025 8.025 0 0 1-2.064-7.723A1 1 0 0 0 9.73 2.034a10.014 10.014 0 0 0-4.489 2.582c-3.898 3.898-3.898 10.243 0 14.143a9.937 9.937 0 0 0 7.072 2.93 9.93 9.93 0 0 0 7.07-2.929 10.007 10.007 0 0 0 2.583-4.491 1.001 1.001 0 0 0-1.224-1.224zm-2.772 4.301a7.947 7.947 0 0 1-5.656 2.343 7.953 7.953 0 0 1-5.658-2.344c-3.118-3.119-3.118-8.195 0-11.314a7.923 7.923 0 0 1 2.06-1.483 10.027 10.027 0 0 0 2.89 7.848 9.972 9.972 0 0 0 7.848 2.891 8.036 8.036 0 0 1-1.484 2.059z"></path></svg>
|
||||
<svg class="light-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z"></path></svg>
|
||||
</label>
|
||||
<script>
|
||||
const el = document.querySelector(".theme-selection input");
|
||||
el.checked = (glob.theme === "dark");
|
||||
</script>
|
||||
</div>
|
||||
<% if (hasTree) { %>
|
||||
<div class="search-item">
|
||||
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M10 18a7.952 7.952 0 0 0 4.897-1.688l4.396 4.396 1.414-1.414-4.396-4.396A7.952 7.952 0 0 0 18 10c0-4.411-3.589-8-8-8s-8 3.589-8 8 3.589 8 8 8zm0-14c3.309 0 6 2.691 6 6s-2.691 6-6 6-6-2.691-6-6 2.691-6 6-6z"></path></svg>
|
||||
<input type="text" class="search-input" placeholder="<%= t("share_theme.search_placeholder") %>">
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (hasTree) { %>
|
||||
<nav id="menu">
|
||||
<%- include("tree_item", {note: subRoot.note, activeNote: note, subRoot: subRoot, ancestors}) %>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<div id="right-pane">
|
||||
<div id="main">
|
||||
<div id="content" class="type-<%= note.type %><% if (note.type === "text" || (note.type === "code" && note.mime === "text/x-markdown")) { %> ck-content<% } %><% if (isEmpty) { %> no-content<% } %>">
|
||||
<%- renderSnippets("content:start") %>
|
||||
<h1 id="title"><%= note.title %></h1>
|
||||
<% if (isEmpty && (!note.hasVisibleChildren() && note.type !== "book")) { %>
|
||||
<p><%= t("share_page.no-content") %></p>
|
||||
<% } else { %>
|
||||
<%
|
||||
content = content.replace(/<img /g, `<img alt="${t("share_theme.image_alt")}" loading="lazy" `);
|
||||
%>
|
||||
<%- content %>
|
||||
<% } %>
|
||||
<%- renderSnippets("content:end") %>
|
||||
</div>
|
||||
|
||||
<% if (note.hasVisibleChildren() || note.type === "book") { %>
|
||||
<nav id="childLinks" class="<% if (isEmpty) { %>grid<% } else { %>list<% } %>">
|
||||
<% if (!isEmpty) { %>
|
||||
<span><%= t("share_theme.subpages") %></span>
|
||||
<% } %>
|
||||
|
||||
<ul>
|
||||
<%
|
||||
const action = note.type === "book" ? "getChildNotes" : "getVisibleChildNotes";
|
||||
for (const childNote of note[action]()) {
|
||||
const isExternalLink = childNote.hasLabel("shareExternal") || childNote.hasLabel("shareExternalLink");
|
||||
const rawHref = isExternalLink ? childNote.getLabelValue("shareExternal") ?? childNote.getLabelValue("shareExternalLink") : `./${childNote.shareId}`;
|
||||
const linkHref = isExternalLink ? sanitizeUrl(rawHref ?? "") : rawHref;
|
||||
%>
|
||||
<li>
|
||||
<a class="type-<%= childNote.type %>" href="<%= linkHref %>"<% if (isExternalLink) { %> target="_blank" rel="noopener noreferrer"<% } %>><%= childNote.title %></a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
<% } %>
|
||||
|
||||
<footer id="content-footer">
|
||||
<% if (!isEmpty && !isStatic) { %>
|
||||
<div class="updated">
|
||||
<% const lastUpdated = new Date(note.utcDateModified); %>
|
||||
<%- t("share_theme.last-updated", { date: `<time datetime="${lastUpdated.toISOString()}">${lastUpdated.toLocaleDateString("en-US")}</time>`}) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (hasTree) { %>
|
||||
<%- include("prev_next", { note: note, subRoot: subRoot }) %>
|
||||
<% } %>
|
||||
|
||||
<% if (showLoginInShareTheme && !isStatic) { %>
|
||||
<div class="login">
|
||||
<a href="../login" class="login-link"><%= t("share_theme.login") %></a>
|
||||
</div>
|
||||
<% } %>
|
||||
</footer>
|
||||
</div>
|
||||
<%
|
||||
if (headingMatches.length > 1) {
|
||||
const level = (m) => parseInt(m[1].replace(/[<h>]+/g, ""));
|
||||
|
||||
const makeEntry = (i) => ({level: level(headingMatches[i]), name: headingMatches[i][2], slug: headingSlugs[i], children: []});
|
||||
const toc = [ makeEntry(0) ];
|
||||
const last = (arr = toc) => arr[arr.length - 1];
|
||||
const getLevelArr = (lvl, arr = toc) => {
|
||||
if (arr[0].level === lvl) return arr;
|
||||
const top = last(arr);
|
||||
return top.children.length ? getLevelArr(lvl, top.children) : top.children;
|
||||
};
|
||||
|
||||
|
||||
for (let m = 1; m < headingMatches.length; m++) {
|
||||
const target = getLevelArr(level(headingMatches[m]));
|
||||
target.push(makeEntry(m));
|
||||
}
|
||||
%>
|
||||
<div id="toc-pane">
|
||||
<h3><%= t("share_theme.on-this-page") %></h3>
|
||||
<ul id="toc">
|
||||
<% for (const entry of toc) { %>
|
||||
<%- include("toc_item", {entry}) %>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<%- renderSnippets("body:end") %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,62 @@
|
||||
<%
|
||||
// TODO: code cleanup + putting this behind a toggle/attribute
|
||||
const previousNote = (() => {
|
||||
// If we are at the subRoot, there is no previous
|
||||
if (note.noteId === subRoot.note.noteId) return null;
|
||||
|
||||
const parent = note.getParentNotes()[0];
|
||||
const children = parent.getVisibleChildNotes();
|
||||
const index = children.findIndex(n => n.noteId === note.noteId);
|
||||
|
||||
// If we are the first child, previous goes up a level
|
||||
// this is already protected by the first if statement
|
||||
if (index === 0) return parent;
|
||||
|
||||
// We are not the first child at this level so previous
|
||||
// should go to the end of the previous tree
|
||||
let candidate = children[index - 1];
|
||||
while (candidate?.hasVisibleChildren()) {
|
||||
const visibleChildren = candidate.getVisibleChildNotes();
|
||||
|
||||
if (visibleChildren.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
candidate = visibleChildren[visibleChildren.length - 1];
|
||||
}
|
||||
|
||||
return candidate ?? null;
|
||||
})();
|
||||
|
||||
const nextNote = (() => {
|
||||
// If this currently active note has children, next
|
||||
// should be the first child
|
||||
if (note.hasVisibleChildren()) return note.getVisibleChildNotes()[0];
|
||||
|
||||
let parent = note.getParentNotes()[0];
|
||||
let children = parent.getVisibleChildNotes();
|
||||
let index = children.findIndex(n => n.noteId === note.noteId);
|
||||
|
||||
// If we are not the last of the current level, just go
|
||||
// to the next sibling note
|
||||
if (index !== children.length - 1) return children[index + 1];
|
||||
|
||||
// We are the last sibling, we need to find the next ancestral note
|
||||
while (index === children.length - 1) {
|
||||
// If we are already at subRoot level, no reason trying to go higher
|
||||
if (parent.noteId === subRoot.note.noteId) return null;
|
||||
|
||||
const originalParent = parent;
|
||||
parent = parent.getParentNotes()[0];
|
||||
children = parent.getVisibleChildNotes();
|
||||
index = children.findIndex(n => n.noteId === originalParent.noteId);
|
||||
}
|
||||
|
||||
return children[index + 1];
|
||||
})();
|
||||
%>
|
||||
|
||||
<div class="navigation">
|
||||
<% if (previousNote) { %><a class="previous" href="./<%= previousNote.shareId %>"><%= previousNote.title %></a><% } %>
|
||||
<% if (nextNote) { %><a class="next" href="./<%= nextNote.shareId %>"><%= nextNote.title %></a><% } %>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<%
|
||||
const strippedName = utils.stripTags(entry.name);
|
||||
const slug = entry.slug ?? utils.slugify(strippedName);
|
||||
%>
|
||||
|
||||
<li>
|
||||
<a href="#<%= slug %>">
|
||||
<span><%= strippedName %></span>
|
||||
</a>
|
||||
|
||||
<% if (entry.children.length) { %>
|
||||
<ul>
|
||||
<% for (const subentry of entry.children) { %>
|
||||
<%- include('toc_item', {entry: subentry}) %>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
</li>
|
||||
@@ -0,0 +1,34 @@
|
||||
<%
|
||||
const linkClass = `type-${note.type}` + (activeNote.noteId === note.noteId ? " active" : "");
|
||||
const isExternalLink = note.hasLabel("shareExternal");
|
||||
let linkHref;
|
||||
|
||||
if (isExternalLink) {
|
||||
linkHref = sanitizeUrl(note.getLabelValue("shareExternal") ?? "");
|
||||
} else if (note.shareId) {
|
||||
linkHref = `./${note.shareId}`;
|
||||
}
|
||||
%>
|
||||
|
||||
<% if (note.noteId !== subRoot.note.noteId) { %>
|
||||
<a class="<%= linkClass %>" href="<%= linkHref %>"<% if (isExternalLink) { %> target="_blank" rel="noopener noreferrer"<% } %>>
|
||||
<% if (note.hasVisibleChildren()) { %><button class="collapse-button" aria-label="<%= t("share_theme.expand") %>"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon right-triangle"><path d="M3 8L12 17L21 8"></path></svg></button><% } %>
|
||||
<span><i class="<%= note.getIcon(iconPackSupportedPrefixes) %>"></i> <%= note.title %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
|
||||
|
||||
<% if (note.hasVisibleChildren()) { %>
|
||||
<ul>
|
||||
<% note.getVisibleChildNotes().forEach(function (childNote) { %>
|
||||
<%
|
||||
const hasChildren = childNote.hasVisibleChildren();
|
||||
const isAncestorOfActive = ancestors.some(p => p === childNote.noteId);
|
||||
const expandedClass = childNote.noteId === activeNote.noteId || isAncestorOfActive ? " expanded" : "";
|
||||
%>
|
||||
<li class="<% if (hasChildren) { %>submenu-<% } %>item<%= expandedClass %>">
|
||||
<%- include('tree_item', {note: childNote, subRoot: subRoot}) %>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<% } %>
|
||||
Reference in New Issue
Block a user