-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathnuxt.config.ts
More file actions
311 lines (296 loc) · 11.9 KB
/
nuxt.config.ts
File metadata and controls
311 lines (296 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
// https://nuxt.com/docs/api/configuration/nuxt-config
import tailwindcss from "@tailwindcss/vite";
import { readEnvFlagOverrides } from "./shared/feature-flags";
const railwayEnvironmentName =
process.env.RAILWAY_ENVIRONMENT_NAME?.toLowerCase() ?? "";
const railwayPublicDomain =
process.env.RAILWAY_PUBLIC_DOMAIN?.toLowerCase() ?? "";
const siteUrl = process.env.NUXT_PUBLIC_SITE_URL || "https://reqcore.com";
const i18nDefaultLocale = "en";
const i18nLocales = [
{ code: "en", language: "en-US", name: "English", file: "en.json" },
{
code: "es",
language: "es-ES",
name: "Español",
file: "es.json",
partial: true,
},
{
code: "fr",
language: "fr-FR",
name: "Français",
file: "fr.json",
partial: true,
},
{
code: "de",
language: "de-DE",
name: "Deutsch",
file: "de.json",
partial: true,
},
{ code: "nb", language: "nb-NO", name: "Norsk Bokmål", file: "nb.json" },
{
code: "vi",
language: "vi-VN",
name: "Tiếng Việt",
file: "vi.json",
partial: true,
},
];
const localizedPublicRouteRules = Object.fromEntries(
i18nLocales
.filter((locale) => locale.code !== i18nDefaultLocale)
.flatMap((locale) => [
[`/${locale.code}/jobs`, { isr: 3600 }],
[`/${locale.code}/jobs/**`, { isr: 3600 }],
]),
);
// Allow search-engine indexing for localized job board pages
const localizedJobsRobotsRules = Object.fromEntries(
i18nLocales
.filter((locale) => locale.code !== i18nDefaultLocale)
.flatMap((locale) => [
[
`/${locale.code}/jobs`,
{ headers: { "X-Robots-Tag": "index, follow" } },
],
[
`/${locale.code}/jobs/**`,
{ headers: { "X-Robots-Tag": "index, follow" } },
],
]),
);
const isRailwayPreview =
railwayEnvironmentName.startsWith("pr") ||
railwayEnvironmentName.includes("pr-") ||
railwayEnvironmentName.includes("pull request") ||
railwayEnvironmentName.includes("pull-request") ||
railwayEnvironmentName.includes("preview") ||
railwayPublicDomain.includes("-pr-");
export default defineNuxtConfig({
compatibilityDate: "2025-07-15",
devtools: { enabled: true },
modules: [
"@nuxtjs/i18n",
"@nuxtjs/mdc",
// Only load PostHog module when the API key is available;
// the SDK crashes during prerender/build if the key is empty.
...(process.env.POSTHOG_PUBLIC_KEY ? ["@posthog/nuxt" as const] : []),
],
css: ["~/assets/css/main.css"],
// ─────────────────────────────────────────────
// PostHog — privacy-focused product analytics & feature flags
// ─────────────────────────────────────────────
// Enable source maps so PostHog error tracking can display readable stack traces
sourcemap: { client: "hidden" },
// @ts-ignore - posthogConfig types only available when @posthog/nuxt module is loaded
posthogConfig: {
publicKey: process.env.POSTHOG_PUBLIC_KEY || "",
host: process.env.POSTHOG_HOST || "https://eu.i.posthog.com",
clientConfig: {
// ── Reverse proxy: route PostHog through reqcore.com to bypass ad blockers ──
// Requests to /ingest/** are proxied by Nitro to eu.i.posthog.com
api_host: "/ingest",
ui_host: "https://eu.posthog.com",
// ── Privacy: disable invasive features ──
autocapture: false,
disable_session_recording: true,
enable_recording_console_log: false,
disable_surveys: true,
capture_pageview: true,
capture_pageleave: true,
// ── Error tracking: capture unhandled errors and rejections ──
capture_exceptions: {
capture_unhandled_errors: true,
capture_unhandled_rejections: true,
capture_console_errors: false,
},
// ── Cookieless tracking — default for visitors who haven't accepted ──
// `persistence: 'sessionStorage'` keeps the distinct_id in the tab's
// sessionStorage only. Nothing is written to cookies or persistent
// localStorage, and the id is wiped when the tab closes — there is no
// cross-session tracking and no cross-site identifier (sessionStorage
// is per-origin, per-tab).
//
// We deliberately avoid `persistence: 'memory'` here: with memory
// persistence every page navigation regenerates the distinct_id,
// which silently shatters any multi-page funnel (signup → onboarding
// → dashboard → jobs) for unconsented users — every step is attributed
// to a different anonymous person, so funnel conversion appears as 0.
//
// `person_profiles: 'identified_only'` means anonymous visitors flow as
// events without creating person profiles, while logged-in users get a
// stable profile keyed by their auth user-id (via posthog.identify()).
// This gives us reliable funnel + retention analytics for real users
// without persistently tracking anonymous visitors across sessions.
persistence: "sessionStorage",
person_profiles: "identified_only",
// ── GDPR: drop IP address from events ──
// PostHog uses $ip server-side for GeoIP, but we do not need it for the
// SaaS analytics use case. Denylisting it minimises personal data sent.
property_denylist: ["$ip", "$initial_ip"],
},
serverConfig: {
// Disabled: the @posthog/nuxt Nitro plugin captures ALL errors
// (including 404s from bot scanners). We use a filtered error hook
// in server/plugins/posthog.ts instead.
enableExceptionAutocapture: false,
},
},
i18n: {
baseUrl: siteUrl,
defaultLocale: i18nDefaultLocale,
strategy: "prefix_except_default",
locales: i18nLocales,
langDir: "locales",
detectBrowserLanguage: {
useCookie: true,
cookieKey: "reqcore_i18n_redirected",
redirectOn: "root",
},
vueI18n: "./i18n.config.ts",
},
// ─────────────────────────────────────────────
// Global <head> — lang, title template, favicon
// ─────────────────────────────────────────────
app: {
head: {
titleTemplate: "%s — Reqcore",
link: [
{ rel: "icon", type: "image/png", href: "/favicon.png" },
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
{
rel: "apple-touch-icon",
sizes: "180x180",
href: "/apple-touch-icon.png",
},
],
meta: [
{ name: "theme-color", content: "#09090b" },
{
name: "viewport",
content: "width=device-width, initial-scale=1.0, maximum-scale=5.0",
},
],
// Dark-mode init script is injected in app/app.vue via useHead() with
// the per-request nonce so it is allowed by the nonce-based CSP.
// Plausible removed — PostHog handles all analytics
},
},
runtimeConfig: {
public: {
/** Base URL of the marketing site (reqcore-web) for cross-domain links */
marketingUrl:
process.env.NUXT_PUBLIC_MARKETING_URL || "https://reqcore.com",
/** Cookie domain for cross-subdomain sharing (e.g. '.reqcore.com') */
cookieDomain: process.env.NUXT_PUBLIC_COOKIE_DOMAIN || "",
// PostHog runtimeConfig is managed by @posthog/nuxt via posthogConfig above.
// Override at runtime with NUXT_PUBLIC_POSTHOG_PUBLIC_KEY / NUXT_PUBLIC_POSTHOG_HOST.
/** When set, the dashboard shows a read-only demo banner for this org slug */
demoOrgSlug:
process.env.DEMO_ORG_SLUG || (isRailwayPreview ? "reqcore-demo" : ""),
/** Public live-demo account email used to prefill sign-in */
liveDemoEmail: (() => {
const email =
process.env.LIVE_DEMO_EMAIL ||
process.env.DEMO_EMAIL ||
"demo@reqcore.com";
// Guard against stale applirank.com domain from old env vars
if (email.endsWith("@applirank.com")) {
console.warn(
"[config] Stale demo email detected (applirank.com domain) — falling back to demo@reqcore.com",
);
return "demo@reqcore.com";
}
return email;
})(),
/** Public live-demo passcode used to prefill sign-in */
liveDemoPasscode:
process.env.LIVE_DEMO_SECRET || process.env.DEMO_PASSWORD || "demo1234",
/** Whether in-app feedback via GitHub Issues is enabled */
feedbackEnabled: !!(
process.env.GITHUB_FEEDBACK_TOKEN && process.env.GITHUB_FEEDBACK_REPO
),
/** Whether OIDC SSO is enabled (all three OIDC env vars are set) */
oidcEnabled: !!(
process.env.OIDC_CLIENT_ID &&
process.env.OIDC_CLIENT_SECRET &&
process.env.OIDC_DISCOVERY_URL
),
/** Display name for the SSO provider button */
oidcProviderName: process.env.OIDC_PROVIDER_NAME || "SSO",
/**
* Feature flag overrides forced by env vars (FEATURE_FLAG_*).
* Self-hosters use these to enable/disable flags without running PostHog.
* See `shared/feature-flags.ts` for the full registry and resolution order.
*/
// Cast: Nuxt narrows public runtime config from the registry's literal
// `defaultValue` types (boolean here), but env overrides can also be
// multivariate strings — and entries are partial. The override map is
// validated at runtime by `parseFlagOverride`, so the cast is safe.
featureFlagOverrides: readEnvFlagOverrides() as Record<
string,
boolean | string
>,
},
},
vite: {
plugins: [tailwindcss()],
},
// ─────────────────────────────────────────────
// Route rules — ISR for public job pages
// ─────────────────────────────────────────────
routeRules: {
// ── PostHog reverse proxy ──
// Handled by server/routes/ingest/[...path].ts (which routes /ingest/static/**
// to eu-assets.i.posthog.com and everything else to eu.i.posthog.com).
// Defining routeRules here would be shadowed by the server route, so we
// intentionally do not declare them.
"/jobs": { isr: 3600 },
"/jobs/**": { isr: 3600 },
...localizedPublicRouteRules,
},
nitro: {
routeRules: {
"/**": {
headers: {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
"Strict-Transport-Security":
"max-age=63072000; includeSubDomains; preload",
// Content-Security-Policy is set dynamically with a per-request
// nonce in server/middleware/csp.ts — do NOT add a static CSP here
// as it would override the nonce and break the XSS protection.
// Block indexing for all non-public routes by default;
// overridden below for /jobs/** which should be indexable.
"X-Robots-Tag": "noindex, nofollow",
},
},
// Public job board pages — allow indexing
"/jobs/**": {
headers: {
"X-Robots-Tag": "index, follow",
},
},
"/jobs": {
headers: {
"X-Robots-Tag": "index, follow",
},
},
// Localized job board pages — allow indexing
...localizedJobsRobotsRules,
// Allow same-origin framing for inline PDF preview in the sidebar iframe
"/api/documents/*/preview": {
headers: {
"X-Frame-Options": "SAMEORIGIN",
"Content-Security-Policy":
"default-src 'none'; style-src 'unsafe-inline'",
},
},
},
},
});