@aravindc26/velu 0.12.7 → 0.12.9

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.
Files changed (74) hide show
  1. package/package.json +1 -1
  2. package/src/build.ts +13 -0
  3. package/src/cli.ts +51 -9
  4. package/src/engine/app/(docs)/[...slug]/layout.tsx +21 -537
  5. package/src/engine/app/_preview/[sessionId]/[...slug]/layout.tsx +96 -0
  6. package/src/engine/app/_preview/[sessionId]/[...slug]/page.tsx +298 -0
  7. package/src/engine/app/_preview/[sessionId]/layout.tsx +56 -0
  8. package/src/{preview-engine/app → engine/app/_preview}/[sessionId]/page.tsx +7 -3
  9. package/src/engine/app/_preview/api/sessions/[sessionId]/assets/[...path]/route.ts +51 -0
  10. package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/init/route.ts +2 -2
  11. package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/route.ts +3 -3
  12. package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/sync/route.ts +2 -2
  13. package/src/{preview-engine/app → engine/app/_preview}/layout.tsx +4 -1
  14. package/src/engine/app/global.css +0 -3623
  15. package/src/engine/app/layout.tsx +4 -3
  16. package/src/engine/components/sidebar-links.tsx +11 -5
  17. package/src/engine/lib/docs-layout.tsx +605 -0
  18. package/src/engine/lib/layout.shared.ts +7 -7
  19. package/src/engine/lib/preview-config.ts +129 -0
  20. package/src/{preview-engine/lib/content-generator.ts → engine/lib/preview-content.ts} +242 -42
  21. package/src/engine/lib/source.ts +80 -97
  22. package/src/engine/lib/velu.ts +79 -55
  23. package/src/engine/mdx-components.tsx +14 -650
  24. package/src/engine/source.config.ts +11 -89
  25. package/src/engine/tsconfig.json +1 -0
  26. package/src/engine-core/components/assistant.tsx +361 -0
  27. package/src/engine-core/components/banner.tsx +80 -0
  28. package/src/engine-core/components/changelog-filters.tsx +114 -0
  29. package/src/engine-core/components/code-group.tsx +383 -0
  30. package/src/engine-core/components/color.tsx +118 -0
  31. package/src/engine-core/components/copy-page.tsx +223 -0
  32. package/src/engine-core/components/dropdown-switcher.tsx +142 -0
  33. package/src/engine-core/components/expandable.tsx +77 -0
  34. package/src/engine-core/components/header-tab-link.tsx +43 -0
  35. package/src/engine-core/components/icon.tsx +136 -0
  36. package/src/engine-core/components/image-zoom-fallback.tsx +147 -0
  37. package/src/engine-core/components/image.tsx +111 -0
  38. package/src/engine-core/components/lang-switcher.tsx +101 -0
  39. package/src/engine-core/components/manual-api-playground.tsx +154 -0
  40. package/src/engine-core/components/mermaid.tsx +142 -0
  41. package/src/engine-core/components/openapi-toc-sync.tsx +59 -0
  42. package/src/engine-core/components/openapi.tsx +1682 -0
  43. package/src/engine-core/components/page-feedback-api.test.ts +83 -0
  44. package/src/engine-core/components/page-feedback-api.ts +89 -0
  45. package/src/engine-core/components/page-feedback.tsx +200 -0
  46. package/src/engine-core/components/product-switcher.tsx +107 -0
  47. package/src/engine-core/components/prompt.tsx +90 -0
  48. package/src/engine-core/components/providers.tsx +21 -0
  49. package/src/engine-core/components/search.tsx +318 -0
  50. package/src/engine-core/components/sidebar-links.tsx +54 -0
  51. package/src/engine-core/components/synced-tabs.tsx +57 -0
  52. package/src/engine-core/components/theme-toggle.tsx +39 -0
  53. package/src/engine-core/components/toc-examples.tsx +110 -0
  54. package/src/engine-core/components/version-switcher.tsx +95 -0
  55. package/src/engine-core/components/view.tsx +344 -0
  56. package/src/engine-core/css/assistant.css +326 -0
  57. package/src/engine-core/css/copy-page.css +206 -0
  58. package/src/engine-core/css/search.css +142 -0
  59. package/src/engine-core/css/shared.css +3628 -0
  60. package/src/engine-core/lib/remark-plugins.ts +102 -0
  61. package/src/engine-core/lib/source-plugins.ts +105 -0
  62. package/src/engine-core/mdx-components.tsx +654 -0
  63. package/src/engine-core/types.ts +49 -0
  64. package/src/preview-engine/app/[sessionId]/[...slug]/page.tsx +0 -41
  65. package/src/preview-engine/app/[sessionId]/layout.tsx +0 -23
  66. package/src/preview-engine/app/global.css +0 -3
  67. package/src/preview-engine/lib/session-config.ts +0 -86
  68. package/src/preview-engine/lib/source.ts +0 -60
  69. package/src/preview-engine/next.config.mjs +0 -20
  70. package/src/preview-engine/postcss.config.mjs +0 -8
  71. package/src/preview-engine/source.config.ts +0 -26
  72. package/src/preview-engine/tsconfig.json +0 -32
  73. /package/src/{preview-engine/app → engine/app/_preview}/page.tsx +0 -0
  74. /package/src/{preview-engine/lib/auth.ts → engine/lib/preview-auth.ts} +0 -0
@@ -0,0 +1,298 @@
1
+ import { createElement } from 'react';
2
+ import { notFound } from 'next/navigation';
3
+ import { createRelativeLink } from 'fumadocs-ui/mdx';
4
+ import {
5
+ DocsBody,
6
+ DocsDescription,
7
+ DocsPage,
8
+ DocsTitle,
9
+ } from 'fumadocs-ui/layouts/notebook/page';
10
+ import { source } from '@/lib/source';
11
+ import { getMDXComponents } from '@/mdx-components';
12
+ import { loadSessionConfigSource } from '@/lib/preview-config';
13
+ import {
14
+ getApiConfig,
15
+ getContextualOptions,
16
+ getFooterSocials,
17
+ getIconLibrary,
18
+ } from '@/lib/velu';
19
+ import { CopyPageButton } from '@/components/copy-page';
20
+ import { VeluImageZoomFallback } from '@/components/image-zoom-fallback';
21
+ import { VeluOpenAPI } from '@/components/openapi';
22
+ import { OpenApiTocSync } from '@/components/openapi-toc-sync';
23
+ import { TocExamples } from '@/components/toc-examples';
24
+ import { VeluIcon } from '@/components/icon';
25
+
26
+ interface PageProps {
27
+ params: Promise<{ sessionId: string; slug: string[] }>;
28
+ }
29
+
30
+ /**
31
+ * Parse raw frontmatter key-value pairs from markdown source.
32
+ * This bypasses fumadocs' Zod schema stripping so we can access custom fields like `openapi`.
33
+ */
34
+ function parseFrontmatterMap(markdown?: string): Record<string, string> {
35
+ if (!markdown) return {};
36
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/);
37
+ if (!match) return {};
38
+
39
+ const output: Record<string, string> = {};
40
+ const lines = match[1].split(/\r?\n/);
41
+ for (const rawLine of lines) {
42
+ const line = rawLine.trim();
43
+ if (!line || line.startsWith('#')) continue;
44
+ const entry = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.+)$/);
45
+ if (!entry) continue;
46
+ const key = entry[1];
47
+ const rawValue = entry[2].trim();
48
+ const value = rawValue.replace(/^['"]|['"]$/g, '').trim();
49
+ output[key] = value;
50
+ }
51
+
52
+ return output;
53
+ }
54
+
55
+ interface ParsedOpenApiFrontmatter {
56
+ spec: string;
57
+ method: string;
58
+ endpoint: string;
59
+ }
60
+
61
+ function parseOpenApiFrontmatter(rawValue: string | undefined, defaultSpec?: string): ParsedOpenApiFrontmatter | null {
62
+ if (!rawValue) return null;
63
+ const trimmed = rawValue.trim();
64
+ if (!trimmed) return null;
65
+
66
+ const withInlineSpec = trimmed.match(/^(\S+)\s+([A-Za-z]+)\s+(.+)$/);
67
+ if (withInlineSpec) {
68
+ const method = withInlineSpec[2].toUpperCase();
69
+ const endpoint = withInlineSpec[3].trim();
70
+ if (method !== 'WEBHOOK' && !endpoint.startsWith('/')) return null;
71
+ if (!endpoint) return null;
72
+ return { spec: withInlineSpec[1], method, endpoint };
73
+ }
74
+
75
+ const withDefaultSpec = trimmed.match(/^([A-Za-z]+)\s+(.+)$/);
76
+ if (withDefaultSpec && defaultSpec) {
77
+ const method = withDefaultSpec[1].toUpperCase();
78
+ const endpoint = withDefaultSpec[2].trim();
79
+ if (method !== 'WEBHOOK' && !endpoint.startsWith('/')) return null;
80
+ if (!endpoint) return null;
81
+ return { spec: defaultSpec, method, endpoint };
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ function withTrailingSlashPath(path: string): string {
88
+ if (!path.startsWith('/')) return path;
89
+ if (path === '/' || path.endsWith('/')) return path;
90
+ if (/\.[a-zA-Z0-9]+$/.test(path)) return path;
91
+ return `${path}/`;
92
+ }
93
+
94
+ /**
95
+ * Prefix absolute internal hrefs with the session ID so that
96
+ * MDX content links like /core-api/quickstart become /mint-test/core-api/quickstart.
97
+ */
98
+ function prefixHref(href: unknown, sessionPrefix: string): unknown {
99
+ if (typeof href !== 'string') return href;
100
+ if (!href.startsWith('/')) return href;
101
+ if (href.startsWith(sessionPrefix + '/') || href === sessionPrefix) return href;
102
+ return `${sessionPrefix}${href}`;
103
+ }
104
+
105
+ /**
106
+ * Wrap MDX components so that all internal link hrefs include the session prefix.
107
+ * Handles: <a> tags, <Card> hrefs, and any other element rendered with href.
108
+ */
109
+ function prefixMdxComponentLinks(
110
+ components: Record<string, any>,
111
+ sessionPrefix: string,
112
+ ): Record<string, any> {
113
+ const patched = { ...components };
114
+
115
+ // Wrap the <a> component
116
+ const OriginalA = patched.a;
117
+ if (OriginalA) {
118
+ patched.a = (props: any) => {
119
+ const href = prefixHref(props.href, sessionPrefix);
120
+ return typeof OriginalA === 'function'
121
+ ? OriginalA({ ...props, href })
122
+ : createElement('a', { ...props, href });
123
+ };
124
+ }
125
+
126
+ // Wrap the Card component
127
+ const OriginalCard = patched.Card;
128
+ if (OriginalCard) {
129
+ patched.Card = (props: any) => {
130
+ const href = prefixHref(props.href, sessionPrefix);
131
+ return createElement(OriginalCard, { ...props, href });
132
+ };
133
+ }
134
+
135
+ return patched;
136
+ }
137
+
138
+ export default async function PreviewPage({ params }: PageProps) {
139
+ const { sessionId, slug } = await params;
140
+
141
+ // The full slug for fumadocs lookup includes the session ID prefix
142
+ const fullSlug = [sessionId, ...slug];
143
+ const page = source.getPage(fullSlug);
144
+
145
+ if (!page) notFound();
146
+
147
+ const pageDataRecord = page.data as unknown as Record<string, unknown>;
148
+ const MDX = pageDataRecord.body as any;
149
+ if (typeof MDX !== 'function') notFound();
150
+
151
+ const configSource = loadSessionConfigSource(sessionId);
152
+ const iconLibrary = configSource ? getIconLibrary(configSource) : 'fontawesome';
153
+ const footerSocials = configSource ? getFooterSocials(configSource) : [];
154
+ const apiConfig = getApiConfig(configSource ?? undefined);
155
+
156
+ const effectiveMarkdown = typeof pageDataRecord.processedMarkdown === 'string'
157
+ ? String(pageDataRecord.processedMarkdown)
158
+ : undefined;
159
+ const hasPanelExamples = typeof effectiveMarkdown === 'string'
160
+ && /<(?:Panel|RequestExample|ResponseExample)(?:\s|>)/.test(effectiveMarkdown);
161
+
162
+ // OpenAPI rendering — parse from raw markdown to bypass Zod schema stripping
163
+ const frontmatter = parseFrontmatterMap(effectiveMarkdown);
164
+ const frontmatterOpenapi = frontmatter.openapi ?? (typeof pageDataRecord.openapi === 'string' ? pageDataRecord.openapi : undefined);
165
+ const parsedOpenApi = parseOpenApiFrontmatter(
166
+ frontmatterOpenapi,
167
+ apiConfig.defaultOpenApiSpec,
168
+ );
169
+ const hasExplicitApiRendering = typeof effectiveMarkdown === 'string'
170
+ && /<(?:APIPlayground|ApiPlayground|OpenAPI)\b/.test(effectiveMarkdown);
171
+ const shouldShowOpenApi = !hasExplicitApiRendering && Boolean(parsedOpenApi);
172
+ const proxyUrl = apiConfig.playgroundProxyEnabled ? '/api/proxy' : '';
173
+ const isDeprecatedPage = (pageDataRecord.deprecated === true)
174
+ || String((pageDataRecord.status ?? '')).trim().toLowerCase() === 'deprecated';
175
+ const pageToc = pageDataRecord.toc as any;
176
+ const pageFull = typeof pageDataRecord.full === 'boolean' ? pageDataRecord.full : undefined;
177
+ const hasApiTocRail = shouldShowOpenApi || hasPanelExamples;
178
+ const tableOfContentHeader = hasApiTocRail
179
+ ? <div className={shouldShowOpenApi ? 'velu-api-toc-rail' : 'velu-toc-panel-rail'}><div id="velu-api-toc-rail-host" /></div>
180
+ : undefined;
181
+
182
+ // Prev/next navigation
183
+ const sessionPrefix = `/${sessionId}`;
184
+ const allPages = source.getPages();
185
+ const orderedSessionPages = allPages.filter((p) => p.url.startsWith(`${sessionPrefix}/`));
186
+ const sourcePageUrl = (page as unknown as { url?: string }).url;
187
+ const fallbackPath = `/${fullSlug.join('/')}`.replace(/\/{2,}/g, '/');
188
+ const pageUrl = (typeof sourcePageUrl === 'string' && sourcePageUrl.trim())
189
+ ? sourcePageUrl
190
+ : (fallbackPath === '' ? '/' : fallbackPath);
191
+ const currentIndex = orderedSessionPages.findIndex((entry) => entry.url === pageUrl);
192
+ const previousPage = currentIndex > 0 ? orderedSessionPages[currentIndex - 1] : undefined;
193
+ const nextPage = currentIndex >= 0 && currentIndex < orderedSessionPages.length - 1 ? orderedSessionPages[currentIndex + 1] : undefined;
194
+
195
+ return (
196
+ <DocsPage
197
+ toc={pageToc}
198
+ full={pageFull}
199
+ tableOfContent={tableOfContentHeader ? { header: tableOfContentHeader } : undefined}
200
+ footer={{ enabled: false }}
201
+ >
202
+ <TocExamples />
203
+ <OpenApiTocSync enabled={hasApiTocRail} />
204
+ <VeluImageZoomFallback />
205
+ <div className="velu-title-row">
206
+ <div className="velu-title-main">
207
+ <DocsTitle>{page.data.title}</DocsTitle>
208
+ {isDeprecatedPage ? <span className="velu-pill velu-pill-deprecated velu-page-deprecated-badge">Deprecated</span> : null}
209
+ </div>
210
+ <div className="velu-title-actions">
211
+ <CopyPageButton options={getContextualOptions(configSource ?? undefined)} mcpUrl="" />
212
+ </div>
213
+ </div>
214
+ {page.data.description ? (
215
+ <DocsDescription>{page.data.description}</DocsDescription>
216
+ ) : null}
217
+ <DocsBody>
218
+ {shouldShowOpenApi && parsedOpenApi ? (
219
+ <VeluOpenAPI
220
+ className="velu-api-playground"
221
+ schemaSource={parsedOpenApi.spec}
222
+ endpoint={parsedOpenApi.endpoint}
223
+ method={parsedOpenApi.method}
224
+ proxyUrl={proxyUrl}
225
+ exampleLanguages={apiConfig.exampleLanguages}
226
+ exampleAutogenerate={apiConfig.exampleAutogenerate}
227
+ layout="playground"
228
+ showTitle={false}
229
+ showDescription={false}
230
+ />
231
+ ) : null}
232
+ <MDX
233
+ components={prefixMdxComponentLinks(
234
+ getMDXComponents({
235
+ a: createRelativeLink(source, page),
236
+ }, configSource ?? undefined),
237
+ sessionPrefix,
238
+ )}
239
+ />
240
+ </DocsBody>
241
+ <section className="velu-page-feedback-wrap" aria-label="Page navigation">
242
+ {(previousPage || nextPage) ? (
243
+ <div className={['velu-page-nav-grid', previousPage && nextPage ? 'velu-page-nav-grid-two' : 'velu-page-nav-grid-one'].join(' ')}>
244
+ {previousPage ? (
245
+ <a href={withTrailingSlashPath(previousPage.url)} className="velu-page-nav-card">
246
+ <p className="velu-page-nav-title">{previousPage.data.title}</p>
247
+ <p className="velu-page-nav-meta">
248
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg>
249
+ <span>{previousPage.data.description ?? 'Previous'}</span>
250
+ </p>
251
+ </a>
252
+ ) : null}
253
+ {nextPage ? (
254
+ <a href={withTrailingSlashPath(nextPage.url)} className="velu-page-nav-card velu-page-nav-card-next">
255
+ <p className="velu-page-nav-title">{nextPage.data.title}</p>
256
+ <p className="velu-page-nav-meta velu-page-nav-meta-next">
257
+ <span>{nextPage.data.description ?? 'Next'}</span>
258
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 18l6-6-6-6" /></svg>
259
+ </p>
260
+ </a>
261
+ ) : null}
262
+ </div>
263
+ ) : null}
264
+ </section>
265
+ <footer className="velu-footer">
266
+ {footerSocials.length > 0 ? (
267
+ <div className="velu-footer-socials" aria-label="Social links">
268
+ {footerSocials.map((social) => (
269
+ <a
270
+ key={`${social.key}:${social.href}`}
271
+ href={social.href}
272
+ target="_blank"
273
+ rel="noopener noreferrer"
274
+ className="velu-footer-social-link"
275
+ aria-label={social.label}
276
+ title={social.label}
277
+ >
278
+ <VeluIcon
279
+ name={social.icon}
280
+ iconType={social.iconType}
281
+ library="fontawesome"
282
+ className="velu-footer-social-icon"
283
+ fallback={false}
284
+ />
285
+ </a>
286
+ ))}
287
+ </div>
288
+ ) : (
289
+ <span />
290
+ )}
291
+ <div className="velu-footer-powered">
292
+ Powered by <a href="https://getvelu.com" target="_blank" rel="noopener noreferrer">Velu</a>
293
+ </div>
294
+ </footer>
295
+ </DocsPage>
296
+ );
297
+ }
298
+
@@ -0,0 +1,56 @@
1
+ import type { ReactNode } from 'react';
2
+ import { loadSessionConfigSource, getSessionThemeCss } from '@/lib/preview-config';
3
+ import { getBannerConfig, getFontsConfig, getSiteFavicon } from '@/lib/velu';
4
+ import { VeluBanner } from '@/components/banner';
5
+
6
+ interface LayoutProps {
7
+ children: ReactNode;
8
+ params: Promise<{ sessionId: string }>;
9
+ }
10
+
11
+ /**
12
+ * Session layout: injects per-session theme CSS, Google Fonts, and banner.
13
+ * Uses React 19 resource hoisting (<style precedence> / <link precedence>)
14
+ * so tags are hoisted to <head> without creating body DOM elements
15
+ * that would break fumadocs' sticky sidebar CSS grid.
16
+ */
17
+ export default async function SessionLayout({ children, params }: LayoutProps) {
18
+ const { sessionId } = await params;
19
+ const themeCss = getSessionThemeCss(sessionId);
20
+
21
+ // Build Google Fonts URL from session config
22
+ let googleFontsUrl: string | null = null;
23
+ const configSource = loadSessionConfigSource(sessionId);
24
+ const bannerConfig = configSource ? getBannerConfig(configSource) : null;
25
+ if (configSource) {
26
+ const fontsConfig = getFontsConfig(configSource);
27
+ if (fontsConfig) {
28
+ const families = new Set<string>();
29
+ for (const def of [fontsConfig.heading, fontsConfig.body]) {
30
+ if (def && !def.source) {
31
+ const weight = def.weight ? `:wght@${def.weight}` : ':wght@400;500;600;700';
32
+ families.add(`${def.family.replace(/ /g, '+')}${weight}`);
33
+ }
34
+ }
35
+ if (families.size > 0) {
36
+ googleFontsUrl = `https://fonts.googleapis.com/css2?${[...families].map(f => `family=${f}`).join('&')}&display=swap`;
37
+ }
38
+ }
39
+ }
40
+
41
+ // Favicon: resolve through the session assets API so it loads from the workspace
42
+ const faviconPath = configSource ? getSiteFavicon(configSource) : undefined;
43
+ const faviconUrl = faviconPath
44
+ ? `/api/sessions/${sessionId}/assets/${faviconPath.replace(/^\//, '')}`
45
+ : undefined;
46
+
47
+ return (
48
+ <>
49
+ {themeCss ? <style precedence="session-theme" href={`velu-session-theme-${sessionId}`}>{themeCss}</style> : null}
50
+ {googleFontsUrl ? <link rel="stylesheet" href={googleFontsUrl} precedence="session-fonts" /> : null}
51
+ {faviconUrl ? <link rel="icon" href={faviconUrl} /> : null}
52
+ {bannerConfig ? <VeluBanner content={bannerConfig.content} dismissible={bannerConfig.dismissible} /> : null}
53
+ {children}
54
+ </>
55
+ );
56
+ }
@@ -1,5 +1,5 @@
1
1
  import { redirect } from 'next/navigation';
2
- import { source, getSessionPageTree } from '@/lib/source';
2
+ import { getSessionPageTree } from '@/lib/source';
3
3
 
4
4
  interface PageProps {
5
5
  params: Promise<{ sessionId: string }>;
@@ -30,6 +30,10 @@ export default async function SessionIndexPage({ params }: PageProps) {
30
30
  redirect(firstUrl.endsWith('/') ? firstUrl : `${firstUrl}/`);
31
31
  }
32
32
 
33
- // Fallback: redirect to session root
34
- redirect(`/${sessionId}/`);
33
+ return (
34
+ <div style={{ padding: '2rem', textAlign: 'center', color: '#888' }}>
35
+ <p>No pages found for session <code>{sessionId}</code>.</p>
36
+ <p>Initialize the session via <code>POST /api/sessions/{sessionId}/init</code></p>
37
+ </div>
38
+ );
35
39
  }
@@ -0,0 +1,51 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join, extname } from 'node:path';
4
+ import { getWorkspaceDir } from '@/lib/preview-config';
5
+
6
+ const MIME_TYPES: Record<string, string> = {
7
+ '.svg': 'image/svg+xml',
8
+ '.png': 'image/png',
9
+ '.jpg': 'image/jpeg',
10
+ '.jpeg': 'image/jpeg',
11
+ '.gif': 'image/gif',
12
+ '.webp': 'image/webp',
13
+ '.ico': 'image/x-icon',
14
+ '.mp4': 'video/mp4',
15
+ '.webm': 'video/webm',
16
+ '.mov': 'video/quicktime',
17
+ '.json': 'application/json',
18
+ '.css': 'text/css',
19
+ '.js': 'application/javascript',
20
+ };
21
+
22
+ export async function GET(
23
+ _req: NextRequest,
24
+ { params }: { params: Promise<{ sessionId: string; path: string[] }> },
25
+ ) {
26
+ const { sessionId, path: segments } = await params;
27
+ const assetPath = segments.join('/');
28
+
29
+ // Prevent path traversal
30
+ if (assetPath.includes('..')) {
31
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
32
+ }
33
+
34
+ const workspaceDir = getWorkspaceDir(sessionId);
35
+ const filePath = join(workspaceDir, assetPath);
36
+
37
+ if (!existsSync(filePath)) {
38
+ return NextResponse.json({ error: 'Not found' }, { status: 404 });
39
+ }
40
+
41
+ const ext = extname(filePath).toLowerCase();
42
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
43
+ const data = readFileSync(filePath);
44
+
45
+ return new NextResponse(data, {
46
+ headers: {
47
+ 'Content-Type': contentType,
48
+ 'Cache-Control': 'public, max-age=60',
49
+ },
50
+ });
51
+ }
@@ -1,6 +1,6 @@
1
1
  import { NextRequest } from 'next/server';
2
- import { generateSessionContent } from '@/lib/content-generator';
3
- import { verifyApiSecret, unauthorizedResponse } from '@/lib/auth';
2
+ import { generateSessionContent } from '@/lib/preview-content';
3
+ import { verifyApiSecret, unauthorizedResponse } from '@/lib/preview-auth';
4
4
 
5
5
  export async function POST(
6
6
  request: NextRequest,
@@ -1,7 +1,7 @@
1
1
  import { NextRequest } from 'next/server';
2
- import { removeSessionContent } from '@/lib/content-generator';
3
- import { clearSessionCache } from '@/lib/session-config';
4
- import { verifyApiSecret, unauthorizedResponse } from '@/lib/auth';
2
+ import { removeSessionContent } from '@/lib/preview-content';
3
+ import { clearSessionCache } from '@/lib/preview-config';
4
+ import { verifyApiSecret, unauthorizedResponse } from '@/lib/preview-auth';
5
5
 
6
6
  export async function DELETE(
7
7
  request: NextRequest,
@@ -1,6 +1,6 @@
1
1
  import { NextRequest } from 'next/server';
2
- import { syncSessionFile } from '@/lib/content-generator';
3
- import { verifyApiSecret, unauthorizedResponse } from '@/lib/auth';
2
+ import { syncSessionFile } from '@/lib/preview-content';
3
+ import { verifyApiSecret, unauthorizedResponse } from '@/lib/preview-auth';
4
4
 
5
5
  export async function POST(
6
6
  request: NextRequest,
@@ -1,12 +1,15 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { RootProvider } from 'fumadocs-ui/provider/next';
3
3
  import './global.css';
4
+ import '@core/css/shared.css';
5
+ import '@core/css/search.css';
6
+ import '@core/css/copy-page.css';
4
7
 
5
8
  export const metadata = {
6
9
  title: 'Preview',
7
10
  };
8
11
 
9
- export default function RootLayout({ children }: { children: ReactNode }) {
12
+ export default function PreviewRootLayout({ children }: { children: ReactNode }) {
10
13
  return (
11
14
  <html lang="en" suppressHydrationWarning>
12
15
  <body className="min-h-screen" suppressHydrationWarning>