@farming-labs/svelte 0.0.29 → 0.0.31

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/dist/server.d.ts CHANGED
@@ -43,6 +43,8 @@ export interface DocsServer {
43
43
  title: string;
44
44
  description?: string;
45
45
  html: string;
46
+ entry?: string;
47
+ locale?: string;
46
48
  slug?: string;
47
49
  previousPage: PageNode | null;
48
50
  nextPage: PageNode | null;
package/dist/server.js CHANGED
@@ -30,6 +30,7 @@
30
30
  import fs from "node:fs";
31
31
  import path from "node:path";
32
32
  import matter from "gray-matter";
33
+ import { resolveDocsI18n, resolveDocsLocale, resolveDocsPath } from "@farming-labs/docs";
33
34
  import { loadDocsNavTree, loadDocsContent, flattenNavTree } from "./content.js";
34
35
  import { renderMarkdown } from "./markdown.js";
35
36
  function resolveAIModelAndProvider(aiConfig, requestedModelId) {
@@ -73,6 +74,25 @@ function stripMarkdownText(content) {
73
74
  .replace(/\n{3,}/g, "\n\n")
74
75
  .trim();
75
76
  }
77
+ function normalizePathSegment(value) {
78
+ return value.replace(/^\/+|\/+$/g, "");
79
+ }
80
+ function joinPathParts(...parts) {
81
+ return parts
82
+ .map((part) => normalizePathSegment(part))
83
+ .filter(Boolean)
84
+ .join("/");
85
+ }
86
+ function toPosixPath(value) {
87
+ return value.replace(/\\\\/g, "/");
88
+ }
89
+ function buildDirPrefix(contentDir) {
90
+ const rel = path.isAbsolute(contentDir)
91
+ ? toPosixPath(path.relative(process.cwd(), contentDir))
92
+ : toPosixPath(contentDir);
93
+ const normalized = normalizePathSegment(rel);
94
+ return normalized ? `/${normalized}/` : "/";
95
+ }
76
96
  function navTreeFromMap(contentMap, dirPrefix, entry, ordering) {
77
97
  const dirs = [];
78
98
  for (const key of Object.keys(contentMap)) {
@@ -272,15 +292,14 @@ function findPageInMap(contentMap, dirPrefix, slug) {
272
292
  */
273
293
  export function createDocsServer(config = {}) {
274
294
  const entry = config.entry ?? "docs";
295
+ const contentDirBase = config.contentDir ?? entry;
296
+ const i18n = resolveDocsI18n(config.i18n);
275
297
  const githubRaw = config.github;
276
298
  const github = typeof githubRaw === "string" ? { url: githubRaw } : (githubRaw ?? null);
277
299
  const githubRepo = github?.url;
278
300
  const githubBranch = github?.branch ?? "main";
279
301
  const githubContentPath = github?.directory;
280
- const contentDir = path.resolve(config.contentDir ?? entry);
281
302
  const rawPreloaded = config._preloadedContent;
282
- const contentDirRel = config.contentDir ?? entry;
283
- const dirPrefix = `/${contentDirRel}/`;
284
303
  // Normalize keys: Vite's import.meta.glob may return paths without leading slash (e.g. "docs/...")
285
304
  const preloaded = rawPreloaded
286
305
  ? Object.fromEntries(Object.entries(rawPreloaded).map(([k, v]) => [k.startsWith("/") ? k : `/${k}`, v]))
@@ -291,20 +310,69 @@ export function createDocsServer(config = {}) {
291
310
  if (config.apiKey && !aiConfig.apiKey) {
292
311
  aiConfig.apiKey = config.apiKey;
293
312
  }
313
+ function resolveContentDirRel(locale) {
314
+ if (!locale)
315
+ return contentDirBase;
316
+ if (path.isAbsolute(contentDirBase))
317
+ return path.join(contentDirBase, locale);
318
+ return joinPathParts(contentDirBase, locale);
319
+ }
320
+ function resolveContextFromPath(pathname, locale) {
321
+ const match = resolveDocsPath(pathname, entry);
322
+ const contentDirRel = resolveContentDirRel(locale);
323
+ return {
324
+ ...match,
325
+ locale,
326
+ contentDirRel,
327
+ contentDirAbs: path.resolve(contentDirRel),
328
+ dirPrefix: buildDirPrefix(contentDirRel),
329
+ };
330
+ }
331
+ function resolveLocaleFromRequest(request) {
332
+ if (!i18n)
333
+ return undefined;
334
+ const url = new URL(request.url);
335
+ const direct = resolveDocsLocale(url.searchParams, i18n);
336
+ if (direct)
337
+ return direct;
338
+ const referrer = request.headers.get("referer") ?? request.headers.get("referrer");
339
+ if (referrer) {
340
+ try {
341
+ const refUrl = new URL(referrer);
342
+ const fromRef = resolveDocsLocale(refUrl.searchParams, i18n);
343
+ if (fromRef)
344
+ return fromRef;
345
+ }
346
+ catch {
347
+ // ignore
348
+ }
349
+ }
350
+ return i18n.defaultLocale;
351
+ }
352
+ function resolveContextFromRequest(request) {
353
+ const locale = resolveLocaleFromRequest(request);
354
+ const url = new URL(request.url);
355
+ const pathnameParam = url.searchParams.get("pathname");
356
+ const referrer = request.headers.get("referer") ?? request.headers.get("referrer");
357
+ const refPath = referrer ? new URL(referrer).pathname : undefined;
358
+ const pathname = pathnameParam ?? refPath ?? `/${entry}`;
359
+ return resolveContextFromPath(pathname, locale);
360
+ }
294
361
  // ─── Unified load (tree + page content in one call) ────────
295
362
  async function load(event) {
363
+ const locale = resolveDocsLocale(event.url.searchParams, i18n) ?? i18n?.defaultLocale;
364
+ const ctx = resolveContextFromPath(event.url.pathname, locale);
296
365
  const tree = preloaded
297
- ? navTreeFromMap(preloaded, dirPrefix, entry, ordering)
298
- : loadDocsNavTree(contentDir, entry, ordering);
366
+ ? navTreeFromMap(preloaded, ctx.dirPrefix, entry, ordering)
367
+ : loadDocsNavTree(ctx.contentDirAbs, entry, ordering);
299
368
  const flatPages = flattenNavTree(tree);
300
- const urlPrefix = new RegExp(`^/${entry}/?`);
301
- const slug = event.url.pathname.replace(urlPrefix, "");
369
+ const slug = ctx.slug;
302
370
  const isIndex = slug === "";
303
371
  let raw;
304
372
  let relPath;
305
373
  let lastModified;
306
374
  if (preloaded) {
307
- const result = findPageInMap(preloaded, dirPrefix, slug);
375
+ const result = findPageInMap(preloaded, ctx.dirPrefix, slug);
308
376
  if (!result) {
309
377
  const err = new Error(`Page not found: /${entry}/${slug}`);
310
378
  err.status = 404;
@@ -323,7 +391,7 @@ export function createDocsServer(config = {}) {
323
391
  relPath = "";
324
392
  if (isIndex) {
325
393
  for (const name of ["page.md", "page.mdx", "index.md"]) {
326
- const candidate = path.join(contentDir, name);
394
+ const candidate = path.join(ctx.contentDirAbs, name);
327
395
  if (fs.existsSync(candidate)) {
328
396
  filePath = candidate;
329
397
  relPath = name;
@@ -333,17 +401,17 @@ export function createDocsServer(config = {}) {
333
401
  }
334
402
  else {
335
403
  const candidates = [
336
- path.join(contentDir, slug, "page.md"),
337
- path.join(contentDir, slug, "page.mdx"),
338
- path.join(contentDir, slug, "index.md"),
339
- path.join(contentDir, slug, "index.svx"),
340
- path.join(contentDir, `${slug}.md`),
341
- path.join(contentDir, `${slug}.svx`),
404
+ path.join(ctx.contentDirAbs, slug, "page.md"),
405
+ path.join(ctx.contentDirAbs, slug, "page.mdx"),
406
+ path.join(ctx.contentDirAbs, slug, "index.md"),
407
+ path.join(ctx.contentDirAbs, slug, "index.svx"),
408
+ path.join(ctx.contentDirAbs, `${slug}.md`),
409
+ path.join(ctx.contentDirAbs, `${slug}.svx`),
342
410
  ];
343
411
  for (const candidate of candidates) {
344
412
  if (fs.existsSync(candidate)) {
345
413
  filePath = candidate;
346
- relPath = path.relative(contentDir, candidate);
414
+ relPath = path.relative(ctx.contentDirAbs, candidate);
347
415
  break;
348
416
  }
349
417
  }
@@ -369,7 +437,9 @@ export function createDocsServer(config = {}) {
369
437
  const nextPage = currentIndex < flatPages.length - 1 ? flatPages[currentIndex + 1] : null;
370
438
  let editOnGithub;
371
439
  if (githubRepo && githubContentPath) {
372
- editOnGithub = `${githubRepo}/blob/${githubBranch}/${githubContentPath}/${relPath}`;
440
+ const trimmed = githubContentPath.replace(/\/+$/, "");
441
+ const localePrefix = ctx.locale ? `${ctx.locale}/` : "";
442
+ editOnGithub = `${githubRepo}/blob/${githubBranch}/${trimmed}/${localePrefix}${relPath}`;
373
443
  }
374
444
  const fallbackTitle = isIndex
375
445
  ? "Documentation"
@@ -380,6 +450,8 @@ export function createDocsServer(config = {}) {
380
450
  title: data.title ?? fallbackTitle,
381
451
  description: data.description,
382
452
  html,
453
+ entry,
454
+ locale: ctx.locale,
383
455
  ...(isIndex ? {} : { slug }),
384
456
  previousPage,
385
457
  nextPage,
@@ -388,17 +460,20 @@ export function createDocsServer(config = {}) {
388
460
  };
389
461
  }
390
462
  // ─── Search index ──────────────────────────────────────────
391
- let searchIndex = null;
392
- function getSearchIndex() {
393
- if (!searchIndex) {
394
- searchIndex = preloaded
395
- ? searchIndexFromMap(preloaded, dirPrefix, entry)
396
- : loadDocsContent(contentDir, entry);
397
- }
398
- return searchIndex;
463
+ const searchIndexByEntry = new Map();
464
+ function getSearchIndex(ctx) {
465
+ const key = ctx.locale ?? "__default__";
466
+ const cached = searchIndexByEntry.get(key);
467
+ if (cached)
468
+ return cached;
469
+ const index = preloaded
470
+ ? searchIndexFromMap(preloaded, ctx.dirPrefix, entry)
471
+ : loadDocsContent(ctx.contentDirAbs, entry);
472
+ searchIndexByEntry.set(key, index);
473
+ return index;
399
474
  }
400
- function searchByQuery(query) {
401
- const index = getSearchIndex();
475
+ function searchByQuery(query, ctx) {
476
+ const index = getSearchIndex(ctx);
402
477
  return index
403
478
  .map((page) => {
404
479
  const titleMatch = page.title.toLowerCase().includes(query) ? 10 : 0;
@@ -420,36 +495,42 @@ export function createDocsServer(config = {}) {
420
495
  const llmsBaseUrl = typeof llmsTxtConfig === "object" ? (llmsTxtConfig.baseUrl ?? "") : "";
421
496
  const llmsTitle = typeof llmsTxtConfig === "object" ? (llmsTxtConfig.siteTitle ?? llmsSiteTitle) : llmsSiteTitle;
422
497
  const llmsDesc = typeof llmsTxtConfig === "object" ? llmsTxtConfig.siteDescription : undefined;
423
- function buildLlmsTxt(full) {
424
- const pages = getSearchIndex();
425
- let out = `# ${llmsTitle}\n\n`;
426
- if (llmsDesc)
427
- out += `> ${llmsDesc}\n\n`;
428
- if (full) {
429
- for (const page of pages) {
430
- out += `## ${page.title}\n\n`;
431
- out += `URL: ${llmsBaseUrl}${page.url}\n\n`;
432
- if (page.description)
433
- out += `${page.description}\n\n`;
434
- out += `${page.content}\n\n---\n\n`;
435
- }
498
+ const llmsCache = new Map();
499
+ function getLlmsContent(ctx) {
500
+ const key = ctx.locale ?? "__default__";
501
+ const cached = llmsCache.get(key);
502
+ if (cached)
503
+ return cached;
504
+ const pages = getSearchIndex(ctx);
505
+ let llmsTxt = `# ${llmsTitle}\n\n`;
506
+ let llmsFullTxt = `# ${llmsTitle}\n\n`;
507
+ if (llmsDesc) {
508
+ llmsTxt += `> ${llmsDesc}\n\n`;
509
+ llmsFullTxt += `> ${llmsDesc}\n\n`;
436
510
  }
437
- else {
438
- out += `## Pages\n\n`;
439
- for (const page of pages) {
440
- out += `- [${page.title}](${llmsBaseUrl}${page.url})`;
441
- if (page.description)
442
- out += `: ${page.description}`;
443
- out += `\n`;
444
- }
511
+ llmsTxt += `## Pages\n\n`;
512
+ for (const page of pages) {
513
+ llmsTxt += `- [${page.title}](${llmsBaseUrl}${page.url})`;
514
+ if (page.description)
515
+ llmsTxt += `: ${page.description}`;
516
+ llmsTxt += `\n`;
517
+ llmsFullTxt += `## ${page.title}\n\n`;
518
+ llmsFullTxt += `URL: ${llmsBaseUrl}${page.url}\n\n`;
519
+ if (page.description)
520
+ llmsFullTxt += `${page.description}\n\n`;
521
+ llmsFullTxt += `${page.content}\n\n---\n\n`;
445
522
  }
446
- return out;
523
+ const next = { llmsTxt, llmsFullTxt };
524
+ llmsCache.set(key, next);
525
+ return next;
447
526
  }
448
527
  // ─── GET /api/docs?query=… | ?format=llms | ?format=llms-full ──
449
528
  function GET(event) {
529
+ const ctx = resolveContextFromRequest(event.request);
450
530
  const format = event.url.searchParams.get("format");
451
531
  if (format === "llms" || format === "llms-full") {
452
- return new Response(buildLlmsTxt(format === "llms-full"), {
532
+ const llmsContent = getLlmsContent(ctx);
533
+ return new Response(format === "llms-full" ? llmsContent.llmsFullTxt : llmsContent.llmsTxt, {
453
534
  headers: {
454
535
  "Content-Type": "text/plain; charset=utf-8",
455
536
  "Cache-Control": "public, max-age=3600",
@@ -462,7 +543,7 @@ export function createDocsServer(config = {}) {
462
543
  headers: { "Content-Type": "application/json" },
463
544
  });
464
545
  }
465
- const results = searchByQuery(query)
546
+ const results = searchByQuery(query, ctx)
466
547
  .slice(0, 10)
467
548
  .map(({ title, url, description }) => ({
468
549
  content: title,
@@ -509,6 +590,7 @@ export function createDocsServer(config = {}) {
509
590
  error: "AI is enabled but no API key was found. Set `apiKey` in your docs config `ai` section or add OPENAI_API_KEY to your environment.",
510
591
  }), { status: 500, headers: { "Content-Type": "application/json" } });
511
592
  }
593
+ const ctx = resolveContextFromRequest(event.request);
512
594
  let body;
513
595
  try {
514
596
  body = await event.request.json();
@@ -528,7 +610,7 @@ export function createDocsServer(config = {}) {
528
610
  });
529
611
  }
530
612
  const maxResults = aiConfig.maxResults ?? 5;
531
- const scored = searchByQuery(lastUserMessage.content.toLowerCase()).slice(0, maxResults);
613
+ const scored = searchByQuery(lastUserMessage.content.toLowerCase(), ctx).slice(0, maxResults);
532
614
  const contextParts = scored.map((doc) => `## ${doc.title}\nURL: ${doc.url}\n${doc.description ? `Description: ${doc.description}\n` : ""}\n${doc.content}`);
533
615
  const context = contextParts.join("\n\n---\n\n");
534
616
  const systemPrompt = aiConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/svelte",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "SvelteKit adapter for @farming-labs/docs — content loading and navigation utilities",
5
5
  "keywords": [
6
6
  "docs",
@@ -50,7 +50,7 @@
50
50
  "devDependencies": {
51
51
  "@types/node": "^22.10.0",
52
52
  "typescript": "^5.9.3",
53
- "@farming-labs/docs": "0.0.29"
53
+ "@farming-labs/docs": "0.0.31"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@farming-labs/docs": "*"