specra 0.2.12 → 0.2.14

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.
@@ -0,0 +1 @@
1
+ export function rehypeBasePath(options?: {}): (tree: any) => void;
@@ -19,6 +19,7 @@ import rehypeKatex from 'rehype-katex'
19
19
  import rehypeRaw from 'rehype-raw'
20
20
  import fs from 'fs'
21
21
  import path from 'path'
22
+ import { rehypeBasePath } from '../dist/rehype-base-path.js'
22
23
 
23
24
  /**
24
25
  * Get mdsvex preprocessor config with all Specra remark/rehype plugins
@@ -44,6 +45,30 @@ export function specraMdsvexConfig(options = {}) {
44
45
  }
45
46
  }
46
47
 
48
+ /**
49
+ * Read the deployment basePath from specra.config.json if present.
50
+ * Falls back to the BASE_PATH environment variable, then empty string.
51
+ */
52
+ function resolveBasePath(configPath = path.join(process.cwd(), 'specra.config.json')) {
53
+ // Environment variable takes priority
54
+ if (process.env.BASE_PATH) return process.env.BASE_PATH
55
+
56
+ try {
57
+ if (fs.existsSync(configPath)) {
58
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'))
59
+ if (raw.deployment?.basePath) {
60
+ const bp = raw.deployment.basePath
61
+ // If custom domain is set, ignore basePath
62
+ if (raw.deployment?.customDomain) return ''
63
+ return bp.startsWith('/') ? bp : `/${bp}`
64
+ }
65
+ }
66
+ } catch {
67
+ // Ignore parse errors
68
+ }
69
+ return ''
70
+ }
71
+
47
72
  /**
48
73
  * Scan the docs/ directory and return prerender entries for all version root pages.
49
74
  * This ensures adapter-static discovers and prerenders every version, not just the active one.
@@ -66,20 +91,36 @@ function discoverVersionEntries(docsDir = path.join(process.cwd(), 'docs')) {
66
91
  }
67
92
 
68
93
  /**
69
- * Create a full SvelteKit config with Specra defaults
94
+ * Create a full SvelteKit config with Specra defaults.
95
+ * Automatically reads deployment.basePath from specra.config.json
96
+ * for GitHub Pages deployments.
70
97
  */
71
98
  export function specraConfig(options = {}) {
72
99
  const { vitePreprocess } = options.vitePreprocess || {}
73
100
  const userPrerender = options.kit?.prerender || {}
101
+ const basePath = options.kit?.paths?.base ?? resolveBasePath()
102
+
103
+ // Inject base path rehype plugin into mdsvex if basePath is set
104
+ const mdsvexOptions = options.mdsvex || {}
105
+ if (basePath) {
106
+ mdsvexOptions.rehypePlugins = [
107
+ ...(mdsvexOptions.rehypePlugins || []),
108
+ [rehypeBasePath, { basePath }],
109
+ ]
110
+ }
74
111
 
75
112
  return {
76
113
  extensions: ['.svelte', '.md', '.svx', '.mdx'],
77
114
  preprocess: [
78
115
  ...(vitePreprocess ? [vitePreprocess()] : []),
79
- mdsvex(specraMdsvexConfig(options.mdsvex || {}))
116
+ mdsvex(specraMdsvexConfig(mdsvexOptions))
80
117
  ],
81
118
  kit: {
82
119
  ...options.kit,
120
+ paths: {
121
+ ...options.kit?.paths,
122
+ base: basePath,
123
+ },
83
124
  prerender: {
84
125
  handleHttpError: 'warn',
85
126
  handleMissingId: 'warn',
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { ChevronRight } from 'lucide-svelte';
3
+ import { base } from '$app/paths';
3
4
  import { getConfigContext } from '../../stores/config.js';
4
5
 
5
6
  interface Props {
@@ -13,8 +14,8 @@
13
14
 
14
15
  let docsBase = $derived(
15
16
  product && product !== '_default_'
16
- ? `/docs/${product}/${version}`
17
- : `/docs/${version}`
17
+ ? `${base}/docs/${product}/${version}`
18
+ : `${base}/docs/${version}`
18
19
  );
19
20
 
20
21
  const configStore = getConfigContext();
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { FileText, ArrowRight } from 'lucide-svelte';
3
+ import { base } from '$app/paths';
3
4
  import type { SpecraConfig } from '../../config.types.js';
4
5
  import type { Snippet } from 'svelte';
5
6
 
@@ -54,8 +55,8 @@
54
55
 
55
56
  const baseUrl = $derived(
56
57
  product && product !== '_default_'
57
- ? `/docs/${product}`
58
- : '/docs'
58
+ ? `${base}/docs/${product}`
59
+ : `${base}/docs`
59
60
  );
60
61
 
61
62
  // Note: We always use '/docs' as the base for non-product routes.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { ChevronLeft, ChevronRight } from 'lucide-svelte';
3
+ import { base } from '$app/paths';
3
4
 
4
5
  interface NavDoc {
5
6
  title: string;
@@ -17,8 +18,8 @@
17
18
 
18
19
  let docsBase = $derived(
19
20
  product && product !== '_default_'
20
- ? `/docs/${product}/${version}`
21
- : `/docs/${version}`
21
+ ? `${base}/docs/${product}/${version}`
22
+ : `${base}/docs/${version}`
22
23
  );
23
24
  </script>
24
25
 
@@ -1,7 +1,16 @@
1
1
  <script lang="ts">
2
2
  import type { SpecraConfig } from '../../config.types.js';
3
+ import { base } from '$app/paths';
3
4
  import Logo from './Logo.svelte';
4
5
 
6
+ /** Prefix internal links with base path */
7
+ function resolveHref(href: string): string {
8
+ if (href.startsWith('/') && !href.startsWith('//')) {
9
+ return `${base}${href}`;
10
+ }
11
+ return href;
12
+ }
13
+
5
14
  interface Props {
6
15
  config: SpecraConfig;
7
16
  }
@@ -25,7 +34,7 @@
25
34
  {#each column.items as item, itemIdx (itemIdx)}
26
35
  <li>
27
36
  <a
28
- href={item.href}
37
+ href={resolveHref(item.href)}
29
38
  class="text-sm text-muted-foreground hover:text-foreground transition-colors"
30
39
  >
31
40
  {item.label}
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Search, Menu, Github, Twitter, MessageCircle } from 'lucide-svelte';
3
+ import { base } from '$app/paths';
3
4
  import { getConfigContext } from '../../stores/config.js';
4
5
  import { sidebarStore } from '../../stores/sidebar.js';
5
6
  import VersionSwitcher from './VersionSwitcher.svelte';
@@ -110,7 +111,7 @@
110
111
  >
111
112
  <Menu class="h-5 w-5" />
112
113
  </button>
113
- <a href="/" class="flex items-center gap-2">
114
+ <a href="{base}/" class="flex items-center gap-2">
114
115
  {#if !config.site.hideLogo}
115
116
  {#if config.site.logo}
116
117
  <Logo logo={config.site.logo} alt={config.site.title} className="w-18 object-contain" />
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { FileQuestion, Home, ArrowLeft } from 'lucide-svelte';
3
+ import { base } from '$app/paths';
3
4
 
4
5
  interface Props {
5
6
  version?: string;
@@ -11,10 +12,10 @@
11
12
  /** URL prefix: /docs/{product}/{version} for named products, /docs/{version} for default */
12
13
  const homeLink = $derived(
13
14
  product && product !== '_default_' && version
14
- ? `/docs/${product}/${version}`
15
+ ? `${base}/docs/${product}/${version}`
15
16
  : version
16
- ? `/docs/${version}`
17
- : '/'
17
+ ? `${base}/docs/${version}`
18
+ : `${base}/`
18
19
  );
19
20
  </script>
20
21
 
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { ChevronDown, Check } from 'lucide-svelte';
3
3
  import { goto } from '$app/navigation';
4
+ import { base } from '$app/paths';
4
5
  import { browser } from '$app/environment';
5
6
  import Icon from './Icon.svelte';
6
7
 
@@ -107,9 +108,9 @@
107
108
 
108
109
  const version = product.activeVersion || 'v1.0.0';
109
110
  if (product.isDefault) {
110
- goto(`/docs/${version}`);
111
+ goto(`${base}/docs/${version}`);
111
112
  } else {
112
- goto(`/docs/${product.slug}/${version}`);
113
+ goto(`${base}/docs/${product.slug}/${version}`);
113
114
  }
114
115
  }
115
116
 
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { page } from '$app/stores';
3
+ import { base } from '$app/paths';
3
4
  import { ChevronRight, ChevronDown, Lock } from 'lucide-svelte';
4
5
  import type { SpecraConfig } from '../../config.types.js';
5
6
  import Icon from './Icon.svelte';
@@ -50,11 +51,11 @@
50
51
 
51
52
  let { docs = [], version, product, onLinkClick, config, activeTabGroup }: Props = $props();
52
53
 
53
- /** URL prefix: /docs/{product}/{version} for named products, /docs/{version} for default */
54
+ /** URL prefix: {base}/docs/{product}/{version} for named products, {base}/docs/{version} for default */
54
55
  let docsBase = $derived(
55
56
  product && product !== '_default_'
56
- ? `/docs/${product}/${version}`
57
- : `/docs/${version}`
57
+ ? `${base}/docs/${product}/${version}`
58
+ : `${base}/docs/${version}`
58
59
  );
59
60
 
60
61
  const STORAGE_KEY = 'specra-sidebar-collapsed';
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { ChevronDown } from 'lucide-svelte';
3
3
  import { goto } from '$app/navigation';
4
+ import { base } from '$app/paths';
4
5
  import type { TabGroup, SpecraConfig } from '../../config.types.js';
5
6
  import Icon from './Icon.svelte';
6
7
 
@@ -32,8 +33,8 @@
32
33
 
33
34
  let docsBase = $derived(
34
35
  product && product !== '_default_' && version
35
- ? `/docs/${product}/${version}`
36
- : version ? `/docs/${version}` : '/docs'
36
+ ? `${base}/docs/${product}/${version}`
37
+ : version ? `${base}/docs/${version}` : `${base}/docs`
37
38
  );
38
39
 
39
40
  let dropdownOpen = $state(false);
package/dist/mdx.js CHANGED
@@ -2,6 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import matter from "gray-matter";
4
4
  import yaml from "js-yaml";
5
+ import { rehypeBasePath } from "./rehype-base-path.js";
5
6
  import { unified } from "unified";
6
7
  import remarkParse from "remark-parse";
7
8
  import remarkGfm from "remark-gfm";
@@ -395,15 +396,36 @@ function parseJsxExpression(expr) {
395
396
  /**
396
397
  * Process markdown content to HTML using remark/rehype pipeline.
397
398
  */
399
+ function resolveDeploymentBasePath() {
400
+ if (process.env.BASE_PATH)
401
+ return process.env.BASE_PATH;
402
+ try {
403
+ const configPath = path.join(process.cwd(), 'specra.config.json');
404
+ if (fs.existsSync(configPath)) {
405
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
406
+ if (raw.deployment?.basePath && !raw.deployment?.customDomain) {
407
+ const bp = raw.deployment.basePath;
408
+ return bp.startsWith('/') ? bp : `/${bp}`;
409
+ }
410
+ }
411
+ }
412
+ catch { /* ignore */ }
413
+ return '';
414
+ }
398
415
  async function processMarkdownToHtml(markdown) {
399
- const result = await unified()
416
+ const basePath = resolveDeploymentBasePath();
417
+ const processor = unified()
400
418
  .use(remarkParse)
401
419
  .use(remarkGfm)
402
420
  .use(remarkMath)
403
421
  .use(remarkRehype, { allowDangerousHtml: true })
404
422
  .use(rehypeRaw)
405
423
  .use(rehypeSlug)
406
- .use(rehypeKatex)
424
+ .use(rehypeKatex);
425
+ if (basePath) {
426
+ processor.use(rehypeBasePath, { basePath });
427
+ }
428
+ const result = await processor
407
429
  .use(rehypeStringify)
408
430
  .process(markdown);
409
431
  return String(result);
@@ -1046,6 +1068,7 @@ async function processMarkdownToMdxNodes(markdown) {
1046
1068
  const dedented = dedentComponentChildren(preprocessed);
1047
1069
  // Ensure component block integrity in the markdown
1048
1070
  const normalized = ensureComponentBlockIntegrity(dedented);
1071
+ const basePath = resolveDeploymentBasePath();
1049
1072
  const processor = unified()
1050
1073
  .use(remarkParse)
1051
1074
  .use(remarkGfm)
@@ -1054,6 +1077,9 @@ async function processMarkdownToMdxNodes(markdown) {
1054
1077
  .use(rehypeRaw)
1055
1078
  .use(rehypeSlug)
1056
1079
  .use(rehypeKatex);
1080
+ if (basePath) {
1081
+ processor.use(rehypeBasePath, { basePath });
1082
+ }
1057
1083
  const mdast = processor.parse(normalized);
1058
1084
  const hast = await processor.run(mdast);
1059
1085
  // The hast root has children - process them into MdxNodes
@@ -0,0 +1,6 @@
1
+ import type { Root } from 'hast';
2
+ interface Options {
3
+ basePath?: string;
4
+ }
5
+ export declare function rehypeBasePath(options?: Options): (tree: Root) => void;
6
+ export {};
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Rehype plugin that prefixes internal absolute links with a base path.
3
+ * Internal links start with "/" and don't start with "http" or "//".
4
+ *
5
+ * Used for GitHub Pages deployments where the site lives under a subpath.
6
+ */
7
+ import { visit } from 'unist-util-visit';
8
+ export function rehypeBasePath(options = {}) {
9
+ const { basePath = '' } = options;
10
+ if (!basePath)
11
+ return () => { };
12
+ const cleanBase = basePath.replace(/\/$/, '');
13
+ return (tree) => {
14
+ visit(tree, 'element', (node) => {
15
+ if (node.tagName === 'a' && node.properties?.href) {
16
+ const href = node.properties.href;
17
+ if (typeof href === 'string' && href.startsWith('/') && !href.startsWith('//') && !href.startsWith(cleanBase + '/')) {
18
+ node.properties.href = cleanBase + href;
19
+ }
20
+ }
21
+ if (node.tagName === 'img' && node.properties?.src) {
22
+ const src = node.properties.src;
23
+ if (typeof src === 'string' && src.startsWith('/') && !src.startsWith('//') && !src.startsWith(cleanBase + '/')) {
24
+ node.properties.src = cleanBase + src;
25
+ }
26
+ }
27
+ });
28
+ };
29
+ }
package/dist/utils.d.ts CHANGED
@@ -1,13 +1,11 @@
1
1
  import { type ClassValue } from 'clsx';
2
2
  export declare function cn(...inputs: ClassValue[]): string;
3
3
  /**
4
- * Get the correct asset path based on deployment configuration
5
- * Handles different deployment scenarios:
6
- * - Vercel/Node.js hosting (standalone build): No basePath needed
7
- * - GitHub Pages without custom domain: Uses basePath from config
8
- * - Static hosting with custom domain: No basePath needed
4
+ * Get the correct asset path based on deployment configuration.
5
+ * Uses SvelteKit's base path (from kit.paths.base) which is resolved
6
+ * from deployment.basePath in specra.config.json or the BASE_PATH env var.
9
7
  *
10
- * @param path - The asset path (can start with or without '/')
11
- * @returns The properly formatted asset path
8
+ * @param assetPath - The asset path (can start with or without '/')
9
+ * @returns The properly formatted asset path with base prefix
12
10
  */
13
- export declare function getAssetPath(path: string): string;
11
+ export declare function getAssetPath(assetPath: string): string;
package/dist/utils.js CHANGED
@@ -4,27 +4,20 @@ export function cn(...inputs) {
4
4
  return twMerge(clsx(inputs));
5
5
  }
6
6
  /**
7
- * Get the correct asset path based on deployment configuration
8
- * Handles different deployment scenarios:
9
- * - Vercel/Node.js hosting (standalone build): No basePath needed
10
- * - GitHub Pages without custom domain: Uses basePath from config
11
- * - Static hosting with custom domain: No basePath needed
7
+ * Get the correct asset path based on deployment configuration.
8
+ * Uses SvelteKit's base path (from kit.paths.base) which is resolved
9
+ * from deployment.basePath in specra.config.json or the BASE_PATH env var.
12
10
  *
13
- * @param path - The asset path (can start with or without '/')
14
- * @returns The properly formatted asset path
11
+ * @param assetPath - The asset path (can start with or without '/')
12
+ * @returns The properly formatted asset path with base prefix
15
13
  */
16
- export function getAssetPath(path) {
17
- // Get basePath from Next.js config (set during build for static exports)
18
- const basePath = process.env.NEXT_PUBLIC_BASE_PATH || process.env.__NEXT_ROUTER_BASEPATH || '';
19
- // Normalize the input path: ensure it starts with '/'
20
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
21
- // If we have a basePath (GitHub Pages without custom domain), prepend it
14
+ export function getAssetPath(assetPath) {
15
+ const basePath = process.env.BASE_PATH || '';
16
+ const normalizedPath = assetPath.startsWith('/') ? assetPath : `/${assetPath}`;
22
17
  if (basePath) {
23
- // Normalize basePath: remove trailing slash, ensure leading slash
24
18
  const normalizedBase = basePath.startsWith('/') ? basePath : `/${basePath}`;
25
19
  const cleanBase = normalizedBase.replace(/\/$/, '');
26
20
  return `${cleanBase}${normalizedPath}`;
27
21
  }
28
- // Default: return the normalized path (works for Vercel, custom domains, and dev)
29
22
  return normalizedPath;
30
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specra",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "A modern documentation library for SvelteKit with built-in versioning, API reference generation, full-text search, and MDX support",
5
5
  "svelte": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",