veslx 0.1.27 → 0.1.29
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/bin/lib/build.ts +2 -1
- package/bin/lib/export.ts +214 -0
- package/bin/lib/serve.ts +2 -1
- package/dist/client/components/front-matter.js +46 -1
- package/dist/client/components/front-matter.js.map +1 -1
- package/dist/client/components/header.js +2 -24
- package/dist/client/components/header.js.map +1 -1
- package/dist/client/components/mdx-components.js +2 -0
- package/dist/client/components/mdx-components.js.map +1 -1
- package/dist/client/components/post-list-item.js +43 -0
- package/dist/client/components/post-list-item.js.map +1 -0
- package/dist/client/components/post-list.js +54 -79
- package/dist/client/components/post-list.js.map +1 -1
- package/dist/client/hooks/use-mdx-content.js +64 -4
- package/dist/client/hooks/use-mdx-content.js.map +1 -1
- package/dist/client/lib/content-classification.js +1 -22
- package/dist/client/lib/content-classification.js.map +1 -1
- package/dist/client/pages/content-router.js +2 -10
- package/dist/client/pages/content-router.js.map +1 -1
- package/dist/client/pages/home.js +20 -23
- package/dist/client/pages/home.js.map +1 -1
- package/dist/client/pages/index-post.js +34 -0
- package/dist/client/pages/index-post.js.map +1 -0
- package/dist/client/pages/post.js +1 -3
- package/dist/client/pages/post.js.map +1 -1
- package/dist/client/pages/slides.js +4 -3
- package/dist/client/pages/slides.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/plugin.ts +127 -30
- package/plugin/src/types.ts +34 -4
- package/src/components/front-matter.tsx +60 -3
- package/src/components/header.tsx +2 -20
- package/src/components/mdx-components.tsx +3 -1
- package/src/components/post-list-item.tsx +54 -0
- package/src/components/post-list.tsx +74 -116
- package/src/components/welcome.tsx +2 -2
- package/src/hooks/use-mdx-content.ts +96 -7
- package/src/index.css +17 -0
- package/src/lib/content-classification.ts +0 -24
- package/src/pages/content-router.tsx +6 -17
- package/src/pages/home.tsx +26 -58
- package/src/pages/index-post.tsx +59 -0
- package/src/pages/post.tsx +1 -3
- package/src/pages/slides.tsx +5 -3
- package/src/vite-env.d.ts +11 -1
- package/vite.config.ts +4 -3
- package/dist/client/components/running-bar.js +0 -15
- package/dist/client/components/running-bar.js.map +0 -1
- package/src/components/content-tabs.tsx +0 -64
- package/src/components/running-bar.tsx +0 -21
|
@@ -2,7 +2,6 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useParams } from "react-router-dom";
|
|
3
3
|
import { useDirectory, isSimulationRunning, findSlides } from "../plugin/src/client.js";
|
|
4
4
|
import Loading from "../components/loading.js";
|
|
5
|
-
import { RunningBar } from "../components/running-bar.js";
|
|
6
5
|
import { Header } from "../components/header.js";
|
|
7
6
|
import { useMDXContent } from "../hooks/use-mdx-content.js";
|
|
8
7
|
import { mdxComponents } from "../components/mdx-components.js";
|
|
@@ -24,7 +23,6 @@ function Post() {
|
|
|
24
23
|
}
|
|
25
24
|
return /* @__PURE__ */ jsxs("div", { className: "flex min-h-screen flex-col bg-background noise-overlay", children: [
|
|
26
25
|
/* @__PURE__ */ jsx("title", { children: frontmatter == null ? void 0 : frontmatter.title }),
|
|
27
|
-
/* @__PURE__ */ jsx(RunningBar, {}),
|
|
28
26
|
/* @__PURE__ */ jsx(Header, {}),
|
|
29
27
|
/* @__PURE__ */ jsxs("main", { className: "flex-1 w-full overflow-x-clip", children: [
|
|
30
28
|
isRunning && /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-50 px-[var(--page-padding)] py-2 bg-red-500 text-primary-foreground font-mono text-xs text-center tracking-wide", children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-3", children: [
|
|
@@ -32,7 +30,7 @@ function Post() {
|
|
|
32
30
|
/* @__PURE__ */ jsx("span", { className: "uppercase tracking-widest", children: "simulation running" }),
|
|
33
31
|
/* @__PURE__ */ jsx("span", { className: "text-primary-foreground/60", children: "Page will auto-refresh on completion" })
|
|
34
32
|
] }) }),
|
|
35
|
-
Content && /* @__PURE__ */ jsx(FrontmatterProvider, { frontmatter, children: /* @__PURE__ */ jsx("article", { className: "
|
|
33
|
+
Content && /* @__PURE__ */ jsx(FrontmatterProvider, { frontmatter, children: /* @__PURE__ */ jsx("article", { className: "mt-12 mb-64 mx-auto px-[var(--page-padding)] max-w-[var(--content-width)] animate-fade-in", children: /* @__PURE__ */ jsx(Content, { components: mdxComponents }) }) })
|
|
36
34
|
] })
|
|
37
35
|
] });
|
|
38
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post.js","sources":["../../../src/pages/post.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\";\nimport { findSlides, isSimulationRunning, useDirectory } from \"../../plugin/src/client\";\nimport Loading from \"@/components/loading\";\nimport { FileEntry } from \"plugin/src/lib\";\nimport {
|
|
1
|
+
{"version":3,"file":"post.js","sources":["../../../src/pages/post.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\";\nimport { findSlides, isSimulationRunning, useDirectory } from \"../../plugin/src/client\";\nimport Loading from \"@/components/loading\";\nimport { FileEntry } from \"plugin/src/lib\";\nimport { Header } from \"@/components/header\";\nimport { useMDXContent } from \"@/hooks/use-mdx-content\";\nimport { mdxComponents } from \"@/components/mdx-components\";\nimport { FrontmatterProvider } from \"@/lib/frontmatter-context\";\n\nexport function Post() {\n const { \"*\": rawPath = \".\" } = useParams();\n\n // The path includes the .mdx extension from the route\n const mdxPath = rawPath;\n\n // Extract directory path for finding sibling files (slides, etc.)\n const dirPath = mdxPath.replace(/\\/[^/]+\\.mdx$/, '') || '.';\n\n const { directory, loading: dirLoading } = useDirectory(dirPath)\n const { Content, frontmatter, loading: mdxLoading, error } = useMDXContent(mdxPath);\n const isRunning = isSimulationRunning();\n\n let slides: FileEntry | null = null;\n if (directory) {\n slides = findSlides(directory);\n }\n\n const loading = dirLoading || mdxLoading;\n\n if (loading) return <Loading />\n\n if (error) {\n return (\n <main className=\"min-h-screen bg-background container mx-auto max-w-4xl py-12\">\n <p className=\"text-center text-red-600\">{error.message}</p>\n </main>\n )\n }\n\n return (\n <div className=\"flex min-h-screen flex-col bg-background noise-overlay\">\n <title>{frontmatter?.title}</title>\n <Header />\n <main className=\"flex-1 w-full overflow-x-clip\">\n {isRunning && (\n <div className=\"sticky top-0 z-50 px-[var(--page-padding)] py-2 bg-red-500 text-primary-foreground font-mono text-xs text-center tracking-wide\">\n <span className=\"inline-flex items-center gap-3\">\n <span className=\"h-1.5 w-1.5 rounded-full bg-current animate-pulse\" />\n <span className=\"uppercase tracking-widest\">simulation running</span>\n <span className=\"text-primary-foreground/60\">Page will auto-refresh on completion</span>\n </span>\n </div>\n )}\n\n {Content && (\n <FrontmatterProvider frontmatter={frontmatter}>\n <article className=\"mt-12 mb-64 mx-auto px-[var(--page-padding)] max-w-[var(--content-width)] animate-fade-in\">\n <Content components={mdxComponents} />\n </article>\n </FrontmatterProvider>\n )}\n </main>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;AASO,SAAS,OAAO;AACrB,QAAM,EAAE,KAAK,UAAU,IAAA,IAAQ,UAAA;AAG/B,QAAM,UAAU;AAGhB,QAAM,UAAU,QAAQ,QAAQ,iBAAiB,EAAE,KAAK;AAExD,QAAM,EAAE,UAA+B,IAAI,aAAa,OAAO;AAC/D,QAAM,EAAE,SAAS,aAAa,SAAS,YAAY,MAAA,IAAU,cAAc,OAAO;AAClF,QAAM,YAAY,oBAAA;AAGlB,MAAI,WAAW;AACJ,eAAW,SAAS;AAAA,EAC/B;AAEA,QAAM,UAAwB;AAE9B,MAAI,QAAS,QAAO,oBAAC,SAAA,CAAA,CAAQ;AAE7B,MAAI,OAAO;AACT,WACE,oBAAC,QAAA,EAAK,WAAU,gEACd,UAAA,oBAAC,OAAE,WAAU,4BAA4B,UAAA,MAAM,QAAA,CAAQ,GACzD;AAAA,EAEJ;AAEA,SACE,qBAAC,OAAA,EAAI,WAAU,0DACb,UAAA;AAAA,IAAA,oBAAC,SAAA,EAAO,qDAAa,MAAA,CAAM;AAAA,wBAC1B,QAAA,EAAO;AAAA,IACR,qBAAC,QAAA,EAAK,WAAU,iCACb,UAAA;AAAA,MAAA,iCACE,OAAA,EAAI,WAAU,kIACb,UAAA,qBAAC,QAAA,EAAK,WAAU,kCACd,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,oDAAA,CAAoD;AAAA,QACpE,oBAAC,QAAA,EAAK,WAAU,6BAA4B,UAAA,sBAAkB;AAAA,QAC9D,oBAAC,QAAA,EAAK,WAAU,8BAA6B,UAAA,uCAAA,CAAoC;AAAA,MAAA,EAAA,CACnF,EAAA,CACF;AAAA,MAGD,WACC,oBAAC,qBAAA,EAAoB,aACnB,UAAA,oBAAC,WAAA,EAAQ,WAAU,6FACjB,UAAA,oBAAC,SAAA,EAAQ,YAAY,cAAA,CAAe,GACtC,EAAA,CACF;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;"}
|
|
@@ -3,11 +3,12 @@ import { useState, useRef, useEffect, useCallback } from "react";
|
|
|
3
3
|
import { FULLSCREEN_DATA_ATTR } from "../lib/constants.js";
|
|
4
4
|
import { useParams, useSearchParams } from "react-router-dom";
|
|
5
5
|
import Loading from "../components/loading.js";
|
|
6
|
-
import { RunningBar } from "../components/running-bar.js";
|
|
7
6
|
import { Header } from "../components/header.js";
|
|
8
7
|
import { useMDXSlides } from "../hooks/use-mdx-content.js";
|
|
9
8
|
import { slidesMdxComponents } from "../components/slides-renderer.js";
|
|
10
9
|
import { FrontmatterProvider } from "../lib/frontmatter-context.js";
|
|
10
|
+
import veslxConfig from "virtual:veslx-config";
|
|
11
|
+
import { cn } from "../lib/utils.js";
|
|
11
12
|
function SlidesPage() {
|
|
12
13
|
const { "*": rawPath = "." } = useParams();
|
|
13
14
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
@@ -87,9 +88,9 @@ function SlidesPage() {
|
|
|
87
88
|
if (!Content) {
|
|
88
89
|
return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-12 text-muted-foreground font-mono text-sm", children: 'no slides found — use "---" to separate slides' });
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
+
const scrollSnap = veslxConfig.slides.scrollSnap;
|
|
92
|
+
return /* @__PURE__ */ jsxs("main", { className: cn("slides-container", scrollSnap && "slides-scroll-snap"), children: [
|
|
91
93
|
/* @__PURE__ */ jsx("title", { children: frontmatter == null ? void 0 : frontmatter.title }),
|
|
92
|
-
/* @__PURE__ */ jsx(RunningBar, {}),
|
|
93
94
|
/* @__PURE__ */ jsx(
|
|
94
95
|
Header,
|
|
95
96
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slides.js","sources":["../../../src/pages/slides.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { FULLSCREEN_DATA_ATTR } from \"@/lib/constants\";\nimport { useParams, useSearchParams } from \"react-router-dom\"\nimport Loading from \"@/components/loading\";\nimport {
|
|
1
|
+
{"version":3,"file":"slides.js","sources":["../../../src/pages/slides.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { FULLSCREEN_DATA_ATTR } from \"@/lib/constants\";\nimport { useParams, useSearchParams } from \"react-router-dom\"\nimport Loading from \"@/components/loading\";\nimport { Header } from \"@/components/header\";\nimport { useMDXSlides } from \"@/hooks/use-mdx-content\";\nimport { slidesMdxComponents } from \"@/components/slides-renderer\";\nimport { FrontmatterProvider } from \"@/lib/frontmatter-context\";\nimport veslxConfig from \"virtual:veslx-config\";\nimport { cn } from \"@/lib/utils\";\n\n\nexport function SlidesPage() {\n const { \"*\": rawPath = \".\" } = useParams();\n const [searchParams, setSearchParams] = useSearchParams();\n\n // The path includes the .mdx extension from the route\n const mdxPath = rawPath;\n\n // Load the compiled MDX module (now includes slideCount export)\n const { Content, frontmatter, slideCount, loading, error } = useMDXSlides(mdxPath);\n\n const totalSlides = slideCount || 0;\n\n const [currentSlide, setCurrentSlide] = useState(0);\n const titleSlideRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n\n // Scroll to slide on initial load if query param is set\n useEffect(() => {\n const slideParam = parseInt(searchParams.get(\"slide\") || \"0\", 10);\n if (slideParam > 0 && contentRef.current) {\n const slideEl = contentRef.current.querySelector(`[data-slide-index=\"${slideParam}\"]`);\n if (slideEl) {\n slideEl.scrollIntoView({ behavior: \"auto\" });\n }\n }\n }, [searchParams, Content]);\n\n // Track current slide based on scroll position\n useEffect(() => {\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const index = entry.target.getAttribute(\"data-slide-index\");\n if (index !== null) {\n const slideNum = index === \"title\" ? 0 : parseInt(index, 10);\n setCurrentSlide(slideNum);\n setSearchParams(slideNum > 0 ? { slide: String(slideNum) } : {}, { replace: true });\n }\n }\n }\n },\n { threshold: 0.5 }\n );\n\n // Observe title slide\n if (titleSlideRef.current) {\n observer.observe(titleSlideRef.current);\n }\n\n // Observe content slides\n if (contentRef.current) {\n const slides = contentRef.current.querySelectorAll(\"[data-slide-index]\");\n slides.forEach((slide) => observer.observe(slide));\n }\n\n return () => observer.disconnect();\n }, [Content, setSearchParams]);\n\n // Keyboard/scroll navigation helpers\n const goToPrevious = useCallback(() => {\n const prev = Math.max(0, currentSlide - 1);\n if (contentRef.current) {\n const slideEl = contentRef.current.querySelector(`[data-slide-index=\"${prev}\"]`);\n slideEl?.scrollIntoView({ behavior: \"smooth\" });\n }\n }, [currentSlide]);\n\n const goToNext = useCallback(() => {\n const next = Math.min(totalSlides - 1, currentSlide + 1);\n if (contentRef.current) {\n const slideEl = contentRef.current.querySelector(`[data-slide-index=\"${next}\"]`);\n slideEl?.scrollIntoView({ behavior: \"smooth\" });\n }\n }, [currentSlide, totalSlides]);\n\n // Keyboard navigation (up/down only - left/right reserved for horizontal scrolling)\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"ArrowUp\") {\n e.preventDefault();\n goToPrevious();\n } else if (e.key === \"ArrowDown\") {\n e.preventDefault();\n goToNext();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [goToPrevious, goToNext]);\n\n if (loading) {\n return <Loading />\n }\n\n if (error) {\n return (\n <main className=\"min-h-screen bg-background container mx-auto max-w-4xl py-12\">\n <p className=\"text-center text-red-600\">{error.message}</p>\n </main>\n )\n }\n\n if (!Content) {\n return (\n <div className=\"flex items-center justify-center p-12 text-muted-foreground font-mono text-sm\">\n no slides found — use \"---\" to separate slides\n </div>\n );\n }\n\n const scrollSnap = veslxConfig.slides.scrollSnap;\n\n return (\n <main className={cn(\"slides-container\", scrollSnap && \"slides-scroll-snap\")}>\n <title>{frontmatter?.title}</title>\n <Header\n slideControls={{\n current: currentSlide,\n total: totalSlides,\n onPrevious: goToPrevious,\n onNext: goToNext,\n }}\n />\n <FrontmatterProvider frontmatter={frontmatter}>\n <div {...{[FULLSCREEN_DATA_ATTR]: \"true\"}}>\n <div ref={contentRef}>\n <Content components={slidesMdxComponents} />\n </div>\n </div>\n </FrontmatterProvider>\n </main>\n )\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/plugin/src/plugin.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path'
|
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
import yaml from 'js-yaml'
|
|
5
5
|
import type { IncomingMessage, ServerResponse } from 'http'
|
|
6
|
-
import { type VeslxConfig, type ResolvedSiteConfig, DEFAULT_SITE_CONFIG } from './types'
|
|
6
|
+
import { type VeslxConfig, type ResolvedSiteConfig, type ResolvedSlidesConfig, type ResolvedPostsConfig, type ResolvedConfig, DEFAULT_SITE_CONFIG, DEFAULT_SLIDES_CONFIG, DEFAULT_POSTS_CONFIG } from './types'
|
|
7
7
|
import matter from 'gray-matter'
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -89,12 +89,22 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
|
|
|
89
89
|
const dir = contentDir
|
|
90
90
|
const configPath = options?.configPath
|
|
91
91
|
|
|
92
|
-
// Mutable
|
|
92
|
+
// Mutable config that can be updated on hot reload
|
|
93
93
|
let siteConfig: ResolvedSiteConfig = {
|
|
94
94
|
...DEFAULT_SITE_CONFIG,
|
|
95
95
|
...config?.site,
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
let slidesConfig: ResolvedSlidesConfig = {
|
|
99
|
+
...DEFAULT_SLIDES_CONFIG,
|
|
100
|
+
...config?.slides,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let postsConfig: ResolvedPostsConfig = {
|
|
104
|
+
...DEFAULT_POSTS_CONFIG,
|
|
105
|
+
...config?.posts,
|
|
106
|
+
}
|
|
107
|
+
|
|
98
108
|
// Helper to reload config from file
|
|
99
109
|
function reloadConfig(): boolean {
|
|
100
110
|
if (!configPath || !fs.existsSync(configPath)) return false
|
|
@@ -105,6 +115,14 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
|
|
|
105
115
|
...DEFAULT_SITE_CONFIG,
|
|
106
116
|
...parsed?.site,
|
|
107
117
|
}
|
|
118
|
+
slidesConfig = {
|
|
119
|
+
...DEFAULT_SLIDES_CONFIG,
|
|
120
|
+
...parsed?.slides,
|
|
121
|
+
}
|
|
122
|
+
postsConfig = {
|
|
123
|
+
...DEFAULT_POSTS_CONFIG,
|
|
124
|
+
...parsed?.posts,
|
|
125
|
+
}
|
|
108
126
|
return true
|
|
109
127
|
} catch (e) {
|
|
110
128
|
console.error('[veslx] Failed to reload config:', e)
|
|
@@ -117,8 +135,8 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
|
|
|
117
135
|
|
|
118
136
|
urlToDir.set('/raw', dir)
|
|
119
137
|
|
|
120
|
-
//
|
|
121
|
-
function
|
|
138
|
+
// Get sorted entries for llms.txt generation
|
|
139
|
+
function getLlmsEntries() {
|
|
122
140
|
const frontmatters = extractFrontmatters(dir)
|
|
123
141
|
const entries: { path: string; title?: string; description?: string; date?: string; isSlides: boolean }[] = []
|
|
124
142
|
|
|
@@ -134,50 +152,100 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
|
|
|
134
152
|
})
|
|
135
153
|
}
|
|
136
154
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
// Sort alphanumerically by path (0-foo before 1-bar before 10-baz)
|
|
156
|
+
entries.sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true }))
|
|
157
|
+
|
|
158
|
+
return entries
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Generate llms.txt index (links to articles)
|
|
162
|
+
function generateLlmsTxt(): string {
|
|
163
|
+
const entries = getLlmsEntries()
|
|
143
164
|
|
|
144
165
|
const lines: string[] = [`# ${siteConfig.name}`]
|
|
145
166
|
if (siteConfig.description) {
|
|
146
|
-
lines.push(siteConfig.description)
|
|
167
|
+
lines.push(`> ${siteConfig.description}`)
|
|
147
168
|
}
|
|
148
169
|
lines.push('')
|
|
149
170
|
|
|
150
171
|
// Links section
|
|
151
172
|
if (siteConfig.homepage) {
|
|
152
|
-
lines.push(
|
|
173
|
+
lines.push(`- Homepage: ${siteConfig.homepage}`)
|
|
153
174
|
}
|
|
154
175
|
if (siteConfig.github) {
|
|
155
|
-
lines.push(
|
|
176
|
+
lines.push(`- GitHub: https://github.com/${siteConfig.github}`)
|
|
156
177
|
}
|
|
157
|
-
lines.push('Install: bun install -g veslx')
|
|
158
178
|
lines.push('')
|
|
159
179
|
|
|
160
|
-
lines.push('
|
|
180
|
+
lines.push('## Documentation')
|
|
161
181
|
lines.push('')
|
|
162
182
|
|
|
163
183
|
for (const entry of entries) {
|
|
164
|
-
const type = entry.isSlides ? '[slides]' : entry.date ? '[post]' : '[doc]'
|
|
165
184
|
const title = entry.title || entry.path.replace(/\.mdx?$/, '').split('/').pop()
|
|
166
|
-
const desc = entry.description ?
|
|
167
|
-
lines.push(
|
|
185
|
+
const desc = entry.description ? `: ${entry.description}` : ''
|
|
186
|
+
lines.push(`- [${title}](/raw/${entry.path})${desc}`)
|
|
187
|
+
}
|
|
188
|
+
lines.push('')
|
|
189
|
+
return lines.join('\n')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Generate llms-full.txt with all article content inline
|
|
193
|
+
function generateLlmsFullTxt(): string {
|
|
194
|
+
const entries = getLlmsEntries()
|
|
195
|
+
|
|
196
|
+
const lines: string[] = [`# ${siteConfig.name}`]
|
|
197
|
+
if (siteConfig.description) {
|
|
198
|
+
lines.push(`> ${siteConfig.description}`)
|
|
168
199
|
}
|
|
169
200
|
lines.push('')
|
|
201
|
+
|
|
202
|
+
for (const entry of entries) {
|
|
203
|
+
const filePath = path.join(dir, entry.path)
|
|
204
|
+
if (!fs.existsSync(filePath)) continue
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
208
|
+
// Remove frontmatter and JSX components
|
|
209
|
+
const contentWithoutFrontmatter = content
|
|
210
|
+
.replace(/^---[\s\S]*?---\n*/, '')
|
|
211
|
+
.replace(/<[A-Z][a-zA-Z]*\s*\/>/g, '') // Self-closing JSX like <FrontMatter />
|
|
212
|
+
.replace(/<[A-Z][a-zA-Z]*[^>]*>[\s\S]*?<\/[A-Z][a-zA-Z]*>/g, '') // JSX with children
|
|
213
|
+
.replace(/\n{3,}/g, '\n\n') // Collapse multiple newlines
|
|
214
|
+
|
|
215
|
+
const title = entry.title || entry.path.replace(/\.mdx?$/, '').split('/').pop()
|
|
216
|
+
|
|
217
|
+
lines.push('---')
|
|
218
|
+
lines.push('')
|
|
219
|
+
lines.push(`## ${title}`)
|
|
220
|
+
if (entry.description) {
|
|
221
|
+
lines.push(`> ${entry.description}`)
|
|
222
|
+
}
|
|
223
|
+
lines.push('')
|
|
224
|
+
lines.push(contentWithoutFrontmatter.trim())
|
|
225
|
+
lines.push('')
|
|
226
|
+
} catch {
|
|
227
|
+
// Skip files that can't be read
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
170
231
|
return lines.join('\n')
|
|
171
232
|
}
|
|
172
233
|
|
|
173
234
|
const middleware: Connect.NextHandleFunction = (req: IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => {
|
|
174
|
-
// Serve llms.txt dynamically
|
|
175
|
-
if (req.url === '/llms.txt') {
|
|
235
|
+
// Serve llms.txt dynamically (only if enabled)
|
|
236
|
+
if (req.url === '/llms.txt' && siteConfig.llmsTxt) {
|
|
176
237
|
res.setHeader('Content-Type', 'text/plain')
|
|
177
238
|
res.end(generateLlmsTxt())
|
|
178
239
|
return
|
|
179
240
|
}
|
|
180
241
|
|
|
242
|
+
// Serve llms-full.txt with all content inline (only if enabled)
|
|
243
|
+
if (req.url === '/llms-full.txt' && siteConfig.llmsTxt) {
|
|
244
|
+
res.setHeader('Content-Type', 'text/plain')
|
|
245
|
+
res.end(generateLlmsFullTxt())
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
181
249
|
// Check if URL matches any registered content directory
|
|
182
250
|
for (const [urlBase, contentDir] of urlToDir.entries()) {
|
|
183
251
|
if (req.url?.startsWith(urlBase + '/')) {
|
|
@@ -215,6 +283,28 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
|
|
|
215
283
|
|
|
216
284
|
return {
|
|
217
285
|
name: 'content',
|
|
286
|
+
enforce: 'pre',
|
|
287
|
+
|
|
288
|
+
// Inject @source directive for Tailwind to scan content directory for classes
|
|
289
|
+
transform(code, id) {
|
|
290
|
+
// Only process CSS files containing the tailwindcss import
|
|
291
|
+
if (!id.endsWith('.css')) return null
|
|
292
|
+
if (!code.includes('@import "tailwindcss"')) return null
|
|
293
|
+
|
|
294
|
+
// Calculate relative path from CSS file to content directory
|
|
295
|
+
const cssDir = path.dirname(id)
|
|
296
|
+
let relativeContentDir = path.relative(cssDir, dir)
|
|
297
|
+
relativeContentDir = relativeContentDir.replace(/\\/g, '/') // Windows compatibility
|
|
298
|
+
|
|
299
|
+
// Inject @source directive after the tailwindcss import
|
|
300
|
+
const sourceDirective = `@source "${relativeContentDir}";`
|
|
301
|
+
const modified = code.replace(
|
|
302
|
+
/(@import\s+["']tailwindcss["'];?)/,
|
|
303
|
+
`$1\n${sourceDirective}`
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return { code: modified, map: null }
|
|
307
|
+
},
|
|
218
308
|
|
|
219
309
|
// Inject @content alias and fs.allow into Vite config
|
|
220
310
|
config() {
|
|
@@ -252,12 +342,12 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
|
|
|
252
342
|
|
|
253
343
|
// Generate virtual module with import.meta.glob for MDX files
|
|
254
344
|
return `
|
|
255
|
-
export const posts = import.meta.glob('@content/**/*.mdx', {
|
|
345
|
+
export const posts = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md'], {
|
|
256
346
|
import: 'default',
|
|
257
347
|
query: { skipSlides: true }
|
|
258
348
|
});
|
|
259
|
-
export const allMdx = import.meta.glob('@content/**/*.mdx');
|
|
260
|
-
export const slides = import.meta.glob(['@content/**/SLIDES.mdx', '@content/**/*.slides.mdx']);
|
|
349
|
+
export const allMdx = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md']);
|
|
350
|
+
export const slides = import.meta.glob(['@content/**/SLIDES.mdx', '@content/**/SLIDES.md', '@content/**/*.slides.mdx', '@content/**/*.slides.md']);
|
|
261
351
|
|
|
262
352
|
// All files for directory tree building (web-compatible files only)
|
|
263
353
|
export const files = import.meta.glob([
|
|
@@ -280,12 +370,13 @@ export const files = import.meta.glob([
|
|
|
280
370
|
export const frontmatters = ${JSON.stringify(frontmatterData)};
|
|
281
371
|
|
|
282
372
|
// Legacy aliases for backwards compatibility
|
|
283
|
-
export const modules = import.meta.glob('@content/**/*.mdx');
|
|
373
|
+
export const modules = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md']);
|
|
284
374
|
`
|
|
285
375
|
}
|
|
286
376
|
if (id === RESOLVED_VIRTUAL_CONFIG_ID) {
|
|
287
|
-
// Generate virtual module with
|
|
288
|
-
|
|
377
|
+
// Generate virtual module with full config
|
|
378
|
+
const fullConfig: ResolvedConfig = { site: siteConfig, slides: slidesConfig, posts: postsConfig }
|
|
379
|
+
return `export default ${JSON.stringify(fullConfig)};`
|
|
289
380
|
}
|
|
290
381
|
},
|
|
291
382
|
|
|
@@ -330,10 +421,16 @@ export const modules = import.meta.glob('@content/**/*.mdx');
|
|
|
330
421
|
copyDirSync(dir, destDir)
|
|
331
422
|
console.log(`Content copied successfully`)
|
|
332
423
|
|
|
333
|
-
// Generate llms.txt
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
424
|
+
// Generate llms.txt files if enabled
|
|
425
|
+
if (siteConfig.llmsTxt) {
|
|
426
|
+
const llmsTxtPath = path.join(outDir, 'llms.txt')
|
|
427
|
+
fs.writeFileSync(llmsTxtPath, generateLlmsTxt())
|
|
428
|
+
console.log(`Generated llms.txt`)
|
|
429
|
+
|
|
430
|
+
const llmsFullTxtPath = path.join(outDir, 'llms-full.txt')
|
|
431
|
+
fs.writeFileSync(llmsFullTxtPath, generateLlmsFullTxt())
|
|
432
|
+
console.log(`Generated llms-full.txt`)
|
|
433
|
+
}
|
|
337
434
|
} else {
|
|
338
435
|
console.warn(`Content directory not found: ${dir}`)
|
|
339
436
|
}
|
package/plugin/src/types.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface SlidesConfig {
|
|
2
|
+
scrollSnap?: boolean;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface PostsConfig {
|
|
6
|
+
sort?: 'date' | 'alpha';
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
export interface SiteConfig {
|
|
4
10
|
name?: string;
|
|
5
11
|
description?: string;
|
|
6
12
|
github?: string;
|
|
7
13
|
homepage?: string;
|
|
8
|
-
|
|
14
|
+
llmsTxt?: boolean;
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export interface VeslxConfig {
|
|
12
18
|
dir?: string;
|
|
13
19
|
site?: SiteConfig;
|
|
20
|
+
slides?: SlidesConfig;
|
|
21
|
+
posts?: PostsConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResolvedSlidesConfig {
|
|
25
|
+
scrollSnap: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ResolvedPostsConfig {
|
|
29
|
+
sort: 'date' | 'alpha';
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
export interface ResolvedSiteConfig {
|
|
@@ -18,7 +34,13 @@ export interface ResolvedSiteConfig {
|
|
|
18
34
|
description: string;
|
|
19
35
|
github: string;
|
|
20
36
|
homepage: string;
|
|
21
|
-
|
|
37
|
+
llmsTxt: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ResolvedConfig {
|
|
41
|
+
site: ResolvedSiteConfig;
|
|
42
|
+
slides: ResolvedSlidesConfig;
|
|
43
|
+
posts: ResolvedPostsConfig;
|
|
22
44
|
}
|
|
23
45
|
|
|
24
46
|
export const DEFAULT_SITE_CONFIG: ResolvedSiteConfig = {
|
|
@@ -26,5 +48,13 @@ export const DEFAULT_SITE_CONFIG: ResolvedSiteConfig = {
|
|
|
26
48
|
description: '',
|
|
27
49
|
github: '',
|
|
28
50
|
homepage: '',
|
|
29
|
-
|
|
51
|
+
llmsTxt: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const DEFAULT_SLIDES_CONFIG: ResolvedSlidesConfig = {
|
|
55
|
+
scrollSnap: true,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const DEFAULT_POSTS_CONFIG: ResolvedPostsConfig = {
|
|
59
|
+
sort: 'alpha',
|
|
30
60
|
};
|
|
@@ -1,8 +1,56 @@
|
|
|
1
|
+
import { useLocation } from "react-router-dom";
|
|
1
2
|
import { useFrontmatter } from "@/lib/frontmatter-context";
|
|
2
|
-
import { formatDate } from "@/lib/format-date"
|
|
3
|
+
import { formatDate } from "@/lib/format-date";
|
|
4
|
+
import veslxConfig from "virtual:veslx-config";
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Convert MDX content to llms.txt format.
|
|
8
|
+
*/
|
|
9
|
+
function convertToLlmsTxt(
|
|
10
|
+
rawMdx: string,
|
|
11
|
+
frontmatter?: { title?: string; description?: string }
|
|
12
|
+
): string {
|
|
13
|
+
const contentWithoutFrontmatter = rawMdx.replace(/^---[\s\S]*?---\n*/, '')
|
|
14
|
+
|
|
15
|
+
const parts: string[] = []
|
|
16
|
+
|
|
17
|
+
const title = frontmatter?.title || 'Untitled'
|
|
18
|
+
parts.push(`# ${title}`)
|
|
19
|
+
|
|
20
|
+
if (frontmatter?.description) {
|
|
21
|
+
parts.push('')
|
|
22
|
+
parts.push(`> ${frontmatter.description}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (contentWithoutFrontmatter.trim()) {
|
|
26
|
+
parts.push('')
|
|
27
|
+
parts.push(contentWithoutFrontmatter.trim())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return parts.join('\n')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function FrontMatter() {
|
|
5
34
|
const frontmatter = useFrontmatter();
|
|
35
|
+
const location = useLocation();
|
|
36
|
+
const config = veslxConfig.site;
|
|
37
|
+
|
|
38
|
+
const rawUrl = `/raw${location.pathname.replace(/^\//, '/')}`;
|
|
39
|
+
|
|
40
|
+
const handleLlmsTxt = async (e: React.MouseEvent) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(rawUrl);
|
|
44
|
+
if (!res.ok) throw new Error('Failed to fetch');
|
|
45
|
+
const rawMdx = await res.text();
|
|
46
|
+
const llmsTxt = convertToLlmsTxt(rawMdx, frontmatter);
|
|
47
|
+
const blob = new Blob([llmsTxt], { type: 'text/plain' });
|
|
48
|
+
const url = URL.createObjectURL(blob);
|
|
49
|
+
window.location.href = url;
|
|
50
|
+
} catch {
|
|
51
|
+
console.error('Failed to load llms.txt');
|
|
52
|
+
}
|
|
53
|
+
};
|
|
6
54
|
|
|
7
55
|
return (
|
|
8
56
|
<div>
|
|
@@ -19,6 +67,15 @@ export function FrontMatter(){
|
|
|
19
67
|
{formatDate(new Date(frontmatter.date as string))}
|
|
20
68
|
</time>
|
|
21
69
|
)}
|
|
70
|
+
{config.llmsTxt && (
|
|
71
|
+
<a
|
|
72
|
+
href="#"
|
|
73
|
+
onClick={handleLlmsTxt}
|
|
74
|
+
className="font-mono text-xs text-muted-foreground/70 hover:text-foreground underline underline-offset-2 transition-colors"
|
|
75
|
+
>
|
|
76
|
+
llms.txt
|
|
77
|
+
</a>
|
|
78
|
+
)}
|
|
22
79
|
</div>
|
|
23
80
|
|
|
24
81
|
{frontmatter?.description && (
|
|
@@ -29,5 +86,5 @@ export function FrontMatter(){
|
|
|
29
86
|
</header>
|
|
30
87
|
)}
|
|
31
88
|
</div>
|
|
32
|
-
)
|
|
89
|
+
);
|
|
33
90
|
}
|
|
@@ -2,7 +2,7 @@ import { Link, useParams } from "react-router-dom";
|
|
|
2
2
|
import { ModeToggle } from "./mode-toggle";
|
|
3
3
|
import { SiGithub } from "@icons-pack/react-simple-icons";
|
|
4
4
|
import { ChevronUp, ChevronDown } from "lucide-react";
|
|
5
|
-
import
|
|
5
|
+
import veslxConfig from "virtual:veslx-config";
|
|
6
6
|
import { cn } from "@/lib/utils";
|
|
7
7
|
|
|
8
8
|
interface HeaderProps {
|
|
@@ -15,7 +15,7 @@ interface HeaderProps {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function Header({ slideControls }: HeaderProps = {}) {
|
|
18
|
-
const config =
|
|
18
|
+
const config = veslxConfig.site;
|
|
19
19
|
|
|
20
20
|
const { "*": path } = useParams()
|
|
21
21
|
|
|
@@ -62,24 +62,6 @@ export function Header({ slideControls }: HeaderProps = {}) {
|
|
|
62
62
|
</button>
|
|
63
63
|
</>
|
|
64
64
|
)}
|
|
65
|
-
<Link
|
|
66
|
-
to={`/posts`}
|
|
67
|
-
className={cn(
|
|
68
|
-
"font-medium text-muted-foreground/70 hover:text-foreground transition-colors duration-300",
|
|
69
|
-
path?.startsWith("posts") && "font-semibold text-foreground",
|
|
70
|
-
)}
|
|
71
|
-
>
|
|
72
|
-
Posts
|
|
73
|
-
</Link>
|
|
74
|
-
<Link
|
|
75
|
-
to={`/docs`}
|
|
76
|
-
className={cn(
|
|
77
|
-
"font-medium text-muted-foreground/70 hover:text-foreground transition-colors duration-300",
|
|
78
|
-
path?.startsWith("docs") && "font-semibold text-foreground",
|
|
79
|
-
)}
|
|
80
|
-
>
|
|
81
|
-
Docs
|
|
82
|
-
</Link>
|
|
83
65
|
{config.github && (
|
|
84
66
|
<Link
|
|
85
67
|
to={`https://github.com/${config.github}`}
|
|
@@ -7,7 +7,7 @@ import { HeroSlide } from './slides/hero-slide'
|
|
|
7
7
|
import { FigureSlide } from './slides/figure-slide'
|
|
8
8
|
import { TextSlide } from './slides/text-slide'
|
|
9
9
|
import { SlideOutline } from './slides/slide-outline'
|
|
10
|
-
|
|
10
|
+
import { PostList } from '@/components/post-list'
|
|
11
11
|
/**
|
|
12
12
|
* Smart link component that uses React Router for internal links
|
|
13
13
|
* and regular anchor tags for external links.
|
|
@@ -89,6 +89,8 @@ export const mdxComponents = {
|
|
|
89
89
|
|
|
90
90
|
SlideOutline,
|
|
91
91
|
|
|
92
|
+
PostList,
|
|
93
|
+
|
|
92
94
|
// Headings - clean sans-serif
|
|
93
95
|
h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => {
|
|
94
96
|
const id = generateId(props.children)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Link } from "react-router-dom";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import { formatDate } from "@/lib/format-date";
|
|
4
|
+
import { ArrowRight, Presentation } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
interface PostListItemProps {
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
date?: Date;
|
|
10
|
+
linkPath: string;
|
|
11
|
+
isSlides?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function PostListItem({ title, description, date, linkPath, isSlides }: PostListItemProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Link
|
|
17
|
+
to={linkPath}
|
|
18
|
+
className={cn(
|
|
19
|
+
"group block py-3 px-3 -mx-3 rounded-md",
|
|
20
|
+
"transition-colors duration-150",
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
<article className="flex items-center gap-4">
|
|
24
|
+
{/* Main content */}
|
|
25
|
+
<div className="flex-1 min-w-0">
|
|
26
|
+
<div className={cn(
|
|
27
|
+
"text-sm font-medium text-foreground",
|
|
28
|
+
"group-hover:underline",
|
|
29
|
+
"flex items-center gap-2"
|
|
30
|
+
)}>
|
|
31
|
+
<span>{title}</span>
|
|
32
|
+
<ArrowRight className="h-3 w-3 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200 text-primary" />
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{description && (
|
|
36
|
+
<div className="text-sm text-muted-foreground line-clamp-1 mt-0.5">
|
|
37
|
+
{description}
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{isSlides && (
|
|
43
|
+
<Presentation className="h-3 w-3 text-muted-foreground" />
|
|
44
|
+
)}
|
|
45
|
+
<time
|
|
46
|
+
dateTime={date?.toISOString()}
|
|
47
|
+
className="font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0"
|
|
48
|
+
>
|
|
49
|
+
{date && formatDate(date)}
|
|
50
|
+
</time>
|
|
51
|
+
</article>
|
|
52
|
+
</Link>
|
|
53
|
+
);
|
|
54
|
+
}
|