@dmthepm/commune 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +468 -0
- package/bin/commune.mjs +29 -0
- package/lib/cli/check.d.ts +13 -0
- package/lib/cli/check.js +58 -0
- package/lib/cli/errors.d.ts +29 -0
- package/lib/cli/errors.js +41 -0
- package/lib/cli/gate.d.ts +34 -0
- package/lib/cli/gate.js +165 -0
- package/lib/cli/main.d.ts +20 -0
- package/lib/cli/main.js +175 -0
- package/lib/cli/query.d.ts +30 -0
- package/lib/cli/query.js +103 -0
- package/lib/cli/related.d.ts +17 -0
- package/lib/cli/related.js +177 -0
- package/lib/cli/render.d.ts +20 -0
- package/lib/cli/render.js +32 -0
- package/lib/cli/root.d.ts +11 -0
- package/lib/cli/root.js +28 -0
- package/lib/cli/usage.d.ts +3 -0
- package/lib/cli/usage.js +46 -0
- package/lib/cli/version.d.ts +24 -0
- package/lib/cli/version.js +29 -0
- package/lib/integration.d.ts +24 -0
- package/lib/integration.js +111 -0
- package/lib/lib/graph.d.ts +354 -0
- package/lib/lib/graph.js +774 -0
- package/lib/markdown.d.ts +30 -0
- package/lib/markdown.js +24 -0
- package/lib/rehype-external-links.d.ts +15 -0
- package/lib/rehype-external-links.js +46 -0
- package/lib/remark-wikilinks.d.ts +25 -0
- package/lib/remark-wikilinks.js +108 -0
- package/package.json +101 -0
- package/src/components/Backlinks.astro +17 -0
- package/src/components/BacklinksScript.astro +117 -0
- package/src/components/Footer.astro +35 -0
- package/src/components/Header.astro +250 -0
- package/src/components/HeaderStarScript.astro +380 -0
- package/src/components/HomeFooterCards.astro +155 -0
- package/src/components/MarkdownLink.astro +36 -0
- package/src/components/PlausibleScript.astro +13 -0
- package/src/components/RelatedNotes.astro +77 -0
- package/src/components/SearchModal.astro +213 -0
- package/src/components/StarredLinksScript.astro +92 -0
- package/src/components/panes.ts +61 -0
- package/src/styles/design-system.css +157 -0
- package/src/styles/notes.css +85 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markdown processor, assembled once.
|
|
3
|
+
*
|
|
4
|
+
* Astro 7 renders markdown with Sätteri and no longer installs the unified
|
|
5
|
+
* pipeline, so a consumer who wants `[[WikiLinks]]` has to build a processor
|
|
6
|
+
* and hand it to `markdown.processor`. That is three imports, an option shape
|
|
7
|
+
* and one ordering rule (the remark plugin turns wikilinks into links; the
|
|
8
|
+
* rehype plugin then decides which links are external) — all of it engine
|
|
9
|
+
* knowledge, none of it a decision a wiki's author should have to re-derive.
|
|
10
|
+
* This function is that assembly, so `astro.config.mjs` reads as one line.
|
|
11
|
+
*
|
|
12
|
+
* `site` is a parameter and not a constant because the engine has no host of
|
|
13
|
+
* its own: what counts as an external link is decided against the consumer's
|
|
14
|
+
* own origin, which lives once in their `defineConfig({ site })`.
|
|
15
|
+
*/
|
|
16
|
+
import type { GraphOptions } from './lib/graph.ts';
|
|
17
|
+
export interface CommuneMarkdownOptions extends GraphOptions {
|
|
18
|
+
/** The site's own origin, exactly as `astro.config.mjs` declares `site`. */
|
|
19
|
+
site?: string | URL;
|
|
20
|
+
/**
|
|
21
|
+
* The project root, for resolving `[[WikiLinks]]` against its content tree.
|
|
22
|
+
*
|
|
23
|
+
* Inherited from `GraphOptions`, and almost never worth passing: it
|
|
24
|
+
* defaults to `process.cwd()`, which is also the default Astro resolves
|
|
25
|
+
* `root` to. Pass it when the build runs from somewhere other than the
|
|
26
|
+
* project directory — the same case `astro build --root` exists for.
|
|
27
|
+
*/
|
|
28
|
+
root?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function communeMarkdown({ site, root }?: CommuneMarkdownOptions): import("@astrojs/markdown-remark").MarkdownProcessor<import("@astrojs/markdown-remark").UnifiedResolvedOptions>;
|
package/lib/markdown.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markdown processor, assembled once.
|
|
3
|
+
*
|
|
4
|
+
* Astro 7 renders markdown with Sätteri and no longer installs the unified
|
|
5
|
+
* pipeline, so a consumer who wants `[[WikiLinks]]` has to build a processor
|
|
6
|
+
* and hand it to `markdown.processor`. That is three imports, an option shape
|
|
7
|
+
* and one ordering rule (the remark plugin turns wikilinks into links; the
|
|
8
|
+
* rehype plugin then decides which links are external) — all of it engine
|
|
9
|
+
* knowledge, none of it a decision a wiki's author should have to re-derive.
|
|
10
|
+
* This function is that assembly, so `astro.config.mjs` reads as one line.
|
|
11
|
+
*
|
|
12
|
+
* `site` is a parameter and not a constant because the engine has no host of
|
|
13
|
+
* its own: what counts as an external link is decided against the consumer's
|
|
14
|
+
* own origin, which lives once in their `defineConfig({ site })`.
|
|
15
|
+
*/
|
|
16
|
+
import { unified } from '@astrojs/markdown-remark';
|
|
17
|
+
import remarkWikiLinks from "./remark-wikilinks.js";
|
|
18
|
+
import rehypeExternalLinks from "./rehype-external-links.js";
|
|
19
|
+
export function communeMarkdown({ site, root } = {}) {
|
|
20
|
+
return unified({
|
|
21
|
+
remarkPlugins: [[remarkWikiLinks, { root }]],
|
|
22
|
+
rehypePlugins: [[rehypeExternalLinks, { site }]],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehype plugin to open every external link in a new tab.
|
|
3
|
+
*
|
|
4
|
+
* The design system marks `a[target="_blank"]` with an arrow, so setting the
|
|
5
|
+
* attribute here is what makes that marker automatic for plain markdown links.
|
|
6
|
+
*/
|
|
7
|
+
import type { Root } from 'hast';
|
|
8
|
+
export interface ExternalLinksOptions {
|
|
9
|
+
/** The site's own origin, exactly as `astro.config.mjs` declares `site`. */
|
|
10
|
+
site?: string | URL;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Rehype plugin function
|
|
14
|
+
*/
|
|
15
|
+
export default function rehypeExternalLinks({ site }?: ExternalLinksOptions): (tree: Root) => void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rehype plugin to open every external link in a new tab.
|
|
3
|
+
*
|
|
4
|
+
* The design system marks `a[target="_blank"]` with an arrow, so setting the
|
|
5
|
+
* attribute here is what makes that marker automatic for plain markdown links.
|
|
6
|
+
*/
|
|
7
|
+
import { visit } from 'unist-util-visit';
|
|
8
|
+
/**
|
|
9
|
+
* Rehype plugin function
|
|
10
|
+
*/
|
|
11
|
+
export default function rehypeExternalLinks({ site } = {}) {
|
|
12
|
+
// A link is internal because it points at the site's own host, and the
|
|
13
|
+
// engine has no host of its own: it comes from the Astro `site` config.
|
|
14
|
+
// Without one, nothing can be recognised as internal by hostname — only
|
|
15
|
+
// relative links stay untouched.
|
|
16
|
+
const siteHost = site ? new URL(site).hostname : null;
|
|
17
|
+
return function transformer(tree) {
|
|
18
|
+
visit(tree, 'element', (node) => {
|
|
19
|
+
if (node.tagName !== 'a')
|
|
20
|
+
return;
|
|
21
|
+
// A link that already declares `target` was written by hand — raw
|
|
22
|
+
// HTML in a note, or a component — and owns its own `rel`.
|
|
23
|
+
// Overwriting it would drop attributes its author chose.
|
|
24
|
+
if (node.properties?.target !== undefined)
|
|
25
|
+
return;
|
|
26
|
+
const href = node.properties?.href;
|
|
27
|
+
if (typeof href !== 'string')
|
|
28
|
+
return;
|
|
29
|
+
// Wikilinks and internal links are relative, so they have no
|
|
30
|
+
// protocol, never match here, and are left alone.
|
|
31
|
+
if (!href.startsWith('http://') && !href.startsWith('https://'))
|
|
32
|
+
return;
|
|
33
|
+
let hostname;
|
|
34
|
+
try {
|
|
35
|
+
hostname = new URL(href).hostname;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (hostname === siteHost)
|
|
41
|
+
return;
|
|
42
|
+
node.properties.target = '_blank';
|
|
43
|
+
node.properties.rel = ['noopener', 'noreferrer'];
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remark plugin to transform WikiLinks [[Note Title]] into proper markdown links.
|
|
3
|
+
*
|
|
4
|
+
* Transforms:
|
|
5
|
+
* [[Atomic Notes]] → [Atomic Notes](/notes/atomic-notes/)
|
|
6
|
+
* [[Note Title|Display Text]] → [Display Text](/notes/note-title/)
|
|
7
|
+
*
|
|
8
|
+
* This runs at build time during markdown compilation, before HTML generation.
|
|
9
|
+
* Which content exists, and the URL each piece lives at, is decided by the
|
|
10
|
+
* graph core in `src/lib/graph.ts` — not here.
|
|
11
|
+
*/
|
|
12
|
+
import type { Root } from 'mdast';
|
|
13
|
+
import { type GraphOptions } from './lib/graph.ts';
|
|
14
|
+
export type WikiLinksOptions = GraphOptions;
|
|
15
|
+
/**
|
|
16
|
+
* Remark plugin function.
|
|
17
|
+
*
|
|
18
|
+
* Takes the project root rather than assuming one, because the plugin resolves
|
|
19
|
+
* links against the content tree of whichever project is being built — which,
|
|
20
|
+
* once this is a package in somebody else's `node_modules`, is not the
|
|
21
|
+
* directory this file lives in. Omitting it falls back to `process.cwd()`,
|
|
22
|
+
* which is also Astro's own default for `root`, so a consumer who does not
|
|
23
|
+
* pass one gets the project they ran `astro build` in.
|
|
24
|
+
*/
|
|
25
|
+
export default function remarkWikiLinks({ root }?: WikiLinksOptions): (tree: Root) => Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remark plugin to transform WikiLinks [[Note Title]] into proper markdown links.
|
|
3
|
+
*
|
|
4
|
+
* Transforms:
|
|
5
|
+
* [[Atomic Notes]] → [Atomic Notes](/notes/atomic-notes/)
|
|
6
|
+
* [[Note Title|Display Text]] → [Display Text](/notes/note-title/)
|
|
7
|
+
*
|
|
8
|
+
* This runs at build time during markdown compilation, before HTML generation.
|
|
9
|
+
* Which content exists, and the URL each piece lives at, is decided by the
|
|
10
|
+
* graph core in `src/lib/graph.ts` — not here.
|
|
11
|
+
*/
|
|
12
|
+
import { visit } from 'unist-util-visit';
|
|
13
|
+
import { getLinkLookup } from "./lib/graph.js";
|
|
14
|
+
/**
|
|
15
|
+
* Remark plugin function.
|
|
16
|
+
*
|
|
17
|
+
* Takes the project root rather than assuming one, because the plugin resolves
|
|
18
|
+
* links against the content tree of whichever project is being built — which,
|
|
19
|
+
* once this is a package in somebody else's `node_modules`, is not the
|
|
20
|
+
* directory this file lives in. Omitting it falls back to `process.cwd()`,
|
|
21
|
+
* which is also Astro's own default for `root`, so a consumer who does not
|
|
22
|
+
* pass one gets the project they ran `astro build` in.
|
|
23
|
+
*/
|
|
24
|
+
export default function remarkWikiLinks({ root } = {}) {
|
|
25
|
+
return async function transformer(tree) {
|
|
26
|
+
const lookup = await getLinkLookup({ root });
|
|
27
|
+
visit(tree, 'text', (node, index, parent) => {
|
|
28
|
+
if (!parent || index === undefined)
|
|
29
|
+
return;
|
|
30
|
+
const text = node.value;
|
|
31
|
+
const wikiLinkRegex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
32
|
+
// Check if this text node contains WikiLinks
|
|
33
|
+
if (!wikiLinkRegex.test(text))
|
|
34
|
+
return;
|
|
35
|
+
// Reset regex
|
|
36
|
+
wikiLinkRegex.lastIndex = 0;
|
|
37
|
+
const newNodes = [];
|
|
38
|
+
let lastIndex = 0;
|
|
39
|
+
let match;
|
|
40
|
+
while ((match = wikiLinkRegex.exec(text)) !== null) {
|
|
41
|
+
const [fullMatch, linkText, displayText] = match;
|
|
42
|
+
const startIndex = match.index;
|
|
43
|
+
// Add text before the WikiLink
|
|
44
|
+
if (startIndex > lastIndex) {
|
|
45
|
+
newNodes.push({
|
|
46
|
+
type: 'text',
|
|
47
|
+
value: text.slice(lastIndex, startIndex),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// Resolve WikiLink to URL path
|
|
51
|
+
const trimmedLinkText = linkText.trim();
|
|
52
|
+
const lookupKey = trimmedLinkText.toLowerCase();
|
|
53
|
+
const resolved = lookup.get(lookupKey);
|
|
54
|
+
if (resolved) {
|
|
55
|
+
// Create a proper link node with correct URL for collection
|
|
56
|
+
// Add data-collection attribute to identify research links for styling
|
|
57
|
+
const linkNode = {
|
|
58
|
+
type: 'link',
|
|
59
|
+
url: resolved.urlPath,
|
|
60
|
+
children: [
|
|
61
|
+
{
|
|
62
|
+
type: 'text',
|
|
63
|
+
value: displayText?.trim() || trimmedLinkText,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
// Add data attribute for research links (will be used for new tab behavior)
|
|
68
|
+
if (resolved.collection === 'research') {
|
|
69
|
+
linkNode.data = {
|
|
70
|
+
hProperties: {
|
|
71
|
+
'data-collection': 'research',
|
|
72
|
+
'class': 'wikilink research-link'
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
linkNode.data = {
|
|
78
|
+
hProperties: {
|
|
79
|
+
'class': 'wikilink'
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
newNodes.push(linkNode);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Leave unresolved WikiLinks as plain text (without brackets)
|
|
87
|
+
// These are notes that don't exist yet in the public wiki
|
|
88
|
+
newNodes.push({
|
|
89
|
+
type: 'text',
|
|
90
|
+
value: displayText?.trim() || trimmedLinkText,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
lastIndex = startIndex + fullMatch.length;
|
|
94
|
+
}
|
|
95
|
+
// Add remaining text
|
|
96
|
+
if (lastIndex < text.length) {
|
|
97
|
+
newNodes.push({
|
|
98
|
+
type: 'text',
|
|
99
|
+
value: text.slice(lastIndex),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Replace the text node with our transformed nodes
|
|
103
|
+
if (newNodes.length > 0) {
|
|
104
|
+
parent.children.splice(index, 1, ...newNodes);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dmthepm/commune",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "An Astro wiki engine with WikiLinks, sliding panes, backlinks and static search, plus a commune CLI that queries the content graph and checks links",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"astro",
|
|
9
|
+
"astro-integration",
|
|
10
|
+
"withastro",
|
|
11
|
+
"wiki",
|
|
12
|
+
"wikilinks",
|
|
13
|
+
"backlinks"
|
|
14
|
+
],
|
|
15
|
+
"bin": {
|
|
16
|
+
"commune": "bin/commune.mjs"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
"./graph": {
|
|
20
|
+
"types": "./lib/lib/graph.d.ts",
|
|
21
|
+
"default": "./lib/lib/graph.js"
|
|
22
|
+
},
|
|
23
|
+
"./astro": {
|
|
24
|
+
"types": "./lib/integration.d.ts",
|
|
25
|
+
"default": "./lib/integration.js"
|
|
26
|
+
},
|
|
27
|
+
"./markdown": {
|
|
28
|
+
"types": "./lib/markdown.d.ts",
|
|
29
|
+
"default": "./lib/markdown.js"
|
|
30
|
+
},
|
|
31
|
+
"./remark": {
|
|
32
|
+
"types": "./lib/remark-wikilinks.d.ts",
|
|
33
|
+
"default": "./lib/remark-wikilinks.js"
|
|
34
|
+
},
|
|
35
|
+
"./rehype": {
|
|
36
|
+
"types": "./lib/rehype-external-links.d.ts",
|
|
37
|
+
"default": "./lib/rehype-external-links.js"
|
|
38
|
+
},
|
|
39
|
+
"./components/*": "./src/components/*",
|
|
40
|
+
"./styles/*": "./src/styles/*",
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"bin",
|
|
45
|
+
"lib",
|
|
46
|
+
"src/components",
|
|
47
|
+
"src/styles",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"README.md"
|
|
50
|
+
],
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.12.0"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/dmthepm/commune-wiki.git"
|
|
60
|
+
},
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/dmthepm/commune-wiki/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/dmthepm/commune-wiki#readme",
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build:lib": "tsc -p tsconfig.build.json",
|
|
67
|
+
"prepare": "pnpm build:lib",
|
|
68
|
+
"pretest": "pnpm build:lib",
|
|
69
|
+
"dev": "astro dev",
|
|
70
|
+
"start": "astro dev",
|
|
71
|
+
"build": "pnpm build:lib && astro build && node bin/commune.mjs gate",
|
|
72
|
+
"test": "node --test \"tests/**/*.test.mjs\"",
|
|
73
|
+
"test:consumer": "pnpm --dir tests/fixtures/consumer install && pnpm --dir tests/fixtures/consumer build",
|
|
74
|
+
"preview": "astro preview",
|
|
75
|
+
"astro": "astro"
|
|
76
|
+
},
|
|
77
|
+
"peerDependencies": {
|
|
78
|
+
"@astrojs/markdown-remark": "^7.3.0",
|
|
79
|
+
"astro": "^7.0.0"
|
|
80
|
+
},
|
|
81
|
+
"dependencies": {
|
|
82
|
+
"github-slugger": "^2.0.0",
|
|
83
|
+
"globby": "^14.0.0",
|
|
84
|
+
"gray-matter": "^4.0.3",
|
|
85
|
+
"unist-util-visit": "^5.0.0"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@astrojs/check": "^0.9.0",
|
|
89
|
+
"@astrojs/markdown-remark": "^7.3.0",
|
|
90
|
+
"@astrojs/sitemap": "^3.7.4",
|
|
91
|
+
"@tailwindcss/typography": "^0.5.10",
|
|
92
|
+
"@types/hast": "^3.0.5",
|
|
93
|
+
"@types/mdast": "^4.0.4",
|
|
94
|
+
"@types/node": "^22.20.1",
|
|
95
|
+
"astro": "^7.2.10",
|
|
96
|
+
"autoprefixer": "^10.4.16",
|
|
97
|
+
"puppeteer": "^24.25.0",
|
|
98
|
+
"tailwindcss": "^3.4.0",
|
|
99
|
+
"typescript": "^5.6.0"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
export interface Props { slug: string; title?: string; hideCount?: boolean; }
|
|
3
|
+
const { slug, hideCount = false } = Astro.props;
|
|
4
|
+
---
|
|
5
|
+
<aside class="backlinks" data-backlinks-for={slug} data-hide-count={hideCount ? 'true' : undefined}>
|
|
6
|
+
<h4>Links to this note</h4>
|
|
7
|
+
<ul class="backlinks-list"></ul>
|
|
8
|
+
</aside>
|
|
9
|
+
|
|
10
|
+
<style>
|
|
11
|
+
.backlinks{margin-top:3rem;padding-top:1.2rem;border-top:1px solid var(--c-border)}
|
|
12
|
+
.backlinks.hidden{display:none}
|
|
13
|
+
.backlinks h4{font-size:.85rem;color:var(--c-text-muted);margin:0 0 .8rem;font-weight:500}
|
|
14
|
+
.backlinks li{margin:.4rem 0;font-size:.9rem}
|
|
15
|
+
.backlinks a{opacity:.85}
|
|
16
|
+
.backlinks a:hover{opacity:1}
|
|
17
|
+
</style>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script is:inline>
|
|
2
|
+
// === BACKLINKS INITIALIZATION ===
|
|
3
|
+
// Handles dynamic backlinks loading for both static and dynamically loaded panes
|
|
4
|
+
|
|
5
|
+
(function() {
|
|
6
|
+
let backlinksData = null;
|
|
7
|
+
|
|
8
|
+
async function initializeBacklinks(scope = document) {
|
|
9
|
+
try {
|
|
10
|
+
// Fetch backlinks data if not already loaded
|
|
11
|
+
if (!backlinksData) {
|
|
12
|
+
const res = await fetch('/backlinks.json');
|
|
13
|
+
if (res.ok) {
|
|
14
|
+
backlinksData = await res.json();
|
|
15
|
+
} else {
|
|
16
|
+
console.warn('Failed to load backlinks.json');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Find all backlinks sections within the scope
|
|
22
|
+
const backlinksSections = scope.querySelectorAll('.backlinks');
|
|
23
|
+
|
|
24
|
+
backlinksSections.forEach(section => {
|
|
25
|
+
const slug = section.dataset.backlinksFor;
|
|
26
|
+
if (!slug) return;
|
|
27
|
+
|
|
28
|
+
const list = section.querySelector('.backlinks-list');
|
|
29
|
+
if (!list) return;
|
|
30
|
+
|
|
31
|
+
// Get backlinks for this slug
|
|
32
|
+
const here = slug.endsWith('/') ? slug : slug + '/';
|
|
33
|
+
const rec = backlinksData?.[here];
|
|
34
|
+
const items = rec?.inbound ?? [];
|
|
35
|
+
|
|
36
|
+
if (!items.length) {
|
|
37
|
+
// Hide the entire section if no backlinks
|
|
38
|
+
section.classList.add('hidden');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Show the section and populate links
|
|
43
|
+
section.classList.remove('hidden');
|
|
44
|
+
|
|
45
|
+
// Update heading with count (unless hideCount attribute is present)
|
|
46
|
+
const heading = section.querySelector('h4');
|
|
47
|
+
const hideCount = section.hasAttribute('data-hide-count');
|
|
48
|
+
if (heading && !hideCount) {
|
|
49
|
+
const count = items.length;
|
|
50
|
+
const linkText = count === 1 ? 'link' : 'links';
|
|
51
|
+
heading.textContent = `${count} ${linkText} to this note`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const titleFor = (u) => {
|
|
55
|
+
const note = backlinksData[u];
|
|
56
|
+
return note?.title || u.split('/').filter(Boolean).pop()?.replace(/-/g,' ');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Render links and then add stars as separate clickable elements
|
|
60
|
+
list.innerHTML = items.map(s => `<li><a href="${s}">${titleFor(s)}</a></li>`).join('');
|
|
61
|
+
|
|
62
|
+
// Add stars to starred notes
|
|
63
|
+
list.querySelectorAll('li').forEach((li, index) => {
|
|
64
|
+
const url = items[index];
|
|
65
|
+
const note = backlinksData[url];
|
|
66
|
+
|
|
67
|
+
if (note?.isStarred && !li.querySelector('.star-indicator-inline')) {
|
|
68
|
+
const link = li.querySelector('a');
|
|
69
|
+
if (link) {
|
|
70
|
+
const star = document.createElement('span');
|
|
71
|
+
star.className = 'star-indicator-inline';
|
|
72
|
+
star.textContent = ' ⭐';
|
|
73
|
+
star.setAttribute('aria-label', 'Top 5% most linked - click to learn more');
|
|
74
|
+
star.setAttribute('role', 'button');
|
|
75
|
+
star.setAttribute('tabindex', '0');
|
|
76
|
+
|
|
77
|
+
// Click handler to open modal
|
|
78
|
+
star.addEventListener('click', (e) => {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
if (typeof window.showStarModal === 'function') {
|
|
82
|
+
window.showStarModal();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Keyboard support
|
|
87
|
+
star.addEventListener('keydown', (e) => {
|
|
88
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
if (typeof window.showStarModal === 'function') {
|
|
92
|
+
window.showStarModal();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
link.insertAdjacentElement('afterend', star);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Error initializing backlinks:', error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Make it globally available for dynamic pane loading
|
|
108
|
+
window.initializeBacklinks = initializeBacklinks;
|
|
109
|
+
|
|
110
|
+
// Auto-initialize on DOM ready
|
|
111
|
+
if (document.readyState === 'loading') {
|
|
112
|
+
document.addEventListener('DOMContentLoaded', () => initializeBacklinks());
|
|
113
|
+
} else {
|
|
114
|
+
initializeBacklinks();
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
117
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Footer component with "Powered by Commune" link
|
|
3
|
+
---
|
|
4
|
+
<footer class="site-footer">
|
|
5
|
+
<p>Powered by <a href="/notes/what-is-commune/">Commune</a></p>
|
|
6
|
+
</footer>
|
|
7
|
+
|
|
8
|
+
<style>
|
|
9
|
+
.site-footer {
|
|
10
|
+
margin-top: 3rem;
|
|
11
|
+
padding-top: 1.5rem;
|
|
12
|
+
border-top: 1px solid var(--c-border);
|
|
13
|
+
text-align: center;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.site-footer p {
|
|
17
|
+
margin: 0;
|
|
18
|
+
font-size: 0.9rem;
|
|
19
|
+
color: var(--c-text-muted);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.site-footer a {
|
|
23
|
+
color: var(--c-accent);
|
|
24
|
+
text-decoration: none;
|
|
25
|
+
border-bottom: 1px solid var(--c-accent);
|
|
26
|
+
padding-bottom: 1px;
|
|
27
|
+
transition: all 0.15s ease;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.site-footer a:hover {
|
|
31
|
+
color: var(--c-accent-hover);
|
|
32
|
+
background: var(--c-accent-soft);
|
|
33
|
+
border-bottom-color: var(--c-accent-hover);
|
|
34
|
+
}
|
|
35
|
+
</style>
|