@because-dev/astro 0.0.0-stage → 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arvind Suresh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # @because-dev/astro
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Astro integration: one static page per Because idea, plus per-idea neighbourhood JSON.
4
+
5
+ See the [repository README](../../README.md) and [format spec](../../spec/format.md).
@@ -0,0 +1,34 @@
1
+ ---
2
+ /**
3
+ * Fallback layout used when the integration's `layout` option is not set.
4
+ * A custom layout should accept the same props and render <slot name="head" />
5
+ * inside <head> and the default <slot /> in the body.
6
+ */
7
+ interface Props {
8
+ title: string;
9
+ description?: string;
10
+ }
11
+ const { title } = Astro.props;
12
+ ---
13
+ <!doctype html>
14
+ <html lang="en">
15
+ <head>
16
+ <meta charset="utf-8" />
17
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
18
+ <title>{title}</title>
19
+ <slot name="head" />
20
+ <style is:global>
21
+ :root { color-scheme: light dark; --bc-muted: #6b6b6b; --bc-accent: #2f6f5e; }
22
+ body { font: 18px/1.6 system-ui, sans-serif; max-width: 42rem; margin: 0 auto; padding: 2rem 1rem; }
23
+ a { color: var(--bc-accent); }
24
+ .bc-trail ol { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: .4rem; font-size: .9rem; }
25
+ .bc-trail li + li::before { content: "›"; margin-right: .4rem; color: var(--bc-muted); }
26
+ .bc-question h2 { font-size: 1.1rem; margin-top: 2rem; }
27
+ .bc-answer h3 { font-size: 1rem; margin: .6rem 0 0; }
28
+ .bc-answer p, .bc-meta { margin: .2rem 0; color: var(--bc-muted); }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ <main><slot /></main>
33
+ </body>
34
+ </html>
@@ -0,0 +1,19 @@
1
+ ---
2
+ /** Meta tags shared by Because pages: description, canonical, Open Graph, JSON-LD. */
3
+ interface Props {
4
+ title: string;
5
+ description: string;
6
+ type?: "article" | "website";
7
+ jsonLd?: unknown[];
8
+ }
9
+ const { title, description, type = "article", jsonLd = [] } = Astro.props;
10
+ const canonical = Astro.site ? new URL(Astro.url.pathname, Astro.site).href : undefined;
11
+ ---
12
+ <meta name="description" content={description} />
13
+ {canonical && <link rel="canonical" href={canonical} />}
14
+ <meta property="og:type" content={type} />
15
+ <meta property="og:title" content={title} />
16
+ <meta property="og:description" content={description} />
17
+ {canonical && <meta property="og:url" content={canonical} />}
18
+ <meta name="twitter:card" content="summary" />
19
+ {jsonLd.map((data) => <script type="application/ld+json" set:html={JSON.stringify(data)} />)}
@@ -0,0 +1,54 @@
1
+ ---
2
+ /** The idea's text: crawlable HTML with each question as an <h2> and its answers as links. */
3
+ import type { IdeaPage } from "../dist/helpers.js";
4
+
5
+ interface Props {
6
+ page: IdeaPage;
7
+ /** URL of the idea's neighbourhood file, for scripts that want it. */
8
+ neighbourhood: string;
9
+ }
10
+ const { page, neighbourhood } = Astro.props;
11
+ const { idea } = page;
12
+ ---
13
+ <article
14
+ class="bc-idea"
15
+ data-because-id={idea.id}
16
+ data-because-kind={idea.kind}
17
+ data-because-neighbourhood={neighbourhood}
18
+ >
19
+ {page.trail.length > 1 && (
20
+ <nav class="bc-trail" aria-label="Trail">
21
+ <ol>
22
+ {page.trail.map((t, i) =>
23
+ i === page.trail.length - 1
24
+ ? <li aria-current="page">{t.idea.title}</li>
25
+ : <li><a href={t.href}>{t.idea.title}</a></li>
26
+ )}
27
+ </ol>
28
+ </nav>
29
+ )}
30
+ <h1>{idea.title}</h1>
31
+ {idea.date && <p class="bc-meta"><time datetime={idea.date}>{idea.date}</time></p>}
32
+ <div class="bc-body" set:html={page.html} />
33
+
34
+ {page.questions.map((q) => (
35
+ <section class="bc-question" data-question-type={q.type} id={q.type}>
36
+ <h2>{q.label}</h2>
37
+ {q.answers.map((a) => (
38
+ <div class="bc-answer">
39
+ <h3><a href={a.href}>{a.idea.title}</a></h3>
40
+ {a.idea.summary && <p>{a.idea.summary}</p>}
41
+ </div>
42
+ ))}
43
+ </section>
44
+ ))}
45
+
46
+ {page.leadsHere.length > 0 && (
47
+ <section class="bc-leads-here">
48
+ <h2>Leads here</h2>
49
+ <ul>
50
+ {page.leadsHere.map((l) => <li><a href={l.href}>{l.idea.title}</a> <span class="bc-meta">{l.label}</span></li>)}
51
+ </ul>
52
+ </section>
53
+ )}
54
+ </article>
@@ -0,0 +1,134 @@
1
+ ---
2
+ /**
3
+ * Full-screen idea page: the map fills the window and the idea's text sits in
4
+ * a panel on the right (below the map on phones). Without JavaScript there is
5
+ * no map and the text panel becomes an ordinary centred article.
6
+ */
7
+ interface Props {
8
+ title: string;
9
+ /** Home link in the top bar. */
10
+ homeHref: string;
11
+ homeLabel: string;
12
+ indexHref: string;
13
+ }
14
+ const { title, homeHref, homeLabel, indexHref } = Astro.props;
15
+ ---
16
+ <!doctype html>
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="utf-8" />
20
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
21
+ <title>{title}</title>
22
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
23
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
24
+ <link
25
+ rel="stylesheet"
26
+ href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500&family=JetBrains+Mono:wght@400;500&display=swap"
27
+ />
28
+ <slot name="head" />
29
+ <style is:global>
30
+ :root {
31
+ --bc-board: #eef1ef; --bc-card: #ffffff; --bc-ink: #18201d; --bc-muted: #5b6863; --bc-line: #d3dbd6;
32
+ --bc-accent: #1f5f4a; --bc-bar: rgba(255,255,255,.93);
33
+ --bc-display: "Bricolage Grotesque", "Helvetica Neue", Arial, sans-serif;
34
+ --bc-text: "Source Serif 4", Georgia, "Times New Roman", serif;
35
+ --bc-mono: "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
36
+ --bc-side: 420px;
37
+ color-scheme: light;
38
+ }
39
+ @media (prefers-color-scheme: dark) {
40
+ :root:not([data-theme="light"]) {
41
+ --bc-board: #101513; --bc-card: #1a211e; --bc-ink: #e6ece9; --bc-muted: #95a39d; --bc-line: #2e3934;
42
+ --bc-accent: #7fcfae; --bc-bar: rgba(22,28,26,.93); color-scheme: dark;
43
+ }
44
+ }
45
+ :root[data-theme="dark"] {
46
+ --bc-board: #101513; --bc-card: #1a211e; --bc-ink: #e6ece9; --bc-muted: #95a39d; --bc-line: #2e3934;
47
+ --bc-accent: #7fcfae; --bc-bar: rgba(22,28,26,.93); color-scheme: dark;
48
+ }
49
+ * { box-sizing: border-box; }
50
+ html, body { height: 100%; }
51
+ body { margin: 0; background: var(--bc-board); color: var(--bc-ink); font-family: var(--bc-text); display: flex; flex-direction: column; }
52
+ :focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 3px; }
53
+
54
+ .bc-top {
55
+ display: flex; align-items: center; gap: 8px 18px; flex-wrap: wrap; z-index: 3;
56
+ padding: 10px max(16px, env(safe-area-inset-right)) 10px max(16px, env(safe-area-inset-left));
57
+ background: var(--bc-bar); border-bottom: 1px solid var(--bc-line);
58
+ }
59
+ .bc-top a { color: var(--bc-ink); text-decoration: none; }
60
+ .bc-top .bc-home { font-family: var(--bc-display); font-weight: 700; font-size: 19px; letter-spacing: -0.01em; }
61
+ .bc-top .bc-nav { font-family: var(--bc-mono); font-size: 12px; color: var(--bc-muted); }
62
+ .bc-top .bc-nav:hover { color: var(--bc-ink); }
63
+ .bc-top .bc-toggle { margin-left: auto; font: 12px var(--bc-mono); color: var(--bc-ink); background: var(--bc-card);
64
+ border: 1px solid var(--bc-line); border-radius: 6px; padding: 5px 9px; cursor: pointer; display: none; }
65
+ .bc-top .bc-toggle:hover { border-color: var(--bc-muted); }
66
+
67
+ .bc-shell { flex: 1; min-height: 0; display: flex; }
68
+ .bc-shell .bc-map-root { display: none; }
69
+ .bc-side { overflow-y: auto; background: var(--bc-card); }
70
+
71
+ /* Without the map (no JavaScript, or map: false): an ordinary article. */
72
+ .bc-shell:not(.has-map) .bc-side { flex: 1; }
73
+ .bc-shell:not(.has-map) .bc-idea { max-width: 42rem; margin: 0 auto; }
74
+
75
+ /* With the map: map fills the window, text in a panel on the right. */
76
+ .has-map .bc-map-root { display: block; flex: 1; min-width: 0; position: relative; }
77
+ .has-map .bc-side { width: var(--bc-side); flex: none; border-left: 1px solid var(--bc-line); }
78
+ body:has(.has-map) .bc-toggle { display: inline-block; }
79
+ .bc-shell.has-map.text-hidden .bc-side { display: none; }
80
+
81
+ @media (max-width: 760px) {
82
+ body { height: auto; min-height: 100%; }
83
+ .has-map { flex-direction: column; }
84
+ .has-map .bc-map-root { flex: none; height: 62vh; }
85
+ .has-map .bc-side { width: auto; border-left: 0; border-top: 1px solid var(--bc-line); overflow: visible; }
86
+ body:has(.has-map) .bc-toggle { display: none; }
87
+ }
88
+
89
+ .bc-idea { padding: 22px 24px 48px; font-size: 16.5px; line-height: 1.6; }
90
+ .bc-idea h1 { font-family: var(--bc-display); font-weight: 700; font-size: 28px; line-height: 1.15; letter-spacing: -0.01em; margin: 6px 0 4px; text-wrap: balance; }
91
+ .bc-idea h2 { font-family: var(--bc-display); font-weight: 700; font-size: 15px; margin: 28px 0 6px; color: var(--qc, var(--bc-ink)); }
92
+ .bc-idea h3 { font-family: var(--bc-display); font-weight: 500; font-size: 16px; margin: 10px 0 0; }
93
+ .bc-idea a { color: var(--bc-accent); }
94
+ .bc-idea .bc-meta, .bc-idea .bc-answer p { color: var(--bc-muted); margin: 2px 0; font-size: 14.5px; }
95
+ .bc-idea .bc-meta { font-family: var(--bc-mono); font-size: 11.5px; }
96
+ .bc-idea .bc-trail ol { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 4px 6px; font-family: var(--bc-mono); font-size: 11.5px; color: var(--bc-muted); }
97
+ .bc-idea .bc-trail li + li::before { content: "›"; margin-right: 6px; }
98
+ .bc-idea .bc-trail a { color: var(--bc-muted); }
99
+ .bc-idea .bc-question { border-left: 3px solid var(--qc, var(--bc-line)); padding-left: 12px; margin-top: 22px; }
100
+ .bc-idea .bc-question h2 { margin-top: 0; }
101
+ .bc-idea [data-question-type="clarify"] { --qc: var(--bc-q-clarify, #6a4bb3); }
102
+ .bc-idea [data-question-type="why"] { --qc: var(--bc-q-why, #1f6fb2); }
103
+ .bc-idea [data-question-type="whynot"] { --qc: var(--bc-q-whynot, #b2452f); }
104
+ .bc-idea [data-question-type="evidence"] { --qc: var(--bc-q-evidence, #2f7d4a); }
105
+ .bc-idea [data-question-type="sowhat"] { --qc: var(--bc-q-sowhat, #a2680f); }
106
+ .bc-idea .bc-leads-here ul { padding-left: 18px; }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <header class="bc-top">
111
+ <a class="bc-home" href={homeHref}>{homeLabel}</a>
112
+ <a class="bc-nav" href={indexHref}>All ideas</a>
113
+ <button type="button" class="bc-toggle" aria-pressed="false">Hide text</button>
114
+ </header>
115
+ <div class="bc-shell">
116
+ <slot name="map" />
117
+ <aside class="bc-side" aria-label="Idea text"><slot /></aside>
118
+ </div>
119
+ <script>
120
+ const shell = document.querySelector(".bc-shell");
121
+ const toggle = document.querySelector<HTMLButtonElement>(".bc-toggle");
122
+ const set = (hidden: boolean) => {
123
+ shell?.classList.toggle("text-hidden", hidden);
124
+ if (toggle) {
125
+ toggle.textContent = hidden ? "Show text" : "Hide text";
126
+ toggle.setAttribute("aria-pressed", String(hidden));
127
+ }
128
+ try { localStorage.setItem("bc-text-hidden", hidden ? "1" : ""); } catch {}
129
+ };
130
+ try { if (localStorage.getItem("bc-text-hidden")) set(true); } catch {}
131
+ toggle?.addEventListener("click", () => set(!shell?.classList.contains("text-hidden")));
132
+ </script>
133
+ </body>
134
+ </html>
@@ -0,0 +1,64 @@
1
+ import { type Graph, type Idea, type IdeaSummary } from "@because-dev/core";
2
+ /** Join URL path segments with single slashes; keeps a trailing slash if the last segment has one. */
3
+ export declare function joinPath(...parts: string[]): string;
4
+ export interface PageContext {
5
+ /** Astro's `import.meta.env.BASE_URL`, for sites served under a subpath. */
6
+ siteBase: string;
7
+ /** Because base, e.g. "/ideas". */
8
+ base: string;
9
+ }
10
+ export declare const ideaHref: (ctx: PageContext, idea: Pick<Idea, "slug">) => string;
11
+ /** Where the graph files live, for the map's StaticStore: e.g. "/ideas". */
12
+ export declare const graphBase: (ctx: PageContext) => string;
13
+ export declare const neighbourhoodHref: (ctx: PageContext, id: string) => string;
14
+ export interface AnswerView {
15
+ idea: IdeaSummary;
16
+ href: string;
17
+ }
18
+ export interface QuestionView {
19
+ id: string;
20
+ type: string;
21
+ label: string;
22
+ answers: AnswerView[];
23
+ }
24
+ export interface IdeaPage {
25
+ idea: Idea;
26
+ html: string;
27
+ questions: QuestionView[];
28
+ /** Ideas whose questions this idea answers ("leads here"). */
29
+ leadsHere: {
30
+ idea: IdeaSummary;
31
+ href: string;
32
+ label: string;
33
+ }[];
34
+ trail: AnswerView[];
35
+ }
36
+ /**
37
+ * Precompute everything the per-idea page needs, so the .astro template stays thin.
38
+ * Memoised per graph and context, since every page render asks for the same site.
39
+ */
40
+ export declare function createSite(graph: Graph, ctx: PageContext): Site;
41
+ export interface Site {
42
+ page: (slug: string) => IdeaPage | undefined;
43
+ posts: Idea[];
44
+ }
45
+ /** Page title with the site name appended: "Heading · Site". */
46
+ export declare function pageTitle(heading: string, siteName?: string): string;
47
+ /** Meta description: the idea summary, or its title when the body is empty. */
48
+ export declare function describe(idea: IdeaSummary): string;
49
+ export declare function breadcrumbJsonLd(trail: AnswerView[], site: URL | undefined): {
50
+ "@context": string;
51
+ "@type": string;
52
+ itemListElement: {
53
+ item?: string | undefined;
54
+ "@type": string;
55
+ position: number;
56
+ name: string;
57
+ }[];
58
+ };
59
+ /** Title, meta description and JSON-LD (breadcrumbs from the trail; Article on posts) for an idea page. */
60
+ export declare function ideaHead(page: IdeaPage, siteName: string | undefined, site: URL | undefined): {
61
+ title: string;
62
+ description: string;
63
+ jsonLd: unknown[];
64
+ };
@@ -0,0 +1,106 @@
1
+ import { indexGraph, neighbourhoodPath, questionLabel, summarizeIdea, trailTo, } from "@because-dev/core";
2
+ import { createRenderer } from "@because-dev/parser";
3
+ /** Join URL path segments with single slashes; keeps a trailing slash if the last segment has one. */
4
+ export function joinPath(...parts) {
5
+ const joined = parts.filter(Boolean).join("/").replace(/\/{2,}/g, "/");
6
+ return joined.startsWith("/") ? joined : `/${joined}`;
7
+ }
8
+ export const ideaHref = (ctx, idea) => joinPath(ctx.siteBase, ctx.base, idea.slug, "/");
9
+ /** Where the graph files live, for the map's StaticStore: e.g. "/ideas". */
10
+ export const graphBase = (ctx) => joinPath(ctx.siteBase, ctx.base).replace(/\/+$/, "");
11
+ export const neighbourhoodHref = (ctx, id) => joinPath(ctx.siteBase, ctx.base, neighbourhoodPath(id));
12
+ const sites = new WeakMap();
13
+ /**
14
+ * Precompute everything the per-idea page needs, so the .astro template stays thin.
15
+ * Memoised per graph and context, since every page render asks for the same site.
16
+ */
17
+ export function createSite(graph, ctx) {
18
+ let byCtx = sites.get(graph);
19
+ if (!byCtx)
20
+ sites.set(graph, (byCtx = new Map()));
21
+ const key = `${ctx.siteBase}\0${ctx.base}`;
22
+ let site = byCtx.get(key);
23
+ if (!site)
24
+ byCtx.set(key, (site = buildSite(graph, ctx)));
25
+ return site;
26
+ }
27
+ function buildSite(graph, ctx) {
28
+ const index = indexGraph(graph);
29
+ const bySlug = new Map(graph.ideas.map((i) => [i.slug, i]));
30
+ const view = (id) => {
31
+ const idea = index.ideas.get(id);
32
+ return idea && { idea: summarizeIdea(idea), href: ideaHref(ctx, idea) };
33
+ };
34
+ const views = (ids) => ids.flatMap((id) => view(id) ?? []);
35
+ // The parser rewrites body links to idea ids and drops unpublished ones.
36
+ const render = createRenderer({
37
+ href: (target) => {
38
+ const hit = index.ideas.get(target);
39
+ return hit ? ideaHref(ctx, hit) : undefined;
40
+ },
41
+ });
42
+ const page = (slug) => {
43
+ const idea = bySlug.get(slug);
44
+ if (!idea)
45
+ return undefined;
46
+ const questions = (index.outgoing.get(idea.id) ?? []).map((q) => ({
47
+ id: q.id,
48
+ type: q.type,
49
+ label: questionLabel(q),
50
+ answers: views(q.answers),
51
+ }));
52
+ const leadsHere = (index.incoming.get(idea.id) ?? []).flatMap((q) => {
53
+ const v = view(q.from);
54
+ return v ? [{ ...v, label: questionLabel(q) }] : [];
55
+ });
56
+ return {
57
+ idea,
58
+ html: render(idea.body),
59
+ questions,
60
+ leadsHere,
61
+ trail: views(trailTo(index, idea.id)),
62
+ };
63
+ };
64
+ return { page, posts: graph.ideas.filter((i) => i.kind === "post") };
65
+ }
66
+ /** Page title with the site name appended: "Heading · Site". */
67
+ export function pageTitle(heading, siteName) {
68
+ return siteName ? `${heading} · ${siteName}` : heading;
69
+ }
70
+ /** Meta description: the idea summary, or its title when the body is empty. */
71
+ export function describe(idea) {
72
+ return idea.summary || idea.title;
73
+ }
74
+ export function breadcrumbJsonLd(trail, site) {
75
+ return {
76
+ "@context": "https://schema.org",
77
+ "@type": "BreadcrumbList",
78
+ itemListElement: trail.map((t, i) => ({
79
+ "@type": "ListItem",
80
+ position: i + 1,
81
+ name: t.idea.title,
82
+ ...(site ? { item: new URL(t.href, site).href } : {}),
83
+ })),
84
+ };
85
+ }
86
+ /** Title, meta description and JSON-LD (breadcrumbs from the trail; Article on posts) for an idea page. */
87
+ export function ideaHead(page, siteName, site) {
88
+ const { idea } = page;
89
+ const description = describe(idea);
90
+ const jsonLd = [];
91
+ if (page.trail.length > 1)
92
+ jsonLd.push(breadcrumbJsonLd(page.trail, site));
93
+ if (idea.kind === "post") {
94
+ const modified = idea.updated ?? idea.date;
95
+ jsonLd.push({
96
+ "@context": "https://schema.org",
97
+ "@type": "Article",
98
+ headline: idea.title,
99
+ description,
100
+ ...(idea.date ? { datePublished: idea.date } : {}),
101
+ ...(modified ? { dateModified: modified } : {}),
102
+ });
103
+ }
104
+ return { title: pageTitle(idea.title, siteName), description, jsonLd };
105
+ }
106
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { AstroIntegration } from "astro";
2
+ export interface BecauseOptions {
3
+ /** Folder of markdown notes, relative to the project root. Default "./ideas". */
4
+ content?: string;
5
+ /** URL prefix for Because pages. Default "/ideas". */
6
+ base?: string;
7
+ /**
8
+ * Layout component wrapping each page, relative to the project root, e.g.
9
+ * "./src/layouts/Base.astro". It receives `title`, `description` and
10
+ * `canonical` props, a default slot and a "head" slot. Defaults to a plain layout.
11
+ */
12
+ layout?: string;
13
+ /** Appended to page titles: "Idea title · <siteName>". */
14
+ siteName?: string;
15
+ /** Draw the interactive map above each idea page. Default true. */
16
+ map?: boolean;
17
+ /** Fail the build on broken links, unanswered questions or orphans. Default false. */
18
+ strict?: boolean;
19
+ /** Include `draft: true` notes. Default: only in `astro dev`. */
20
+ drafts?: boolean;
21
+ }
22
+ /** Serializable config the routes read from `virtual:because/config`. */
23
+ export interface ResolvedBecauseConfig {
24
+ /** Normalised base with a leading slash and no trailing slash, e.g. "/ideas". */
25
+ base: string;
26
+ siteName?: string;
27
+ map: boolean;
28
+ }
29
+ export declare function normaliseBase(base: string): string;
30
+ export default function because(options?: BecauseOptions): AstroIntegration;
package/dist/index.js ADDED
@@ -0,0 +1,104 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { checkGraph, graphPath, ideasIndexPath, indexGraph, postsIndexPath, staticFile } from "@because-dev/core";
4
+ import { loadVault } from "@because-dev/parser/fs";
5
+ const DATA = "virtual:because/data";
6
+ const CONFIG = "virtual:because/config";
7
+ const LAYOUT = "virtual:because/layout";
8
+ const route = (file) => fileURLToPath(new URL(`../routes/${file}`, import.meta.url));
9
+ export function normaliseBase(base) {
10
+ const trimmed = base.trim().replace(/^\/+|\/+$/g, "");
11
+ return trimmed ? `/${trimmed}` : "";
12
+ }
13
+ export default function because(options = {}) {
14
+ const base = normaliseBase(options.base ?? "/ideas");
15
+ return {
16
+ name: "@because-dev/astro",
17
+ hooks: {
18
+ "astro:config:setup": ({ config, command, injectRoute, updateConfig, logger }) => {
19
+ const root = fileURLToPath(config.root);
20
+ const abs = (p) => (isAbsolute(p) ? p : resolve(root, p));
21
+ const contentDir = abs(options.content ?? "./ideas");
22
+ const layoutPath = options.layout ? abs(options.layout) : route("../components/DefaultLayout.astro");
23
+ const drafts = options.drafts ?? command === "dev";
24
+ const resolved = { base, siteName: options.siteName, map: options.map ?? true };
25
+ const read = async () => {
26
+ const { graph, issues } = await loadVault(contentDir, { drafts });
27
+ const all = [...issues.map((i) => `${i.path}: ${i.message}`), ...checkGraph(graph).map((i) => i.message)];
28
+ for (const msg of all)
29
+ logger.warn(msg);
30
+ logger.info(`${graph.ideas.length} ideas, ${graph.questions.length} questions from ${contentDir}`);
31
+ if (options.strict && command === "build" && all.length) {
32
+ throw new Error(`Because found ${all.length} problem(s) and strict mode is on`);
33
+ }
34
+ return graph;
35
+ };
36
+ // Read the vault once; in dev, a change to any note clears the cache.
37
+ let cached = null;
38
+ const load = () => {
39
+ cached ??= read();
40
+ cached.catch(() => (cached = null));
41
+ return cached;
42
+ };
43
+ const plugin = {
44
+ name: "because:virtual",
45
+ resolveId(id) {
46
+ if (id === DATA || id === CONFIG || id === LAYOUT)
47
+ return `\0${id}`;
48
+ },
49
+ async load(id) {
50
+ if (id === `\0${DATA}`)
51
+ return `export const graph = ${JSON.stringify(await load())};`;
52
+ if (id === `\0${CONFIG}`)
53
+ return `export const config = ${JSON.stringify(resolved)};`;
54
+ if (id === `\0${LAYOUT}`)
55
+ return `export { default } from ${JSON.stringify(layoutPath)};`;
56
+ },
57
+ configureServer(server) {
58
+ server.watcher.add(contentDir);
59
+ const reload = (file) => {
60
+ if (!file.startsWith(contentDir) || !file.endsWith(".md"))
61
+ return;
62
+ cached = null;
63
+ const mod = server.moduleGraph.getModuleById(`\0${DATA}`);
64
+ if (mod)
65
+ server.moduleGraph.invalidateModule(mod);
66
+ server.ws.send({ type: "full-reload" });
67
+ };
68
+ server.watcher.on("add", reload).on("change", reload).on("unlink", reload);
69
+ // Serve the graph files directly in dev, so the map's fetches work whatever the
70
+ // site's trailingSlash setting (Astro's dev router would demand "x.json/" under "always").
71
+ const prefix = `/${[config.base, base].join("/").split("/").filter(Boolean).join("/")}`;
72
+ server.middlewares.use(async (req, res, next) => {
73
+ const path = (req.url ?? "").split("?")[0];
74
+ if (!path.startsWith(`${prefix}/`))
75
+ return next();
76
+ const rest = path.slice(prefix.length + 1);
77
+ if (!rest.endsWith(".json"))
78
+ return next();
79
+ try {
80
+ const body = staticFile(indexGraph(await load()), rest);
81
+ if (body === undefined)
82
+ return next();
83
+ res.setHeader("content-type", "application/json");
84
+ res.end(JSON.stringify(body));
85
+ }
86
+ catch (err) {
87
+ next(err);
88
+ }
89
+ });
90
+ },
91
+ };
92
+ updateConfig({ vite: { plugins: [plugin] } });
93
+ injectRoute({ pattern: base || "/", entrypoint: route("index.astro") });
94
+ injectRoute({ pattern: `${base}/[slug]`, entrypoint: route(resolved.map ? "idea.astro" : "idea-plain.astro") });
95
+ injectRoute({ pattern: `${base}/${graphPath}`, entrypoint: route("graph.json.ts") });
96
+ injectRoute({ pattern: `${base}/_graph/[id].json`, entrypoint: route("neighbourhood.json.ts") });
97
+ injectRoute({ pattern: `${base}/_ideas/[id].json`, entrypoint: route("idea.json.ts") });
98
+ injectRoute({ pattern: `${base}/${postsIndexPath}`, entrypoint: route("posts.json.ts") });
99
+ injectRoute({ pattern: `${base}/${ideasIndexPath}`, entrypoint: route("ideas.json.ts") });
100
+ },
101
+ },
102
+ };
103
+ }
104
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@because-dev/astro",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Astro integration: one static page per Because idea, the interactive map, and per-idea neighbourhood JSON.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "astro-integration",
9
+ "withastro",
10
+ "because",
11
+ "blog",
12
+ "digital-garden",
13
+ "linked-notes",
14
+ "obsidian"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./routes/*": "./routes/*",
22
+ "./components/*": "./components/*",
23
+ "./virtual": {
24
+ "types": "./virtual.d.ts"
25
+ },
26
+ "./helpers": {
27
+ "types": "./dist/helpers.d.ts",
28
+ "import": "./dist/helpers.js"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "routes",
34
+ "components",
35
+ "virtual.d.ts"
36
+ ],
37
+ "dependencies": {
38
+ "@because-dev/core": "0.1.0",
39
+ "@because-dev/parser": "0.1.0",
40
+ "@because-dev/map": "0.1.0"
41
+ },
42
+ "peerDependencies": {
43
+ "astro": "^5.0.0 || ^6.0.0 || ^7.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "astro": "^5.18.2",
47
+ "vite": "^6.4.1"
48
+ },
49
+ "author": "Arvind Suresh",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/arvind0598/because.git",
53
+ "directory": "packages/astro"
54
+ },
55
+ "homepage": "https://github.com/arvind0598/because#readme",
56
+ "bugs": "https://github.com/arvind0598/because/issues",
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "scripts": {
61
+ "build": "tsc -p tsconfig.json",
62
+ "typecheck": "tsc -p tsconfig.json --noEmit"
63
+ }
6
64
  }
@@ -0,0 +1,2 @@
1
+ /// <reference types="astro/client" />
2
+ /// <reference path="../virtual.d.ts" />
@@ -0,0 +1,8 @@
1
+ import type { APIRoute } from "astro";
2
+ import { graphPath } from "@because-dev/core";
3
+ import { staticResponse } from "./static-file.js";
4
+
5
+ export const prerender = true;
6
+
7
+ /** The public export of the whole graph, in the spec format. The map never fetches it. */
8
+ export const GET: APIRoute = () => staticResponse(graphPath);
@@ -0,0 +1,24 @@
1
+ ---
2
+ import type { GetStaticPaths } from "astro";
3
+ import { graph } from "virtual:because/data";
4
+ import { config } from "virtual:because/config";
5
+ import { createSite, ideaHead, neighbourhoodHref } from "../dist/helpers.js";
6
+ import Layout from "virtual:because/layout";
7
+ import Head from "../components/Head.astro";
8
+ import IdeaArticle from "../components/IdeaArticle.astro";
9
+
10
+ export const prerender = true;
11
+
12
+ export const getStaticPaths = (() => graph.ideas.map((idea) => ({ params: { slug: idea.slug } }))) satisfies GetStaticPaths;
13
+
14
+ const ctx = { siteBase: import.meta.env.BASE_URL, base: config.base };
15
+ const site = createSite(graph, ctx);
16
+ const page = site.page(Astro.params.slug!)!;
17
+ const { idea } = page;
18
+ const { title, description, jsonLd } = ideaHead(page, config.siteName, Astro.site);
19
+ ---
20
+ <!-- Used when the integration's map option is false: the idea as an article in the site's own layout. -->
21
+ <Layout title={title} description={description}>
22
+ <Head slot="head" title={idea.title} description={description} jsonLd={jsonLd} />
23
+ <IdeaArticle page={page} neighbourhood={neighbourhoodHref(ctx, idea.id)} />
24
+ </Layout>
@@ -0,0 +1,31 @@
1
+ ---
2
+ import type { GetStaticPaths } from "astro";
3
+ import { graph } from "virtual:because/data";
4
+ import { config } from "virtual:because/config";
5
+ import { createSite, graphBase, ideaHead, joinPath, neighbourhoodHref } from "../dist/helpers.js";
6
+ import Head from "../components/Head.astro";
7
+ import IdeaShell from "../components/IdeaShell.astro";
8
+ import IdeaArticle from "../components/IdeaArticle.astro";
9
+
10
+ export const prerender = true;
11
+
12
+ export const getStaticPaths = (() => graph.ideas.map((idea) => ({ params: { slug: idea.slug } }))) satisfies GetStaticPaths;
13
+
14
+ const ctx = { siteBase: import.meta.env.BASE_URL, base: config.base };
15
+ const site = createSite(graph, ctx);
16
+ const page = site.page(Astro.params.slug!)!;
17
+ const { idea } = page;
18
+ const { title, description, jsonLd } = ideaHead(page, config.siteName, Astro.site);
19
+ const shellProps = { title, homeHref: ctx.siteBase, homeLabel: config.siteName ?? "Home", indexHref: joinPath(ctx.siteBase, config.base, "/") };
20
+ ---
21
+ <!-- The full-screen map, with the idea's text in a side panel. -->
22
+ <IdeaShell {...shellProps}>
23
+ <Head slot="head" title={idea.title} description={description} jsonLd={jsonLd} />
24
+ <div slot="map" class="bc-map-root" data-because-map data-because-base={graphBase(ctx)} data-because-focus={idea.id}></div>
25
+ <IdeaArticle page={page} neighbourhood={neighbourhoodHref(ctx, idea.id)} />
26
+ </IdeaShell>
27
+
28
+ <script>
29
+ import { mountAll } from "@because-dev/map/mount";
30
+ mountAll();
31
+ </script>
@@ -0,0 +1,9 @@
1
+ import type { APIRoute } from "astro";
2
+ import { ideaPath } from "@because-dev/core";
3
+ import { ideaParams, staticResponse } from "./static-file.js";
4
+
5
+ export const prerender = true;
6
+ export const getStaticPaths = ideaParams;
7
+
8
+ /** One full idea with its body; what StaticStore.getIdea fetches. */
9
+ export const GET: APIRoute = ({ params }) => staticResponse(ideaPath(params.id!));
@@ -0,0 +1,8 @@
1
+ import type { APIRoute } from "astro";
2
+ import { ideasIndexPath } from "@because-dev/core";
3
+ import { staticResponse } from "./static-file.js";
4
+
5
+ export const prerender = true;
6
+
7
+ /** Every idea without bodies; what StaticStore.listIdeas fetches. */
8
+ export const GET: APIRoute = () => staticResponse(ideasIndexPath);
@@ -0,0 +1,35 @@
1
+ ---
2
+ import Layout from "virtual:because/layout";
3
+ import { graph } from "virtual:because/data";
4
+ import { config } from "virtual:because/config";
5
+ import { createSite, ideaHref, pageTitle } from "../dist/helpers.js";
6
+ import Head from "../components/Head.astro";
7
+
8
+ export const prerender = true;
9
+
10
+ const ctx = { siteBase: import.meta.env.BASE_URL, base: config.base };
11
+ const site = createSite(graph, ctx);
12
+ const heading = "Ideas";
13
+ const title = pageTitle(heading, config.siteName);
14
+ const description = `${site.posts.length} posts and ${graph.ideas.length} linked ideas.`;
15
+ const others = graph.ideas.filter((i) => i.kind !== "post");
16
+ ---
17
+ <Layout title={title} description={description}>
18
+ <Head slot="head" title={heading} description={description} type="website" />
19
+ <h1>{heading}</h1>
20
+ <section class="bc-posts">
21
+ <h2>Posts</h2>
22
+ {site.posts.map((p) => (
23
+ <div class="bc-answer">
24
+ <h3><a href={ideaHref(ctx, p)}>{p.title}</a></h3>
25
+ {p.summary && <p>{p.summary}</p>}
26
+ </div>
27
+ ))}
28
+ </section>
29
+ {others.length > 0 && (
30
+ <section class="bc-all">
31
+ <h2>All ideas</h2>
32
+ <ul>{others.map((i) => <li><a href={ideaHref(ctx, i)}>{i.title}</a></li>)}</ul>
33
+ </section>
34
+ )}
35
+ </Layout>
@@ -0,0 +1,9 @@
1
+ import type { APIRoute } from "astro";
2
+ import { neighbourhoodPath } from "@because-dev/core";
3
+ import { ideaParams, staticResponse } from "./static-file.js";
4
+
5
+ export const prerender = true;
6
+ export const getStaticPaths = ideaParams;
7
+
8
+ /** One precomputed depth-1 neighbourhood per idea; what StaticStore.getNeighbourhood fetches. */
9
+ export const GET: APIRoute = ({ params }) => staticResponse(neighbourhoodPath(params.id!));
@@ -0,0 +1,8 @@
1
+ import type { APIRoute } from "astro";
2
+ import { postsIndexPath } from "@because-dev/core";
3
+ import { staticResponse } from "./static-file.js";
4
+
5
+ export const prerender = true;
6
+
7
+ /** Posts without bodies; what StaticStore.listPosts fetches. */
8
+ export const GET: APIRoute = () => staticResponse(postsIndexPath);
@@ -0,0 +1,13 @@
1
+ import { indexGraph, staticFile } from "@because-dev/core";
2
+ import { graph } from "virtual:because/data";
3
+
4
+ const index = indexGraph(graph);
5
+
6
+ /** Response for one file of the static layout (see @because-dev/core static.ts). */
7
+ export function staticResponse(path: string): Response {
8
+ const body = staticFile(index, path);
9
+ if (body === undefined) return new Response("Not found", { status: 404 });
10
+ return new Response(JSON.stringify(body), { headers: { "content-type": "application/json" } });
11
+ }
12
+
13
+ export const ideaParams = () => graph.ideas.map((idea) => ({ params: { id: idea.id } }));
package/virtual.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ declare module "virtual:because/data" {
2
+ import type { Graph } from "@because-dev/core";
3
+ export const graph: Graph;
4
+ }
5
+
6
+ declare module "virtual:because/config" {
7
+ export const config: import("./dist/index.js").ResolvedBecauseConfig;
8
+ }
9
+
10
+ declare module "virtual:because/layout" {
11
+ const Layout: (props: Record<string, unknown>) => unknown;
12
+ export default Layout;
13
+ }