@silicajs/next 0.2.2 → 0.3.1

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.
@@ -1,7 +1,16 @@
1
1
  import path from "node:path";
2
- import fs from "fs-extra";
3
- import { createWikiLinkResolutionIndex } from "@silicajs/core/runtime";
4
- let pageRuntimeDataCache;
2
+ import fs from "node:fs";
3
+ import Database from "better-sqlite3";
4
+ import {
5
+ asFullSlug,
6
+ normalizeAssetReference,
7
+ normalizeSlug,
8
+ resolveRelativeAsset,
9
+ resolveRelative,
10
+ slugToHref
11
+ } from "@silicajs/core/runtime";
12
+ const VAULT_DATABASE_FILENAME = "vault.db";
13
+ let loadedVaultDb;
5
14
  function getProjectRoot() {
6
15
  const projectRoot = process.env.SILICA_PROJECT_ROOT;
7
16
  if (!projectRoot) {
@@ -12,79 +21,309 @@ function getProjectRoot() {
12
21
  function getSilicaRoot() {
13
22
  return path.join(getProjectRoot(), ".silica");
14
23
  }
15
- async function loadManifest() {
16
- const manifest = await fs.readJson(
17
- path.join(getSilicaRoot(), "manifest.json")
18
- );
19
- const entries = manifest.entries.map((entry) => ({
20
- ...entry,
21
- file: path.isAbsolute(entry.file) ? entry.file : path.join(getProjectRoot(), entry.file)
22
- }));
24
+ function getVaultDatabasePath() {
25
+ return path.join(getSilicaRoot(), VAULT_DATABASE_FILENAME);
26
+ }
27
+ function loadVaultDb() {
28
+ const databasePath = getVaultDatabasePath();
29
+ const stat = fs.statSync(databasePath);
30
+ if (loadedVaultDb?.databasePath === databasePath && loadedVaultDb.mtimeMs === stat.mtimeMs) {
31
+ return loadedVaultDb;
32
+ }
33
+ loadedVaultDb?.close();
34
+ const db = new Database(databasePath, {
35
+ fileMustExist: true,
36
+ readonly: true
37
+ });
38
+ db.pragma("query_only = ON");
39
+ const metadata = readMetadata(db);
40
+ loadedVaultDb = {
41
+ databasePath,
42
+ generatedAt: metadata.generatedAt,
43
+ renderEnvironmentHash: metadata.renderEnvironmentHash,
44
+ config: metadata.config,
45
+ cacheState: metadata.cacheState,
46
+ mtimeMs: stat.mtimeMs,
47
+ db,
48
+ close: () => {
49
+ db.close();
50
+ if (loadedVaultDb?.db === db) loadedVaultDb = void 0;
51
+ }
52
+ };
53
+ return loadedVaultDb;
54
+ }
55
+ function getPage(slug) {
56
+ const row = loadVaultDb().db.prepare("SELECT * FROM notes WHERE slug = ?").get(slug);
57
+ return row ? noteRowToEntry(row) : void 0;
58
+ }
59
+ function getPageRuntimeData(slug) {
60
+ const entry = getPage(slug);
61
+ if (!entry) return void 0;
62
+ return {
63
+ entry,
64
+ config: getConfig(),
65
+ cacheState: getCacheState()
66
+ };
67
+ }
68
+ function getRenderKey(slug) {
69
+ const loaded = loadVaultDb();
70
+ const row = loaded.db.prepare("SELECT render_hash FROM notes WHERE slug = ?").get(slug);
71
+ return {
72
+ renderHash: row?.render_hash ?? "missing",
73
+ renderEnvironmentHash: loaded.renderEnvironmentHash
74
+ };
75
+ }
76
+ function loadRenderEnvironmentHash() {
77
+ return loadVaultDb().renderEnvironmentHash;
78
+ }
79
+ function getPrerenderSlugs() {
80
+ return loadVaultDb().db.prepare(
81
+ "SELECT slug FROM notes WHERE prerender = 1 ORDER BY COALESCE(sort_key, slug), slug"
82
+ ).all().map((row) => row.slug);
83
+ }
84
+ function getAllSlugs() {
85
+ return loadVaultDb().db.prepare("SELECT slug FROM notes ORDER BY slug").all().map((row) => row.slug);
86
+ }
87
+ function getNavigation() {
88
+ const entries = loadVaultDb().db.prepare(
89
+ `
90
+ SELECT slug, menu_label AS title, sort_key
91
+ FROM notes
92
+ WHERE listed = 1
93
+ ORDER BY COALESCE(sort_key, slug), slug
94
+ `
95
+ ).all();
23
96
  return {
24
- ...manifest,
25
- entries,
26
- allSlugs: manifest.allSlugs ?? entries.map((entry) => entry.slug),
27
- bySlug: manifest.bySlug ?? Object.fromEntries(entries.map((entry) => [entry.slug, entry]))
97
+ version: 1,
98
+ entries: entries.map((entry) => ({
99
+ slug: entry.slug,
100
+ title: entry.title,
101
+ sortKey: entry.sort_key ?? void 0
102
+ }))
28
103
  };
29
104
  }
30
- async function loadGraph() {
31
- return fs.readJson(
32
- path.join(getSilicaRoot(), "graph.json")
33
- );
34
- }
35
- async function loadNavigation() {
36
- return fs.readJson(
37
- path.join(getSilicaRoot(), "navigation.json")
38
- );
39
- }
40
- async function loadBuildId() {
41
- return (await fs.readFile(path.join(getSilicaRoot(), "build-id.txt"), "utf8")).trim();
42
- }
43
- async function loadResolvedConfig() {
44
- return fs.readJson(
45
- path.join(getSilicaRoot(), "config.json")
46
- );
47
- }
48
- async function loadPageRuntimeData() {
49
- const buildId = await loadBuildId();
50
- const cacheKey = `${getProjectRoot()}:${buildId}`;
51
- if (pageRuntimeDataCache?.cacheKey === cacheKey) {
52
- return pageRuntimeDataCache.promise;
105
+ function getBacklinks(slug) {
106
+ return loadVaultDb().db.prepare(
107
+ `
108
+ SELECT n.slug, n.title
109
+ FROM links l
110
+ JOIN notes n ON n.slug = l.source_slug
111
+ WHERE l.target_slug = ?
112
+ AND l.kind = 'link'
113
+ ORDER BY n.title COLLATE NOCASE ASC, n.slug
114
+ `
115
+ ).all(slug);
116
+ }
117
+ function getConfig() {
118
+ return loadVaultDb().config;
119
+ }
120
+ function getCacheState() {
121
+ return loadVaultDb().cacheState;
122
+ }
123
+ function getTagSlugs() {
124
+ return loadVaultDb().db.prepare("SELECT DISTINCT tag FROM note_tags ORDER BY tag").all().map((row) => row.tag);
125
+ }
126
+ function getEntriesForTag(tag) {
127
+ return loadVaultDb().db.prepare(
128
+ `
129
+ SELECT n.*
130
+ FROM notes n
131
+ WHERE n.listed = 1
132
+ AND EXISTS (
133
+ SELECT 1
134
+ FROM note_tags nt
135
+ WHERE nt.slug = n.slug
136
+ AND nt.tag = ?
137
+ )
138
+ ORDER BY n.title COLLATE NOCASE ASC, n.slug
139
+ `
140
+ ).all(tag).map((row) => noteRowToEntry(row));
141
+ }
142
+ function getRelatedTagsForEntries(slugs, tag) {
143
+ if (slugs.length === 0) return [];
144
+ return loadVaultDb().db.prepare(
145
+ `
146
+ SELECT nt.tag, COUNT(*) AS count
147
+ FROM note_tags nt
148
+ WHERE nt.slug IN (${slugs.map(() => "?").join(", ")})
149
+ AND nt.tag != ?
150
+ GROUP BY nt.tag
151
+ ORDER BY count DESC, nt.tag ASC
152
+ LIMIT 12
153
+ `
154
+ ).all(...slugs, tag).map((row) => row.tag);
155
+ }
156
+ function resolveWikiLinkFromDb(currentSlug, target, strategy, ordering) {
157
+ const [rawPath] = target.split("#");
158
+ const slugOptions = ordering ?? getConfig().ordering;
159
+ const normalizedTarget = normalizeSlug(rawPath ?? target, slugOptions);
160
+ const db = loadVaultDb().db;
161
+ if (strategy === "absolute") {
162
+ return lookupAlias(db, "absolute", normalizedTarget);
53
163
  }
54
- const promise = Promise.all([
55
- loadManifest(),
56
- loadGraph(),
57
- loadResolvedConfig()
58
- ]).then(([manifest, graph, config]) => ({
59
- buildId,
60
- manifest,
61
- graph,
62
- config,
63
- wikilinkIndex: createWikiLinkResolutionIndex(
64
- manifest.allSlugs,
65
- config.ordering
66
- )
67
- }));
68
- pageRuntimeDataCache = { cacheKey, promise };
69
- promise.catch(() => {
70
- if (pageRuntimeDataCache?.cacheKey === cacheKey) {
71
- pageRuntimeDataCache = void 0;
72
- }
73
- });
74
- return promise;
164
+ if (strategy === "relative") {
165
+ const relative = resolveRelative(
166
+ asFullSlug(currentSlug),
167
+ normalizedTarget,
168
+ slugOptions
169
+ );
170
+ const resolved = lookupAlias(db, "absolute", relative);
171
+ if (resolved) return resolved;
172
+ }
173
+ return lookupAlias(db, "absolute", normalizedTarget) ?? lookupAlias(db, "absolute", `${normalizedTarget}/index`) ?? lookupAlias(db, "shortest", normalizedTarget.split("/").at(-1) ?? "");
174
+ }
175
+ function resolveAssetFromDb(currentSourcePath, target, strategy, ordering) {
176
+ const assetOptions = ordering ?? getConfig().ordering;
177
+ const normalizedTarget = normalizeAssetReference(target, assetOptions);
178
+ if (!normalizedTarget) return void 0;
179
+ const db = loadVaultDb().db;
180
+ const isExplicitRelative = isExplicitRelativeAssetReference(target);
181
+ if (isExplicitRelative || strategy === "relative") {
182
+ const relative = resolveRelativeAsset(
183
+ currentSourcePath,
184
+ target,
185
+ assetOptions
186
+ );
187
+ const resolved = lookupAssetAlias(db, "absolute", relative);
188
+ if (resolved || isExplicitRelative) return resolved;
189
+ }
190
+ if (strategy === "absolute") {
191
+ return lookupAssetAlias(db, "absolute", normalizedTarget);
192
+ }
193
+ return lookupAssetAlias(db, "absolute", normalizedTarget) ?? lookupAssetAlias(db, "shortest", normalizedTarget.split("/").at(-1) ?? "");
194
+ }
195
+ function getBreadcrumbs(slug) {
196
+ if (slug === "index" || !slug.includes("/")) return [];
197
+ const breadcrumbs = [
198
+ { label: "Home", href: "/" }
199
+ ];
200
+ const segments = slug.split("/").slice(0, -1);
201
+ let acc = "";
202
+ for (const segment of segments) {
203
+ acc = acc ? `${acc}/${segment}` : segment;
204
+ breadcrumbs.push({
205
+ label: prettySegment(segment),
206
+ href: breadcrumbSegmentHref(acc)
207
+ });
208
+ }
209
+ return breadcrumbs;
210
+ }
211
+ function loadSearchIndex() {
212
+ const loaded = loadVaultDb();
213
+ return {
214
+ databasePath: loaded.databasePath,
215
+ db: loaded.db,
216
+ close: () => void 0
217
+ };
75
218
  }
76
219
  function normalizeRouteSlug(slug) {
77
220
  return slug?.length ? slug.join("/") : "index";
78
221
  }
222
+ function readMetadata(db) {
223
+ const rows = db.prepare("SELECT key, value FROM vault_metadata").all();
224
+ const metadata = Object.fromEntries(rows.map((row) => [row.key, row.value]));
225
+ return {
226
+ generatedAt: metadata.generatedAt ?? "",
227
+ renderEnvironmentHash: metadata.renderEnvironmentHash ?? "silica",
228
+ config: parseConfigMetadata(metadata.configJson),
229
+ cacheState: JSON.parse(metadata.cacheStateJson ?? "{}")
230
+ };
231
+ }
232
+ function parseConfigMetadata(value) {
233
+ if (!value) throw new Error("vault.db is missing configJson metadata.");
234
+ return JSON.parse(value);
235
+ }
236
+ function lookupAlias(db, strategy, alias) {
237
+ const row = db.prepare(
238
+ `
239
+ SELECT slug
240
+ FROM slug_aliases
241
+ WHERE strategy_key = ?
242
+ AND alias = ?
243
+ ORDER BY sort_key, slug
244
+ LIMIT 2
245
+ `
246
+ ).all(strategy, alias);
247
+ return row.length === 1 ? row[0]?.slug : void 0;
248
+ }
249
+ function lookupAssetAlias(db, strategy, alias) {
250
+ const row = db.prepare(
251
+ `
252
+ SELECT asset_path
253
+ FROM asset_aliases
254
+ WHERE strategy_key = ?
255
+ AND alias = ?
256
+ ORDER BY sort_key, asset_path
257
+ LIMIT 2
258
+ `
259
+ ).all(strategy, alias);
260
+ return row.length === 1 ? row[0]?.asset_path : void 0;
261
+ }
262
+ function isExplicitRelativeAssetReference(value) {
263
+ const withoutSuffix = value.split(/[?#]/)[0] ?? "";
264
+ const normalized = withoutSuffix.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
265
+ return /^\.{1,2}\//.test(normalized);
266
+ }
267
+ function noteRowToEntry(row) {
268
+ return {
269
+ slug: row.slug,
270
+ title: row.title,
271
+ menuLabel: row.menu_label,
272
+ description: row.description ?? void 0,
273
+ generatedDescription: row.generated_description ?? void 0,
274
+ tags: parseJsonArray(row.tags_json),
275
+ file: path.isAbsolute(row.file) ? row.file : path.join(getProjectRoot(), row.file),
276
+ sourcePath: row.source_path,
277
+ sortKey: row.sort_key ?? void 0,
278
+ created: row.created ?? void 0,
279
+ modified: row.modified ?? void 0,
280
+ frontmatter: parseObject(row.frontmatter_json),
281
+ contentHash: row.content_hash,
282
+ embeds: getEmbeds(row.slug)
283
+ };
284
+ }
285
+ function getEmbeds(slug) {
286
+ return loadVaultDb().db.prepare(
287
+ "SELECT target_slug FROM links WHERE source_slug = ? AND kind = 'embed' ORDER BY target_slug"
288
+ ).all(slug).map((row) => row.target_slug);
289
+ }
290
+ function breadcrumbSegmentHref(segmentPath) {
291
+ if (getPage(segmentPath)) return slugToHref(segmentPath);
292
+ const indexSlug = `${segmentPath}/index`;
293
+ if (getPage(indexSlug)) return slugToHref(indexSlug);
294
+ return void 0;
295
+ }
296
+ function prettySegment(segment) {
297
+ return segment.replace(/[-_]/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
298
+ }
299
+ function parseObject(value) {
300
+ return JSON.parse(value);
301
+ }
302
+ function parseJsonArray(value) {
303
+ return JSON.parse(value);
304
+ }
79
305
  export {
306
+ getAllSlugs,
307
+ getBacklinks,
308
+ getBreadcrumbs,
309
+ getCacheState,
310
+ getConfig,
311
+ getEntriesForTag,
312
+ getNavigation,
313
+ getPage,
314
+ getPageRuntimeData,
315
+ getPrerenderSlugs,
80
316
  getProjectRoot,
317
+ getRelatedTagsForEntries,
318
+ getRenderKey,
81
319
  getSilicaRoot,
82
- loadBuildId,
83
- loadGraph,
84
- loadManifest,
85
- loadNavigation,
86
- loadPageRuntimeData,
87
- loadResolvedConfig,
88
- normalizeRouteSlug
320
+ getTagSlugs,
321
+ getVaultDatabasePath,
322
+ loadRenderEnvironmentHash,
323
+ loadSearchIndex,
324
+ loadVaultDb,
325
+ normalizeRouteSlug,
326
+ resolveAssetFromDb,
327
+ resolveWikiLinkFromDb
89
328
  };
90
329
  //# sourceMappingURL=server-data.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server-data.ts"],"sourcesContent":["import path from \"node:path\";\nimport fs from \"fs-extra\";\nimport type {\n Graph,\n Manifest,\n Navigation,\n ResolvedSilicaConfig,\n WikiLinkResolutionIndex,\n} from \"@silicajs/core/runtime\";\nimport { createWikiLinkResolutionIndex } from \"@silicajs/core/runtime\";\n\nexport type PageRuntimeData = {\n buildId: string;\n manifest: Manifest;\n graph: Graph;\n config: ResolvedSilicaConfig;\n wikilinkIndex: WikiLinkResolutionIndex;\n};\n\nlet pageRuntimeDataCache:\n | { cacheKey: string; promise: Promise<PageRuntimeData> }\n | undefined;\n\nexport function getProjectRoot(): string {\n const projectRoot = process.env.SILICA_PROJECT_ROOT;\n if (!projectRoot) {\n throw new Error(\"SILICA_PROJECT_ROOT must be set by the Silica CLI.\");\n }\n\n return projectRoot;\n}\n\nexport function getSilicaRoot(): string {\n return path.join(getProjectRoot(), \".silica\");\n}\n\nexport async function loadManifest(): Promise<Manifest> {\n const manifest = (await fs.readJson(\n path.join(getSilicaRoot(), \"manifest.json\"),\n )) as Omit<Manifest, \"allSlugs\" | \"bySlug\"> &\n Partial<Pick<Manifest, \"allSlugs\" | \"bySlug\">>;\n const entries = manifest.entries.map((entry) => ({\n ...entry,\n file: path.isAbsolute(entry.file)\n ? entry.file\n : path.join(getProjectRoot(), entry.file),\n }));\n return {\n ...manifest,\n entries,\n allSlugs: manifest.allSlugs ?? entries.map((entry) => entry.slug),\n bySlug:\n manifest.bySlug ??\n Object.fromEntries(entries.map((entry) => [entry.slug, entry])),\n };\n}\n\nexport async function loadGraph(): Promise<Graph> {\n return fs.readJson(\n path.join(getSilicaRoot(), \"graph.json\"),\n ) as Promise<Graph>;\n}\n\nexport async function loadNavigation(): Promise<Navigation> {\n return fs.readJson(\n path.join(getSilicaRoot(), \"navigation.json\"),\n ) as Promise<Navigation>;\n}\n\nexport async function loadBuildId(): Promise<string> {\n return (\n await fs.readFile(path.join(getSilicaRoot(), \"build-id.txt\"), \"utf8\")\n ).trim();\n}\n\nexport async function loadResolvedConfig() {\n return fs.readJson(\n path.join(getSilicaRoot(), \"config.json\"),\n ) as Promise<ResolvedSilicaConfig>;\n}\n\nexport async function loadPageRuntimeData(): Promise<PageRuntimeData> {\n const buildId = await loadBuildId();\n const cacheKey = `${getProjectRoot()}:${buildId}`;\n if (pageRuntimeDataCache?.cacheKey === cacheKey) {\n return pageRuntimeDataCache.promise;\n }\n\n const promise = Promise.all([\n loadManifest(),\n loadGraph(),\n loadResolvedConfig(),\n ]).then(([manifest, graph, config]) => ({\n buildId,\n manifest,\n graph,\n config,\n wikilinkIndex: createWikiLinkResolutionIndex(\n manifest.allSlugs,\n config.ordering,\n ),\n }));\n\n pageRuntimeDataCache = { cacheKey, promise };\n promise.catch(() => {\n if (pageRuntimeDataCache?.cacheKey === cacheKey) {\n pageRuntimeDataCache = undefined;\n }\n });\n return promise;\n}\n\nexport function normalizeRouteSlug(slug?: string[]): string {\n return slug?.length ? slug.join(\"/\") : \"index\";\n}\n"],"mappings":"AAAA,OAAO,UAAU;AACjB,OAAO,QAAQ;AAQf,SAAS,qCAAqC;AAU9C,IAAI;AAIG,SAAS,iBAAyB;AACvC,QAAM,cAAc,QAAQ,IAAI;AAChC,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE;AAEA,SAAO;AACT;AAEO,SAAS,gBAAwB;AACtC,SAAO,KAAK,KAAK,eAAe,GAAG,SAAS;AAC9C;AAEA,eAAsB,eAAkC;AACtD,QAAM,WAAY,MAAM,GAAG;AAAA,IACzB,KAAK,KAAK,cAAc,GAAG,eAAe;AAAA,EAC5C;AAEA,QAAM,UAAU,SAAS,QAAQ,IAAI,CAAC,WAAW;AAAA,IAC/C,GAAG;AAAA,IACH,MAAM,KAAK,WAAW,MAAM,IAAI,IAC5B,MAAM,OACN,KAAK,KAAK,eAAe,GAAG,MAAM,IAAI;AAAA,EAC5C,EAAE;AACF,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,UAAU,SAAS,YAAY,QAAQ,IAAI,CAAC,UAAU,MAAM,IAAI;AAAA,IAChE,QACE,SAAS,UACT,OAAO,YAAY,QAAQ,IAAI,CAAC,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;AAAA,EAClE;AACF;AAEA,eAAsB,YAA4B;AAChD,SAAO,GAAG;AAAA,IACR,KAAK,KAAK,cAAc,GAAG,YAAY;AAAA,EACzC;AACF;AAEA,eAAsB,iBAAsC;AAC1D,SAAO,GAAG;AAAA,IACR,KAAK,KAAK,cAAc,GAAG,iBAAiB;AAAA,EAC9C;AACF;AAEA,eAAsB,cAA+B;AACnD,UACE,MAAM,GAAG,SAAS,KAAK,KAAK,cAAc,GAAG,cAAc,GAAG,MAAM,GACpE,KAAK;AACT;AAEA,eAAsB,qBAAqB;AACzC,SAAO,GAAG;AAAA,IACR,KAAK,KAAK,cAAc,GAAG,aAAa;AAAA,EAC1C;AACF;AAEA,eAAsB,sBAAgD;AACpE,QAAM,UAAU,MAAM,YAAY;AAClC,QAAM,WAAW,GAAG,eAAe,CAAC,IAAI,OAAO;AAC/C,MAAI,sBAAsB,aAAa,UAAU;AAC/C,WAAO,qBAAqB;AAAA,EAC9B;AAEA,QAAM,UAAU,QAAQ,IAAI;AAAA,IAC1B,aAAa;AAAA,IACb,UAAU;AAAA,IACV,mBAAmB;AAAA,EACrB,CAAC,EAAE,KAAK,CAAC,CAAC,UAAU,OAAO,MAAM,OAAO;AAAA,IACtC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe;AAAA,MACb,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF,EAAE;AAEF,yBAAuB,EAAE,UAAU,QAAQ;AAC3C,UAAQ,MAAM,MAAM;AAClB,QAAI,sBAAsB,aAAa,UAAU;AAC/C,6BAAuB;AAAA,IACzB;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAEO,SAAS,mBAAmB,MAAyB;AAC1D,SAAO,MAAM,SAAS,KAAK,KAAK,GAAG,IAAI;AACzC;","names":[]}
1
+ {"version":3,"sources":["../src/server-data.ts"],"sourcesContent":["import path from \"node:path\";\nimport fs from \"node:fs\";\nimport Database from \"better-sqlite3\";\nimport type { LoadedSearchIndex } from \"@silicajs/search\";\nimport type {\n Navigation,\n ManifestEntry,\n RenderCacheState,\n ResolvedSilicaConfig,\n} from \"@silicajs/core/runtime\";\nimport {\n asFullSlug,\n normalizeAssetReference,\n normalizeSlug,\n resolveRelativeAsset,\n resolveRelative,\n slugToHref,\n} from \"@silicajs/core/runtime\";\n\nconst VAULT_DATABASE_FILENAME = \"vault.db\";\n\nexport type LoadedVaultDb = {\n databasePath: string;\n generatedAt: string;\n renderEnvironmentHash: string;\n config: ResolvedSilicaConfig;\n cacheState: RenderCacheState;\n mtimeMs: number;\n db: Database.Database;\n close(): void;\n};\n\nexport type VaultPageData = {\n cacheState: RenderCacheState;\n config: ResolvedSilicaConfig;\n entry: ManifestEntry;\n};\n\ntype MetadataRows = {\n generatedAt: string;\n renderEnvironmentHash: string;\n config: ResolvedSilicaConfig;\n cacheState: RenderCacheState;\n};\n\ntype NoteRow = {\n slug: string;\n file: string;\n source_path: string;\n title: string;\n menu_label: string;\n description: string | null;\n generated_description: string | null;\n frontmatter_json: string;\n tags_json: string;\n created: string | null;\n modified: string | null;\n sort_key: string | null;\n listed: 0 | 1;\n content_hash: string;\n render_hash: string;\n prerender: 0 | 1;\n};\n\ntype NavigationRow = {\n slug: string;\n title: string;\n sort_key: string | null;\n};\n\ntype BacklinkRow = {\n slug: string;\n title: string;\n};\n\nlet loadedVaultDb: LoadedVaultDb | undefined;\n\nexport function getProjectRoot(): string {\n const projectRoot = process.env.SILICA_PROJECT_ROOT;\n if (!projectRoot) {\n throw new Error(\"SILICA_PROJECT_ROOT must be set by the Silica CLI.\");\n }\n\n return projectRoot;\n}\n\nexport function getSilicaRoot(): string {\n return path.join(getProjectRoot(), \".silica\");\n}\n\nexport function getVaultDatabasePath(): string {\n return path.join(getSilicaRoot(), VAULT_DATABASE_FILENAME);\n}\n\nexport function loadVaultDb(): LoadedVaultDb {\n const databasePath = getVaultDatabasePath();\n const stat = fs.statSync(databasePath);\n if (\n loadedVaultDb?.databasePath === databasePath &&\n loadedVaultDb.mtimeMs === stat.mtimeMs\n ) {\n return loadedVaultDb;\n }\n\n loadedVaultDb?.close();\n\n const db = new Database(databasePath, {\n fileMustExist: true,\n readonly: true,\n });\n db.pragma(\"query_only = ON\");\n const metadata = readMetadata(db);\n loadedVaultDb = {\n databasePath,\n generatedAt: metadata.generatedAt,\n renderEnvironmentHash: metadata.renderEnvironmentHash,\n config: metadata.config,\n cacheState: metadata.cacheState,\n mtimeMs: stat.mtimeMs,\n db,\n close: () => {\n db.close();\n if (loadedVaultDb?.db === db) loadedVaultDb = undefined;\n },\n };\n return loadedVaultDb;\n}\n\nexport function getPage(slug: string): ManifestEntry | undefined {\n const row = loadVaultDb()\n .db.prepare(\"SELECT * FROM notes WHERE slug = ?\")\n .get(slug) as NoteRow | undefined;\n return row ? noteRowToEntry(row) : undefined;\n}\n\nexport function getPageRuntimeData(slug: string): VaultPageData | undefined {\n const entry = getPage(slug);\n if (!entry) return undefined;\n return {\n entry,\n config: getConfig(),\n cacheState: getCacheState(),\n };\n}\n\nexport function getRenderKey(slug: string): {\n renderHash: string;\n renderEnvironmentHash: string;\n} {\n const loaded = loadVaultDb();\n const row = loaded.db\n .prepare(\"SELECT render_hash FROM notes WHERE slug = ?\")\n .get(slug) as { render_hash: string } | undefined;\n return {\n renderHash: row?.render_hash ?? \"missing\",\n renderEnvironmentHash: loaded.renderEnvironmentHash,\n };\n}\n\nexport function loadRenderEnvironmentHash(): string {\n return loadVaultDb().renderEnvironmentHash;\n}\n\nexport function getPrerenderSlugs(): string[] {\n return loadVaultDb()\n .db.prepare(\n \"SELECT slug FROM notes WHERE prerender = 1 ORDER BY COALESCE(sort_key, slug), slug\",\n )\n .all()\n .map((row) => (row as { slug: string }).slug);\n}\n\nexport function getAllSlugs(): string[] {\n return loadVaultDb()\n .db.prepare(\"SELECT slug FROM notes ORDER BY slug\")\n .all()\n .map((row) => (row as { slug: string }).slug);\n}\n\nexport function getNavigation(): Navigation {\n const entries = loadVaultDb()\n .db.prepare(\n `\n SELECT slug, menu_label AS title, sort_key\n FROM notes\n WHERE listed = 1\n ORDER BY COALESCE(sort_key, slug), slug\n `,\n )\n .all() as NavigationRow[];\n return {\n version: 1,\n entries: entries.map((entry) => ({\n slug: entry.slug,\n title: entry.title,\n sortKey: entry.sort_key ?? undefined,\n })),\n };\n}\n\nexport function getBacklinks(slug: string): BacklinkRow[] {\n return loadVaultDb()\n .db.prepare(\n `\n SELECT n.slug, n.title\n FROM links l\n JOIN notes n ON n.slug = l.source_slug\n WHERE l.target_slug = ?\n AND l.kind = 'link'\n ORDER BY n.title COLLATE NOCASE ASC, n.slug\n `,\n )\n .all(slug) as BacklinkRow[];\n}\n\nexport function getConfig(): ResolvedSilicaConfig {\n return loadVaultDb().config;\n}\n\nexport function getCacheState(): RenderCacheState {\n return loadVaultDb().cacheState;\n}\n\nexport function getTagSlugs(): string[] {\n return loadVaultDb()\n .db.prepare(\"SELECT DISTINCT tag FROM note_tags ORDER BY tag\")\n .all()\n .map((row) => (row as { tag: string }).tag);\n}\n\nexport function getEntriesForTag(tag: string): ManifestEntry[] {\n return loadVaultDb()\n .db.prepare(\n `\n SELECT n.*\n FROM notes n\n WHERE n.listed = 1\n AND EXISTS (\n SELECT 1\n FROM note_tags nt\n WHERE nt.slug = n.slug\n AND nt.tag = ?\n )\n ORDER BY n.title COLLATE NOCASE ASC, n.slug\n `,\n )\n .all(tag)\n .map((row) => noteRowToEntry(row as NoteRow));\n}\n\nexport function getRelatedTagsForEntries(\n slugs: string[],\n tag: string,\n): string[] {\n if (slugs.length === 0) return [];\n return loadVaultDb()\n .db.prepare(\n `\n SELECT nt.tag, COUNT(*) AS count\n FROM note_tags nt\n WHERE nt.slug IN (${slugs.map(() => \"?\").join(\", \")})\n AND nt.tag != ?\n GROUP BY nt.tag\n ORDER BY count DESC, nt.tag ASC\n LIMIT 12\n `,\n )\n .all(...slugs, tag)\n .map((row) => (row as { tag: string }).tag);\n}\n\nexport function resolveWikiLinkFromDb(\n currentSlug: string,\n target: string,\n strategy: \"absolute\" | \"relative\" | \"shortest\",\n ordering?: { numericPrefixes?: boolean },\n): string | undefined {\n const [rawPath] = target.split(\"#\");\n const slugOptions = ordering ?? getConfig().ordering;\n const normalizedTarget = normalizeSlug(rawPath ?? target, slugOptions);\n const db = loadVaultDb().db;\n\n if (strategy === \"absolute\") {\n return lookupAlias(db, \"absolute\", normalizedTarget);\n }\n\n if (strategy === \"relative\") {\n const relative = resolveRelative(\n asFullSlug(currentSlug),\n normalizedTarget,\n slugOptions,\n );\n const resolved = lookupAlias(db, \"absolute\", relative);\n if (resolved) return resolved;\n }\n\n return (\n lookupAlias(db, \"absolute\", normalizedTarget) ??\n lookupAlias(db, \"absolute\", `${normalizedTarget}/index`) ??\n lookupAlias(db, \"shortest\", normalizedTarget.split(\"/\").at(-1) ?? \"\")\n );\n}\n\nexport function resolveAssetFromDb(\n currentSourcePath: string,\n target: string,\n strategy: \"absolute\" | \"relative\" | \"shortest\",\n ordering?: { numericPrefixes?: boolean },\n): string | undefined {\n const assetOptions = ordering ?? getConfig().ordering;\n const normalizedTarget = normalizeAssetReference(target, assetOptions);\n if (!normalizedTarget) return undefined;\n const db = loadVaultDb().db;\n const isExplicitRelative = isExplicitRelativeAssetReference(target);\n\n if (isExplicitRelative || strategy === \"relative\") {\n const relative = resolveRelativeAsset(\n currentSourcePath,\n target,\n assetOptions,\n );\n const resolved = lookupAssetAlias(db, \"absolute\", relative);\n if (resolved || isExplicitRelative) return resolved;\n }\n\n if (strategy === \"absolute\") {\n return lookupAssetAlias(db, \"absolute\", normalizedTarget);\n }\n\n return (\n lookupAssetAlias(db, \"absolute\", normalizedTarget) ??\n lookupAssetAlias(db, \"shortest\", normalizedTarget.split(\"/\").at(-1) ?? \"\")\n );\n}\n\nexport function getBreadcrumbs(slug: string) {\n if (slug === \"index\" || !slug.includes(\"/\")) return [];\n\n const breadcrumbs: Array<{ label: string; href?: string }> = [\n { label: \"Home\", href: \"/\" },\n ];\n const segments = slug.split(\"/\").slice(0, -1);\n let acc = \"\";\n for (const segment of segments) {\n acc = acc ? `${acc}/${segment}` : segment;\n breadcrumbs.push({\n label: prettySegment(segment),\n href: breadcrumbSegmentHref(acc),\n });\n }\n return breadcrumbs;\n}\n\nexport function loadSearchIndex(): LoadedSearchIndex {\n const loaded = loadVaultDb();\n return {\n databasePath: loaded.databasePath,\n db: loaded.db,\n close: () => undefined,\n };\n}\n\nexport function normalizeRouteSlug(slug?: string[]): string {\n return slug?.length ? slug.join(\"/\") : \"index\";\n}\n\nfunction readMetadata(db: Database.Database): MetadataRows {\n const rows = db\n .prepare(\"SELECT key, value FROM vault_metadata\")\n .all() as Array<{ key: string; value: string }>;\n const metadata = Object.fromEntries(rows.map((row) => [row.key, row.value]));\n return {\n generatedAt: metadata.generatedAt ?? \"\",\n renderEnvironmentHash: metadata.renderEnvironmentHash ?? \"silica\",\n config: parseConfigMetadata(metadata.configJson),\n cacheState: JSON.parse(metadata.cacheStateJson ?? \"{}\") as RenderCacheState,\n };\n}\n\nfunction parseConfigMetadata(value: string | undefined): ResolvedSilicaConfig {\n if (!value) throw new Error(\"vault.db is missing configJson metadata.\");\n return JSON.parse(value) as ResolvedSilicaConfig;\n}\n\nfunction lookupAlias(\n db: Database.Database,\n strategy: string,\n alias: string,\n): string | undefined {\n const row = db\n .prepare(\n `\n SELECT slug\n FROM slug_aliases\n WHERE strategy_key = ?\n AND alias = ?\n ORDER BY sort_key, slug\n LIMIT 2\n `,\n )\n .all(strategy, alias) as Array<{ slug: string }>;\n return row.length === 1 ? row[0]?.slug : undefined;\n}\n\nfunction lookupAssetAlias(\n db: Database.Database,\n strategy: string,\n alias: string,\n): string | undefined {\n const row = db\n .prepare(\n `\n SELECT asset_path\n FROM asset_aliases\n WHERE strategy_key = ?\n AND alias = ?\n ORDER BY sort_key, asset_path\n LIMIT 2\n `,\n )\n .all(strategy, alias) as Array<{ asset_path: string }>;\n return row.length === 1 ? row[0]?.asset_path : undefined;\n}\n\nfunction isExplicitRelativeAssetReference(value: string): boolean {\n const withoutSuffix = value.split(/[?#]/)[0] ?? \"\";\n const normalized = withoutSuffix\n .replace(/\\\\/g, \"/\")\n .replace(/^\\/+/, \"\")\n .replace(/\\/+$/, \"\");\n return /^\\.{1,2}\\//.test(normalized);\n}\n\nfunction noteRowToEntry(row: NoteRow): ManifestEntry {\n return {\n slug: row.slug,\n title: row.title,\n menuLabel: row.menu_label,\n description: row.description ?? undefined,\n generatedDescription: row.generated_description ?? undefined,\n tags: parseJsonArray(row.tags_json),\n file: path.isAbsolute(row.file)\n ? row.file\n : path.join(getProjectRoot(), row.file),\n sourcePath: row.source_path,\n sortKey: row.sort_key ?? undefined,\n created: row.created ?? undefined,\n modified: row.modified ?? undefined,\n frontmatter: parseObject(row.frontmatter_json),\n contentHash: row.content_hash,\n embeds: getEmbeds(row.slug),\n };\n}\n\nfunction getEmbeds(slug: string): string[] {\n return loadVaultDb()\n .db.prepare(\n \"SELECT target_slug FROM links WHERE source_slug = ? AND kind = 'embed' ORDER BY target_slug\",\n )\n .all(slug)\n .map((row) => (row as { target_slug: string }).target_slug);\n}\n\nfunction breadcrumbSegmentHref(segmentPath: string): string | undefined {\n if (getPage(segmentPath)) return slugToHref(segmentPath);\n const indexSlug = `${segmentPath}/index`;\n if (getPage(indexSlug)) return slugToHref(indexSlug);\n return undefined;\n}\n\nfunction prettySegment(segment: string): string {\n return segment\n .replace(/[-_]/g, \" \")\n .replace(/\\b\\w/g, (letter) => letter.toUpperCase());\n}\n\nfunction parseObject(value: string): Record<string, unknown> {\n return JSON.parse(value) as Record<string, unknown>;\n}\n\nfunction parseJsonArray(value: string): string[] {\n return JSON.parse(value) as string[];\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,6 @@
1
1
  import theme from "../../../silica-theme";
2
2
  import { VaultContent } from "@silicajs/next/routes/page";
3
+ import { getRenderKey, normalizeRouteSlug } from "@silicajs/next/server-data";
3
4
  export {
4
5
  generateMetadata,
5
6
  generateStaticParams,
@@ -11,8 +12,14 @@ export default async function Page({
11
12
  params: Promise<{ slug?: string[] }> | { slug?: string[] };
12
13
  }) {
13
14
  const resolvedParams = await params;
14
- const slug = resolvedParams?.slug?.length
15
- ? resolvedParams.slug.join("/")
16
- : "index";
17
- return <VaultContent slug={slug} theme={theme} />;
15
+ const slug = normalizeRouteSlug(resolvedParams?.slug);
16
+ const renderKey = getRenderKey(slug);
17
+ return (
18
+ <VaultContent
19
+ slug={slug}
20
+ renderHash={renderKey.renderHash}
21
+ renderEnvironmentHash={renderKey.renderEnvironmentHash}
22
+ theme={theme}
23
+ />
24
+ );
18
25
  }
@@ -0,0 +1 @@
1
+ export { default } from "@silicajs/next/cache-handlers/filesystem";
@@ -1,8 +1,32 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { fileURLToPath } from "node:url";
1
5
  import type { NextConfig } from "next";
2
6
  /* __SILICA_CONFIG_IMPORT__ */
3
7
 
8
+ const require = createRequire(import.meta.url);
9
+ const nextRoot = path.dirname(fileURLToPath(import.meta.url));
10
+ const silicaRoot = path.resolve(nextRoot, "..");
11
+ const vaultMetadata = readVaultMetadata(path.join(silicaRoot, "vault.db"));
12
+ type VaultConfig = {
13
+ render?: { cache?: { storage?: "memory" | "filesystem" } };
14
+ };
15
+ const resolvedConfig = parseJson<VaultConfig>(vaultMetadata.configJson);
16
+ const useFilesystemCache = resolvedConfig?.render?.cache?.storage !== "memory";
17
+
4
18
  const baseNextConfig: NextConfig = {
5
19
  cacheComponents: true,
20
+ ...(useFilesystemCache
21
+ ? {
22
+ cacheHandlers: {
23
+ default: require.resolve("./cache-handlers/filesystem-cache.js"),
24
+ remote: require.resolve("./cache-handlers/filesystem-cache.js"),
25
+ },
26
+ }
27
+ : {}),
28
+ generateBuildId: async () => vaultMetadata.renderEnvironmentHash ?? "silica",
29
+ deploymentId: process.env.SILICA_DEPLOYMENT_ID,
6
30
  output: "standalone",
7
31
  transpilePackages: [
8
32
  "@silicajs/core",
@@ -15,15 +39,7 @@ const baseNextConfig: NextConfig = {
15
39
  ],
16
40
  serverExternalPackages: ["better-sqlite3"],
17
41
  outputFileTracingIncludes: {
18
- "/*": [
19
- "../content/**/*",
20
- "../manifest.json",
21
- "../navigation.json",
22
- "../graph.json",
23
- "../config.json",
24
- "../search.db",
25
- "../build-id.txt",
26
- ],
42
+ "/*": ["../content/**/*", "../vault.db"],
27
43
  },
28
44
  experimental: {
29
45
  externalDir: true,
@@ -31,6 +47,30 @@ const baseNextConfig: NextConfig = {
31
47
  },
32
48
  };
33
49
 
50
+ function readVaultMetadata(databasePath: string): {
51
+ renderEnvironmentHash?: string;
52
+ configJson?: string;
53
+ } {
54
+ if (!fs.existsSync(databasePath)) return {};
55
+ const Database = require("better-sqlite3") as typeof import("better-sqlite3");
56
+ const db = new Database(databasePath, {
57
+ fileMustExist: true,
58
+ readonly: true,
59
+ });
60
+ try {
61
+ const rows = db
62
+ .prepare("SELECT key, value FROM vault_metadata")
63
+ .all() as Array<{ key: string; value: string }>;
64
+ return Object.fromEntries(rows.map((row) => [row.key, row.value]));
65
+ } finally {
66
+ db.close();
67
+ }
68
+ }
69
+
70
+ function parseJson<T>(value: string | undefined): T | undefined {
71
+ return value ? (JSON.parse(value) as T) : undefined;
72
+ }
73
+
34
74
  /* __SILICA_CONFIG_OVERRIDE__ */
35
75
 
36
76
  export default nextConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silicajs/next",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Next.js runtime, routes, templates, and proxy for Silica.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -60,6 +60,14 @@
60
60
  "types": "./dist/dev-reload-client.d.ts",
61
61
  "import": "./dist/dev-reload-client.js"
62
62
  },
63
+ "./server-data": {
64
+ "types": "./dist/server-data.d.ts",
65
+ "import": "./dist/server-data.js"
66
+ },
67
+ "./cache-handlers/filesystem": {
68
+ "types": "./dist/cache-handlers/filesystem.d.ts",
69
+ "import": "./dist/cache-handlers/filesystem.js"
70
+ },
63
71
  "./proxy": {
64
72
  "types": "./dist/proxy.d.ts",
65
73
  "import": "./dist/proxy.js"
@@ -88,11 +96,12 @@
88
96
  },
89
97
  "dependencies": {
90
98
  "@silicajs/auth": "^0.1.1",
91
- "@silicajs/components": "^0.2.2",
92
- "@silicajs/core": "^0.4.0",
99
+ "@silicajs/components": "^0.2.4",
100
+ "@silicajs/core": "^0.6.0",
93
101
  "@silicajs/remark-obsidian": "^0.1.0",
94
- "@silicajs/search": "^0.2.0",
102
+ "@silicajs/search": "^0.3.1",
95
103
  "better-auth": "1.6.11",
104
+ "better-sqlite3": "^12.10.0",
96
105
  "jiti": "^2.7.0",
97
106
  "katex": "^0.17.0"
98
107
  },