@rahuldshetty/inscribe 0.0.2 → 0.0.3

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.
@@ -14,6 +14,14 @@ export interface BuildOptions {
14
14
  env: string;
15
15
  }
16
16
 
17
+ const normalizeUrl = (url: string, config: InscribeConfig) => {
18
+ if (url.startsWith('http')) return url;
19
+ let base = config.base_url || '/';
20
+ if (!base.endsWith('/')) base += '/';
21
+ const suffix = url.startsWith('/') ? url.slice(1) : url;
22
+ return base + suffix;
23
+ }
24
+
17
25
  const buildSection = async (
18
26
  type: 'blog' | 'doc',
19
27
  sourceDir: string,
@@ -102,7 +110,7 @@ export async function build(options: BuildOptions) {
102
110
  if (isRelease) blogIndex = await minifyHtml(blogIndex);
103
111
  await fs.writeFile(path.join(outputDir, "blogs", "index.html"), blogIndex);
104
112
 
105
- if (!redirectUrl) redirectUrl = "/blogs/";
113
+ if (!redirectUrl) redirectUrl = normalizeUrl("/blogs/", inscribe);
106
114
  }
107
115
 
108
116
  // Build docs
@@ -120,11 +128,11 @@ export async function build(options: BuildOptions) {
120
128
  if (docs.length > 0) {
121
129
  const firstLevelDoc = docs.find(p => !((p as any).relativePath).includes('/') && !((p as any).relativePath).includes('\\'));
122
130
  const firstDocSlug = (firstLevelDoc || docs[0]).metadata.slug;
123
- const redirectHtml = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=/doc/${firstDocSlug}"></head><body>Redirecting...</body></html>`;
131
+ const redirectHtml = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${normalizeUrl(`/doc/${firstDocSlug}`, inscribe)}"></head><body>Redirecting...</body></html>`;
124
132
  await fs.writeFile(path.join(outputDir, "docs", "index.html"), redirectHtml);
125
133
  }
126
134
 
127
- if (!redirectUrl) redirectUrl = "/docs/";
135
+ if (!redirectUrl) redirectUrl = normalizeUrl("/docs/", inscribe);
128
136
  }
129
137
 
130
138
  // Generate index.html
@@ -26,16 +26,29 @@ export const parseBlogPost = async (filePath: string) => {
26
26
  * 1. User project layouts
27
27
  * 2. CLI built-in layouts
28
28
  */
29
- const getRenderer = (sourceDir: string) => {
29
+ const getRenderer = (sourceDir: string, config: InscribeConfig) => {
30
30
  const userLayouts = path.resolve(sourceDir, "layouts");
31
31
  const builtInLayouts = path.resolve(__dirname, "../../template/layouts");
32
32
 
33
33
  const searchPaths = [userLayouts, builtInLayouts];
34
34
 
35
- return new nunjucks.Environment(
35
+ const env = new nunjucks.Environment(
36
36
  new nunjucks.FileSystemLoader(searchPaths),
37
37
  { autoescape: true }
38
38
  );
39
+
40
+ env.addFilter('url', (urlPath: string) => {
41
+ if (!urlPath) return urlPath;
42
+ if (urlPath.startsWith('http') || urlPath.startsWith('//') || urlPath.startsWith('data:')) return urlPath;
43
+
44
+ let base = config.base_url || '/';
45
+ if (!base.endsWith('/')) base += '/';
46
+
47
+ const pathSuffix = urlPath.startsWith('/') ? urlPath.slice(1) : urlPath;
48
+ return base + pathSuffix;
49
+ });
50
+
51
+ return env;
39
52
  };
40
53
 
41
54
  import { FolderMetadata } from "../schemas/folder";
@@ -127,8 +140,8 @@ export const renderSectionPage = async (
127
140
  navState: NavState,
128
141
  isDev: boolean = false
129
142
  ) => {
130
- const html = await markdown2HTML(post.markdown, post.isMDX);
131
- const env = getRenderer(sourceDir);
143
+ const html = await markdown2HTML(post.markdown, post.isMDX, inscribe);
144
+ const env = getRenderer(sourceDir, inscribe);
132
145
  const themeCSS = resolveThemeCSS(inscribe.theme ?? 'default', sourceDir);
133
146
 
134
147
  const template = type === 'blog' ? "blog.njk" : "doc.njk";
@@ -160,7 +173,7 @@ export const renderSectionIndexPage = (
160
173
  navState: NavState,
161
174
  isDev: boolean = false
162
175
  ) => {
163
- const env = getRenderer(sourceDir);
176
+ const env = getRenderer(sourceDir, inscribe);
164
177
  const themeCSS = resolveThemeCSS(inscribe.theme ?? 'default', sourceDir);
165
178
 
166
179
  const template = type === 'blog' ? "blog_index.njk" : "doc_index.njk";
@@ -187,7 +200,7 @@ export const renderHomePage = (
187
200
  navState: NavState,
188
201
  isDev: boolean = false
189
202
  ) => {
190
- const env = getRenderer(sourceDir);
203
+ const env = getRenderer(sourceDir, inscribe);
191
204
  const themeCSS = resolveThemeCSS(inscribe.theme ?? 'default', sourceDir);
192
205
 
193
206
  return env.render("home.njk", {
@@ -9,6 +9,7 @@ export const InscribeSchema = z.object({
9
9
  blog_path: z.string().default('blog').optional(),
10
10
  doc_path: z.string().default('docs').optional(),
11
11
  show_doc_nav: z.preprocess((val) => (typeof val === "string" ? val.toLowerCase() === "true" : val), z.boolean()).default(true).optional(),
12
+ base_url: z.string().default('/').optional(),
12
13
  })
13
14
 
14
15
  export type InscribeConfig = z.infer<typeof InscribeSchema>;
@@ -46,7 +46,33 @@ export function parseFrontMatter(content: string) {
46
46
  }
47
47
  }
48
48
 
49
- export const markdown2HTML = async (content: string, isMDX: boolean = false) => {
49
+ import { InscribeConfig } from "../schemas/inscribe";
50
+
51
+ export const markdown2HTML = async (content: string, isMDX: boolean = false, config?: InscribeConfig) => {
52
+ const base = config?.base_url || '/';
53
+ const normalizedBase = base.endsWith('/') ? base : base + '/';
54
+
55
+ const renderer = new marked.Renderer();
56
+
57
+ // Custom renderer to handle base_url for links and images
58
+ renderer.link = ({ href, title, text }: any) => {
59
+ let finalHref = href;
60
+ if (href && !href.startsWith('http') && !href.startsWith('//') && !href.startsWith('#')) {
61
+ const pathSuffix = href.startsWith('/') ? href.slice(1) : href;
62
+ finalHref = normalizedBase + pathSuffix;
63
+ }
64
+ return `<a href="${finalHref}"${title ? ` title="${title}"` : ""}>${text}</a>`;
65
+ };
66
+
67
+ renderer.image = ({ href, title, text }: any) => {
68
+ let finalHref = href;
69
+ if (href && !href.startsWith('http') && !href.startsWith('//') && !href.startsWith('data:')) {
70
+ const pathSuffix = href.startsWith('/') ? href.slice(1) : href;
71
+ finalHref = normalizedBase + pathSuffix;
72
+ }
73
+ return `<img src="${finalHref}" alt="${text || ""}"${title ? ` title="${title}"` : ""}>`;
74
+ };
75
+
50
76
  if (isMDX) {
51
77
  try {
52
78
  // compile MDX -> JS
@@ -64,9 +90,9 @@ export const markdown2HTML = async (content: string, isMDX: boolean = false) =>
64
90
  return html;
65
91
  } catch (e) {
66
92
  console.error("MDX compilation error:", e);
67
- return await marked(content);
93
+ return await marked(content, { renderer });
68
94
  }
69
95
  }
70
- const html = await marked(content)
96
+ const html = await marked(content, { renderer })
71
97
  return html;
72
98
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rahuldshetty/inscribe",
3
3
  "private": false,
4
- "version": "0.0.2",
4
+ "version": "0.0.3",
5
5
  "description": "A minimalist Static Site Generator (SSG) for blogs and documentation.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -34,7 +34,8 @@
34
34
  "lint": "prettier --check . && eslint .",
35
35
  "format": "prettier --write .",
36
36
  "inscribe": "bun ./cli/index.ts",
37
- "build:cli": "bun build ./cli/index.ts --outdir ./dist --target bun"
37
+ "build:cli": "bun build ./cli/index.ts --outdir ./dist --target bun",
38
+ "doc:deploy": "bun inscribe build docs --output docs-dist && gh-pages -d docs-dist"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@eslint/compat": "^2.0.2",
@@ -60,6 +61,7 @@
60
61
  "clerc": "^1.3.1",
61
62
  "dompurify": "^3.3.2",
62
63
  "fs-extra": "^11.3.4",
64
+ "gh-pages": "^6.3.0",
63
65
  "html-minifier-terser": "^7.2.0",
64
66
  "jsdom": "^28.1.0",
65
67
  "marked": "^17.0.4",
@@ -3,3 +3,4 @@ theme: default
3
3
  show_home: true
4
4
  blog_path: 'blogs'
5
5
  doc_path: 'docs'
6
+ base_url: '/'
@@ -42,7 +42,7 @@
42
42
  {% if blog.metadata.cover %}
43
43
  <figure class="mb-10 -mx-6 sm:-mx-12">
44
44
  <img
45
- src="{{ blog.metadata.cover }}"
45
+ src="{{ blog.metadata.cover | url }}"
46
46
  alt="{{ blog.metadata.cover_alt or blog.metadata.title }}"
47
47
  class="w-full object-cover rounded-none sm:rounded-xl max-h-[480px]"
48
48
  />
@@ -65,7 +65,7 @@
65
65
  <hr class="my-12 border-[var(--color-border)]" />
66
66
 
67
67
  <footer class="flex items-center justify-between text-sm text-[var(--color-muted)]">
68
- <a href="/"
68
+ <a href="{{ '/blogs/' | url }}"
69
69
  class="hover:text-[var(--color-text)] transition-colors flex items-center gap-1.5 font-medium">
70
70
  <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
71
71
  <path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
@@ -17,7 +17,7 @@
17
17
  <ul class="divide-y divide-[var(--color-border)]">
18
18
  {% for blog in blogs %}
19
19
  <li>
20
- <a href="/blog/{{ blog.metadata.slug }}"
20
+ <a href="{{ ('/blog/' + blog.metadata.slug) | url }}"
21
21
  class="group flex items-start justify-between gap-6 py-7 hover:no-underline">
22
22
 
23
23
  {# Text content #}
@@ -55,7 +55,7 @@
55
55
  {% if blog.metadata.cover %}
56
56
  <div class="flex-shrink-0 w-24 h-16 sm:w-32 sm:h-20 rounded-lg overflow-hidden bg-[var(--color-tag-bg)]">
57
57
  <img
58
- src="{{ blog.metadata.cover }}"
58
+ src="{{ blog.metadata.cover | url }}"
59
59
  alt="{{ blog.metadata.title }}"
60
60
  class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
61
61
  />
@@ -27,7 +27,7 @@
27
27
  {{ renderNode(item.node, depth + 1, currentDirPath) }}
28
28
  {% else %}
29
29
  <div class="rounded-md hover:bg-[var(--color-tag-bg)] transition-all">
30
- <a href="/doc/{{ item.post.metadata.slug }}"
30
+ <a href="{{ ('/doc/' + item.post.metadata.slug) | url }}"
31
31
  style="padding-left: {{ (depth + 1) * 16 + 8 }}px"
32
32
  class="text-sm py-1.5 block transition-colors {% if item.post.metadata.slug == doc.metadata.slug %}text-[var(--color-text)] font-semibold border-l-2 border-[var(--color-text)] -ml-[2px]!{% else %}text-[var(--color-muted)] hover:text-[var(--color-text)]{% endif %}">
33
33
  {{ item.post.metadata.title }}
@@ -44,7 +44,7 @@
44
44
  {{ renderNode(item.node, depth, currentDirPath) }}
45
45
  {% else %}
46
46
  <div class="rounded-md hover:bg-[var(--color-tag-bg)] transition-all">
47
- <a href="/doc/{{ item.post.metadata.slug }}"
47
+ <a href="{{ ('/doc/' + item.post.metadata.slug) | url }}"
48
48
  style="padding-left: {{ depth * 16 + 8 }}px"
49
49
  class="text-sm py-1.5 block transition-colors {% if item.post.metadata.slug == doc.metadata.slug %}text-[var(--color-text)] font-semibold border-l-2 border-[var(--color-text)] -ml-[2px]!{% else %}text-[var(--color-muted)] hover:text-[var(--color-text)]{% endif %}">
50
50
  {{ item.post.metadata.title }}
@@ -69,7 +69,7 @@
69
69
  {% if blog.metadata.cover %}
70
70
  <figure class="mb-10 -mx-6 sm:-mx-12">
71
71
  <img
72
- src="{{ blog.metadata.cover }}"
72
+ src="{{ blog.metadata.cover | url }}"
73
73
  alt="{{ blog.metadata.cover_alt or blog.metadata.title }}"
74
74
  class="w-full object-cover rounded-none sm:rounded-xl max-h-[480px]"
75
75
  />
@@ -92,7 +92,7 @@
92
92
  <hr class="my-12 border-[var(--color-border)]" />
93
93
 
94
94
  <footer class="flex items-center justify-between text-sm text-[var(--color-muted)]">
95
- <a href="/docs/"
95
+ <a href="{{ '/docs/' | url }}"
96
96
  class="hover:text-[var(--color-text)] transition-colors flex items-center gap-1.5 font-medium">
97
97
  <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
98
98
  <path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
@@ -1,6 +1,6 @@
1
1
  {% extends "base.njk" %}
2
2
 
3
- {% block title %}{{ config.title }} — Blog{% endblock %}
3
+ {% block title %}{{ config.title }} — Documentation{% endblock %}
4
4
 
5
5
  {% block content %}
6
6
  <div class="min-h-screen bg-[var(--color-bg)] w-full max-w-[1400px] mx-auto flex flex-col md:flex-row md:px-8">
@@ -27,7 +27,7 @@
27
27
  {{ renderNode(item.node, depth + 1, currentDirPath) }}
28
28
  {% else %}
29
29
  <div class="rounded-md hover:bg-[var(--color-tag-bg)] transition-all">
30
- <a href="/doc/{{ item.post.metadata.slug }}"
30
+ <a href="{{ ('/doc/' + item.post.metadata.slug) | url }}"
31
31
  style="padding-left: {{ (depth + 1) * 16 + 8 }}px"
32
32
  class="text-sm py-1.5 block text-[var(--color-muted)] hover:text-[var(--color-text)] transition-colors">
33
33
  {{ item.post.metadata.title }}
@@ -44,7 +44,7 @@
44
44
  {{ renderNode(item.node, depth, currentDirPath) }}
45
45
  {% else %}
46
46
  <div class="rounded-md hover:bg-[var(--color-tag-bg)] transition-all">
47
- <a href="/doc/{{ item.post.metadata.slug }}"
47
+ <a href="{{ ('/doc/' + item.post.metadata.slug) | url }}"
48
48
  style="padding-left: {{ depth * 16 + 8 }}px"
49
49
  class="text-sm py-1.5 block text-[var(--color-muted)] hover:text-[var(--color-text)] transition-colors">
50
50
  {{ item.post.metadata.title }}
@@ -74,7 +74,7 @@
74
74
  <ul class="divide-y divide-[var(--color-border)]">
75
75
  {% for item in docs %}
76
76
  <li>
77
- <a href="/docs/{{ item.metadata.slug }}"
77
+ <a href="{{ ('/doc/' + item.metadata.slug) | url }}"
78
78
  class="group flex items-start justify-between gap-6 py-7 hover:no-underline">
79
79
 
80
80
  {# Text content #}
@@ -112,7 +112,7 @@
112
112
  {% if item.metadata.cover %}
113
113
  <div class="flex-shrink-0 w-24 h-16 sm:w-32 sm:h-20 rounded-lg overflow-hidden bg-[var(--color-tag-bg)]">
114
114
  <img
115
- src="{{ item.metadata.cover }}"
115
+ src="{{ item.metadata.cover | url }}"
116
116
  alt="{{ item.metadata.title }}"
117
117
  class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
118
118
  />
@@ -13,12 +13,12 @@
13
13
  </p>
14
14
  <div class="flex flex-col sm:flex-row gap-4 items-center justify-center">
15
15
  {% if navState.hasBlog %}
16
- <a href="/blogs/" class="px-8 py-3 w-full sm:w-auto text-lg font-medium text-[var(--color-bg)] bg-[var(--color-text)] rounded-full hover:opacity-90 transition-opacity">
16
+ <a href="{{ '/blogs/' | url }}" class="px-8 py-3 w-full sm:w-auto text-lg font-medium text-[var(--color-bg)] bg-[var(--color-text)] rounded-full hover:opacity-90 transition-opacity">
17
17
  Read the Blog
18
18
  </a>
19
19
  {% endif %}
20
20
  {% if navState.hasDocs %}
21
- <a href="/docs/" class="px-8 py-3 w-full sm:w-auto text-lg font-medium text-[var(--color-text)] bg-[var(--color-border)] rounded-full hover:bg-[var(--color-muted)] hover:text-white transition-colors">
21
+ <a href="{{ '/docs/' | url }}" class="px-8 py-3 w-full sm:w-auto text-lg font-medium text-[var(--color-text)] bg-[var(--color-border)] rounded-full hover:bg-[var(--color-muted)] hover:text-white transition-colors">
22
22
  Documentation
23
23
  </a>
24
24
  {% endif %}
@@ -3,7 +3,7 @@
3
3
  <nav class="sticky top-0 z-50 border-b border-[var(--color-border)] backdrop-blur-sm"
4
4
  style="background-color: var(--color-bg-nav);">
5
5
  <div class="max-w-4xl mx-auto px-6 h-14 flex items-center justify-between">
6
- <a href="/" class="text-lg font-bold text-[var(--color-text)] flex items-center gap-2 hover:opacity-80 transition-opacity">
6
+ <a href="{{ '/' | url }}" class="text-lg font-bold text-[var(--color-text)] flex items-center gap-2 hover:opacity-80 transition-opacity">
7
7
  <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-[var(--color-accent)]" viewBox="0 0 20 20" fill="currentColor">
8
8
  <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 110 2h-3a1 1 0 01-1-1v-2a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 01-1 1H4a1 1 0 110-2V4zm3 1h2v2H7V5zm2 4H7v2h2V9zm2-4h2v2h-2V5zm2 4h-2v2h2V9z" clip-rule="evenodd" />
9
9
  </svg>
@@ -11,13 +11,13 @@
11
11
  </a>
12
12
  <div class="flex gap-6 items-center">
13
13
  {% if navState.hasHome %}
14
- <a href="/" class="text-sm font-medium text-[var(--color-muted)] hover:text-[var(--color-accent)] transition-colors">Home</a>
14
+ <a href="{{ '/' | url }}" class="text-sm font-medium text-[var(--color-muted)] hover:text-[var(--color-accent)] transition-colors">Home</a>
15
15
  {% endif %}
16
16
  {% if navState.hasDocs %}
17
- <a href="/docs/" class="text-sm font-medium text-[var(--color-muted)] hover:text-[var(--color-accent)] transition-colors">Docs</a>
17
+ <a href="{{ '/docs/' | url }}" class="text-sm font-medium text-[var(--color-muted)] hover:text-[var(--color-accent)] transition-colors">Docs</a>
18
18
  {% endif %}
19
19
  {% if navState.hasBlog %}
20
- <a href="/blogs/" class="text-sm font-medium text-[var(--color-muted)] hover:text-[var(--color-accent)] transition-colors">Blogs</a>
20
+ <a href="{{ '/blogs/' | url }}" class="text-sm font-medium text-[var(--color-muted)] hover:text-[var(--color-accent)] transition-colors">Blogs</a>
21
21
  {% endif %}
22
22
  </div>
23
23
  </div>