@aravindc26/velu 0.13.7 → 0.13.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.
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
1
2
|
import type { ReactNode } from 'react';
|
|
2
3
|
import { loadSessionConfigSource, getSessionThemeCss } from '@/lib/preview-config';
|
|
3
|
-
import { getBannerConfig, getFontsConfig, getSiteFavicon } from '@/lib/velu';
|
|
4
|
+
import { getBannerConfig, getFontsConfig, getSiteDescription, getSiteFavicon, getSiteName } from '@/lib/velu';
|
|
4
5
|
import { VeluBanner } from '@/components/banner';
|
|
5
6
|
|
|
6
7
|
interface LayoutProps {
|
|
@@ -8,6 +9,18 @@ interface LayoutProps {
|
|
|
8
9
|
params: Promise<{ sessionId: string }>;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
export async function generateMetadata({ params }: { params: Promise<{ sessionId: string }> }): Promise<Metadata> {
|
|
13
|
+
const { sessionId } = await params;
|
|
14
|
+
const configSource = loadSessionConfigSource(sessionId);
|
|
15
|
+
if (!configSource) return {};
|
|
16
|
+
const title = getSiteName(configSource);
|
|
17
|
+
const description = getSiteDescription(configSource);
|
|
18
|
+
return {
|
|
19
|
+
...(title ? { title: { default: title, template: `%s - ${title}` } } : {}),
|
|
20
|
+
...(description ? { description } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
/**
|
|
12
25
|
* Session layout: injects per-session theme CSS, Google Fonts, and banner.
|
|
13
26
|
* Uses React 19 resource hoisting (<style precedence> / <link precedence>)
|
|
@@ -6,6 +6,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { normalizeConfigNavigation } from './navigation-normalize';
|
|
8
8
|
import type { VeluConfigSource, VeluConfig } from './velu';
|
|
9
|
+
import { getFontsConfig } from './velu';
|
|
9
10
|
|
|
10
11
|
const WORKSPACE_DIR = process.env.WORKSPACE_DIR || '/mnt/nfs_share/editor_sessions';
|
|
11
12
|
const PRIMARY_CONFIG_NAME = 'docs.json';
|
|
@@ -91,38 +92,80 @@ function textColorFor(hex: string): string {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
|
-
* Generate CSS custom properties for a session's primary color theme.
|
|
95
|
+
* Generate CSS custom properties for a session's primary color theme and font overrides.
|
|
95
96
|
*/
|
|
96
97
|
export function getSessionThemeCss(sessionId: string): string | null {
|
|
97
98
|
const configSource = loadSessionConfigSource(sessionId);
|
|
98
|
-
|
|
99
|
-
if (!colors?.primary) return null;
|
|
99
|
+
if (!configSource) return null;
|
|
100
100
|
|
|
101
|
-
const { primary, light, dark } = colors;
|
|
102
|
-
const lightAccent = light || primary;
|
|
103
|
-
const darkAccent = dark || primary;
|
|
104
101
|
const lines: string[] = [];
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
// ── Color overrides ──
|
|
104
|
+
const colors = configSource.config.colors;
|
|
105
|
+
if (colors?.primary) {
|
|
106
|
+
const { primary, light, dark } = colors;
|
|
107
|
+
const lightAccent = light || primary;
|
|
108
|
+
const darkAccent = dark || primary;
|
|
109
|
+
|
|
110
|
+
if (lightAccent) {
|
|
111
|
+
const accentLow = mixColors(lightAccent, '#ffffff', 0.15);
|
|
112
|
+
lines.push(':root {');
|
|
113
|
+
lines.push(` --color-fd-primary: ${lightAccent};`);
|
|
114
|
+
lines.push(` --color-fd-primary-foreground: ${textColorFor(lightAccent)};`);
|
|
115
|
+
lines.push(` --color-fd-accent: ${accentLow};`);
|
|
116
|
+
lines.push(` --color-fd-accent-foreground: ${textColorFor(accentLow)};`);
|
|
117
|
+
lines.push(` --color-fd-ring: ${lightAccent};`);
|
|
118
|
+
lines.push('}');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (darkAccent) {
|
|
122
|
+
const accentLow = mixColors(darkAccent, '#000000', 0.3);
|
|
123
|
+
lines.push('.dark {');
|
|
124
|
+
lines.push(` --color-fd-primary: ${darkAccent};`);
|
|
125
|
+
lines.push(` --color-fd-primary-foreground: ${textColorFor(darkAccent)};`);
|
|
126
|
+
lines.push(` --color-fd-accent: ${accentLow};`);
|
|
127
|
+
lines.push(` --color-fd-accent-foreground: ${textColorFor(accentLow)};`);
|
|
128
|
+
lines.push(` --color-fd-ring: ${darkAccent};`);
|
|
129
|
+
lines.push('}');
|
|
130
|
+
}
|
|
115
131
|
}
|
|
116
132
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
// ── Font overrides ──
|
|
134
|
+
const fontsConfig = getFontsConfig(configSource);
|
|
135
|
+
if (fontsConfig) {
|
|
136
|
+
const { heading, body } = fontsConfig;
|
|
137
|
+
// @font-face for custom sources
|
|
138
|
+
for (const def of [heading, body]) {
|
|
139
|
+
if (def?.source) {
|
|
140
|
+
const fmt = def.format || (def.source.endsWith('.woff2') ? 'woff2' : 'woff');
|
|
141
|
+
lines.push(`@font-face {`);
|
|
142
|
+
lines.push(` font-family: '${def.family}';`);
|
|
143
|
+
lines.push(` src: url('${def.source}') format('${fmt}');`);
|
|
144
|
+
if (def.weight) lines.push(` font-weight: ${def.weight};`);
|
|
145
|
+
lines.push(` font-display: swap;`);
|
|
146
|
+
lines.push(`}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// CSS variable overrides
|
|
150
|
+
const vars: string[] = [];
|
|
151
|
+
if (body) {
|
|
152
|
+
vars.push(` --font-fd-sans: '${body.family}', ui-sans-serif, system-ui, sans-serif;`);
|
|
153
|
+
}
|
|
154
|
+
if (heading) {
|
|
155
|
+
vars.push(` --velu-font-heading: '${heading.family}', ui-sans-serif, system-ui, sans-serif;`);
|
|
156
|
+
}
|
|
157
|
+
if (vars.length) {
|
|
158
|
+
lines.push(':root {');
|
|
159
|
+
lines.push(...vars);
|
|
160
|
+
lines.push('}');
|
|
161
|
+
}
|
|
162
|
+
// Heading font-family rule
|
|
163
|
+
if (heading) {
|
|
164
|
+
lines.push('h1, h2, h3, h4, h5, h6 {');
|
|
165
|
+
lines.push(` font-family: var(--velu-font-heading);`);
|
|
166
|
+
if (heading.weight) lines.push(` font-weight: ${heading.weight};`);
|
|
167
|
+
lines.push('}');
|
|
168
|
+
}
|
|
126
169
|
}
|
|
127
170
|
|
|
128
171
|
return lines.length > 0 ? lines.join('\n') : null;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { NextRequest
|
|
1
|
+
import type { NextRequest } from 'next/server';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
2
3
|
import { getPreviewSecret, verifyPreviewToken } from './lib/preview-auth';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Next.js middleware that enforces HMAC-signed token authentication
|
|
6
|
-
* preview page routes.
|
|
6
|
+
* Next.js proxy (middleware) that enforces HMAC-signed token authentication
|
|
7
|
+
* for preview page routes.
|
|
7
8
|
*
|
|
8
9
|
* Flow:
|
|
9
10
|
* 1. Requests with `x-preview-secret` header pass through (server-to-server API calls).
|
|
@@ -13,7 +14,7 @@ import { getPreviewSecret, verifyPreviewToken } from './lib/preview-auth';
|
|
|
13
14
|
* 4. If a valid `__velu_preview_{sessionId}` cookie exists: allow.
|
|
14
15
|
* 5. Otherwise: 403.
|
|
15
16
|
*/
|
|
16
|
-
export function
|
|
17
|
+
export function proxy(request: NextRequest) {
|
|
17
18
|
const secret = getPreviewSecret();
|
|
18
19
|
|
|
19
20
|
// No secret configured — allow everything (dev / local)
|
|
@@ -76,16 +77,8 @@ export function middleware(request: NextRequest) {
|
|
|
76
77
|
return new NextResponse('Forbidden', { status: 403 });
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
// Use Node.js runtime so we can access the crypto module for HMAC verification
|
|
80
|
-
export const runtime = 'nodejs';
|
|
81
|
-
|
|
82
80
|
export const config = {
|
|
83
81
|
matcher: [
|
|
84
|
-
/*
|
|
85
|
-
* Match all request paths except:
|
|
86
|
-
* - _next (Next.js internals)
|
|
87
|
-
* - favicon.ico, robots.txt, sitemap.xml (static meta)
|
|
88
|
-
*/
|
|
89
82
|
'/((?!_next|favicon\\.ico|robots\\.txt|sitemap\\.xml).*)',
|
|
90
83
|
],
|
|
91
84
|
};
|