@usequark/quark-create-app 1.23.11

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 (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. package/templates/worker/src/lib/tokens.js +81 -0
@@ -0,0 +1,47 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
2
+ <title>Quark</title>
3
+ <!--
4
+ Quark logo - SVG master
5
+ Geometry mirrors QuarkAnimation.js constants (ring/dash thickness ratio ~0.20)
6
+
7
+ Ring: center (100,100), centerline radius 65, stroke-width 26
8
+ Gap: centred at 45.8° (dashAngle=0.8 rad), half-width 27.1° (0.472 rad)
9
+ Dash: radial at 45.8°, from radius 23.4 → 103.4 (proportional to animation dL)
10
+ stroke-width 28 (= dashThickness/ringThickness × 26 = 2.8/2.6 × 26)
11
+
12
+ Colors: #2d3436 (dark/top) · #377dff (blue/bottom-left) · #ff4757 (red/tail)
13
+ -->
14
+
15
+ <!-- Dark arc - upper semicircle: left (180°) → top (270°) → right (360°) -->
16
+ <path
17
+ d="M 35,100 A 65,65 0 0,1 165,100"
18
+ stroke="#2d3436"
19
+ stroke-width="26"
20
+ stroke-linecap="butt"
21
+ />
22
+
23
+ <!-- Blue arc - bottom-left: gap-end (72.9°) → left (180°) -->
24
+ <path
25
+ d="M 119.1,162.1 A 65,65 0 0,1 35,100"
26
+ stroke="#377dff"
27
+ stroke-width="26"
28
+ stroke-linecap="butt"
29
+ />
30
+
31
+ <!-- Red arc - tiny pre-gap sliver: right (0°) → gap-start (18.8°) -->
32
+ <path
33
+ d="M 165,100 A 65,65 0 0,1 161.5,121"
34
+ stroke="#ff4757"
35
+ stroke-width="26"
36
+ stroke-linecap="butt"
37
+ />
38
+
39
+ <!-- Red dash - radial tail through the gap -->
40
+ <line
41
+ x1="116.3" y1="116.8"
42
+ x2="172" y2="174.2"
43
+ stroke="#ff4757"
44
+ stroke-width="28"
45
+ stroke-linecap="butt"
46
+ />
47
+ </svg>
@@ -0,0 +1,57 @@
1
+ import "./globals.css";
2
+ import {
3
+ THEME_ATTR,
4
+ THEME_STORAGE_KEY,
5
+ } from "@techstream/quark-ui/theme-constants";
6
+ import { buildUmamiBeforeSendScript } from "../lib/analytics/umami-before-send.js";
7
+ import { getUmamiConfig } from "../lib/analytics/umami-config.js";
8
+ import { getSiteMetadata } from "../lib/seo/site-metadata.js";
9
+ import UmamiReplayRecorder from "./_components/UmamiReplayRecorder.js";
10
+ import UmamiWebVitals from "./_components/UmamiWebVitals.js";
11
+ export function generateMetadata() {
12
+ return getSiteMetadata();
13
+ }
14
+
15
+ /* Blocking script that sets data-theme before first paint (FOUC prevention). */
16
+ const themeScript = `(function(){try{var t=localStorage.getItem("${THEME_STORAGE_KEY}");if(t==="dark"||t==="light"){document.documentElement.setAttribute("${THEME_ATTR}",t)}else if(matchMedia("(prefers-color-scheme:dark)").matches){document.documentElement.setAttribute("${THEME_ATTR}","dark")}}catch(e){}})()`;
17
+
18
+ // Inline Umami before-send handler that drops events from admin pages (/admin/*)
19
+ // and any staff user (identified by the umami_user_role cookie set by the admin layout).
20
+ const umamiBeforeSendScript = buildUmamiBeforeSendScript();
21
+
22
+ export default function RootLayout({ children }) {
23
+ const umamiConfig = getUmamiConfig();
24
+
25
+ return (
26
+ <html lang="en" suppressHydrationWarning>
27
+ <head>
28
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: static constant, not user input */}
29
+ <script dangerouslySetInnerHTML={{ __html: themeScript }} />
30
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: static inline before-send handler */}
31
+ <script dangerouslySetInnerHTML={{ __html: umamiBeforeSendScript }} />
32
+ {umamiConfig.enabled ? (
33
+ <>
34
+ <link
35
+ crossOrigin="anonymous"
36
+ href={umamiConfig.origin}
37
+ rel="preconnect"
38
+ />
39
+ <link href={umamiConfig.dnsPrefetchHref} rel="dns-prefetch" />
40
+ <script
41
+ defer
42
+ data-performance="true"
43
+ data-website-id={umamiConfig.websiteId}
44
+ src={umamiConfig.scriptUrl}
45
+ data-before-send="__umamiBeforeSend"
46
+ />
47
+ </>
48
+ ) : null}
49
+ </head>
50
+ <body>
51
+ {children}
52
+ {umamiConfig.enabled ? <UmamiWebVitals /> : null}
53
+ {umamiConfig.replayEnabled ? <UmamiReplayRecorder /> : null}
54
+ </body>
55
+ </html>
56
+ );
57
+ }
@@ -0,0 +1,12 @@
1
+ import { config, getAppUrl } from "@techstream/quark-config";
2
+
3
+ export default function manifest() {
4
+ return {
5
+ name: config.appName,
6
+ short_name: config.appName,
7
+ description: config.appDescription,
8
+ start_url: "/",
9
+ display: "standalone",
10
+ id: getAppUrl(),
11
+ };
12
+ }
@@ -0,0 +1,39 @@
1
+ import Link from "next/link";
2
+
3
+ export const metadata = {
4
+ title: "Not Found",
5
+ };
6
+
7
+ export default function NotFound() {
8
+ return (
9
+ <main
10
+ className="fixed inset-0 flex flex-col items-center justify-center"
11
+ style={{ background: "var(--color-bg, #05070a)" }}
12
+ >
13
+ <p
14
+ className="font-mono uppercase"
15
+ style={{
16
+ color: "var(--color-text-faint, #3a3a4a)",
17
+ fontSize: "11px",
18
+ letterSpacing: "0.15em",
19
+ }}
20
+ >
21
+ 404
22
+ </p>
23
+ <p
24
+ className="font-mono mt-2"
25
+ style={{ color: "var(--color-text-faint, #2a2a3a)", fontSize: "11px" }}
26
+ >
27
+ page not found
28
+ </p>
29
+ <nav
30
+ className="mt-6"
31
+ style={{ fontSize: "11px", fontFamily: "monospace" }}
32
+ >
33
+ <Link href="/" className="quark-home-link">
34
+ ← home
35
+ </Link>
36
+ </nav>
37
+ </main>
38
+ );
39
+ }
@@ -0,0 +1,262 @@
1
+ import HealthIndicator from "./_components/HealthIndicator.js";
2
+ import HomeThemeToggle from "./_components/HomeThemeToggle.js";
3
+ import QuarkAnimation from "./_components/QuarkAnimation.js";
4
+ import SyncedPanels from "./_components/SyncedPanels.js";
5
+
6
+ const PANELS = [
7
+ {
8
+ title: "Need a landing page?",
9
+ description:
10
+ "Build a high-converting landing page with a hero section, social proof, and lead capture",
11
+ prompt: `You are a senior full-stack engineer building a production-ready landing page for a local business using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod validation, Biome linting). The app runs on Railway with PostgreSQL and Redis. ESM only, no TypeScript, no console.log in app code — use createLogger from @techstream/quark-core. No throw new Error — use AppError/ValidationError. All Server Actions must validate with Zod. Every Prisma model needs createdAt and updatedAt. Import UI from @techstream/quark-ui, never deep-import.
12
+
13
+ Goal: Build an SEO-optimised landing page for a local business (e.g., plumber, restaurant, salon).
14
+
15
+ Ask the user: What is the business name and type? What services do they offer? Do they have testimonials or reviews? What is the primary call-to-action (book, call, quote request)? Do they have a logo or brand colours?
16
+
17
+ Build: Hero section with business name and tagline, services list, testimonials carousel, contact form with server-side Zod validation, SEO metadata (title, description, OG tags), responsive layout. Add a ContactInquiry Prisma model for form submissions with status tracking.
18
+
19
+ Patterns to follow: Use AppError/ValidationError for error handling, prisma from @techstream/quark-db, validateBody with Zod, withCsrfProtection on POST routes.`,
20
+ },
21
+ {
22
+ title: "Want a dashboard?",
23
+ description:
24
+ "Create an interactive analytics portal with live charts, auth, and customizable metrics",
25
+ prompt: `You are a senior full-stack engineer building a management dashboard using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis, BullMQ for background jobs. ESM only, no TypeScript. Use createLogger from @techstream/quark-core for logging, AppError/ValidationError for errors. All Server Actions validate with Zod. Import UI from @techstream/quark-ui.
26
+
27
+ Goal: Build an SEO performance dashboard where users can track site metrics, keyword rankings, and crawl health.
28
+
29
+ Ask the user: What metrics matter most (traffic, rankings, backlinks, crawl errors)? How many sites should the user manage? Do they need weekly email reports? What auth provider (GitHub, Google, email/password)?
30
+
31
+ Build: Auth via NextAuth with role-based access, dashboard page with metric cards (total sites, avg rank, crawl errors), site detail page with analytics charts, keyword ranking table with sorting, crawl error list with status badges, weekly report generation via BullMQ job. Add Site, KeywordRank, CrawlError, and Report Prisma models.
32
+
33
+ Patterns to follow: Use requireAuth() for protected routes, prisma queries with safe selects, Server Actions for mutations, BullMQ job queue for report generation.`,
34
+ },
35
+ {
36
+ title: "Building a SaaS?",
37
+ description:
38
+ "Ship a multi-tenant SaaS foundation with subscription billing, team seats, and background jobs",
39
+ prompt: `You are a senior full-stack engineer building a multi-tenant SaaS application using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis, BullMQ. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod required on all Server Actions. Import UI from @techstream/quark-ui.
40
+
41
+ Goal: Build a SaaS platform with user auth, team workspaces, Stripe subscription billing, and background job processing.
42
+
43
+ Ask the user: What is the SaaS about (what problem does it solve)? What pricing tiers do they want? How many team seats per tier? Do they need a free trial? What background jobs are needed (reports, notifications, data sync)?
44
+
45
+ Build: NextAuth with GitHub + email providers, Workspace and WorkspaceMember Prisma models with roles (admin/editor/viewer), Stripe checkout + webhook handling for subscriptions, team invite flow, dashboard with workspace-scoped data, BullMQ worker for background jobs. Add Subscription and Invoice models.
46
+
47
+ Patterns to follow: Use Workspace-scoped queries (filter by workspaceId), requireRole('admin') for admin actions, safe selects for user data, Stripe webhook signature verification, job queue with retries.`,
48
+ },
49
+ {
50
+ title: "Running a store?",
51
+ description:
52
+ "Launch an e-commerce store with a searchable catalog, shopping cart, and Stripe checkout",
53
+ prompt: `You are a senior full-stack engineer building an e-commerce storefront using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis, BullMQ. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
54
+
55
+ Goal: Build a product catalog with shopping cart, Stripe checkout, and order management.
56
+
57
+ Ask the user: What kind of products (physical, digital, subscriptions)? Do they need inventory tracking? What payment methods beyond Stripe (PayPal)? Do they need shipping calculation? How should order confirmation work (email via BullMQ job)?
58
+
59
+ Build: Product and Category Prisma models with images, shopping cart, Stripe checkout session creation, webhook handler for payment confirmation, Order and OrderItem models with status workflow (PENDING → CONFIRMED → SHIPPED → DELIVERED), order history page, admin product management. Add a BullMQ job for order confirmation emails.
60
+
61
+ Patterns to follow: Use Decimal for prices, cascade deletes for owned relationships, withCsrfProtection on POST routes, validateBody with Zod schemas for product/order creation.`,
62
+ },
63
+ {
64
+ title: "Publishing content?",
65
+ description:
66
+ "Start a modern blog engine with MDX posts, category tags, instant search, and RSS",
67
+ prompt: `You are a senior full-stack engineer building a content platform using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
68
+
69
+ Goal: Build a blog with MDX support, tag filtering, syntax highlighting, and an RSS feed.
70
+
71
+ Ask the user: Who is the author? What categories/tags should exist? Do they need a drafts workflow (draft → published)? Should posts support code snippets (what languages)? Do they need an admin editor or is a CMS-style admin sufficient?
72
+
73
+ Build: Post and Tag Prisma models with many-to-many relation, MDX rendering with syntax highlighting, tag-based filtering pages, RSS feed route handler (/api/rss), sitemap generation, admin post editor with draft preview, SEO metadata per post.
74
+
75
+ Patterns to follow: Use Post model with published Boolean, include author relation with safe select, paginate with skip/take, tag queries via relation filters.`,
76
+ },
77
+ {
78
+ title: "Need a booking system?",
79
+ description:
80
+ "Set up automated scheduling with calendar sync, client availability, and event reminders",
81
+ prompt: `You are a senior full-stack engineer building a booking system using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis, BullMQ for notifications. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
82
+
83
+ Goal: Build an appointment booking system with service listings, calendar availability, and email notifications.
84
+
85
+ Ask the user: What services are offered (haircut, consultation, repair)? What are the business hours? How far in advance can users book? Do they need staff/provider assignment? Should reminders be sent (24h before via BullMQ)?
86
+
87
+ Build: Service, Booking, Availability, and Staff Prisma models, calendar view showing available time slots, booking creation with conflict detection, email confirmation via BullMQ job, reminder notifications, admin calendar view with booking management, cancellation/rescheduling flow.
88
+
89
+ Patterns to follow: Use DateTime fields for scheduling, unique constraints to prevent double-booking, Server Actions for booking mutations, BullMQ for async email delivery.`,
90
+ },
91
+ {
92
+ title: "Building a portfolio?",
93
+ description:
94
+ "Showcase your work with a developer portfolio, project highlights, and a contact form",
95
+ prompt: `You are a senior full-stack engineer building a developer portfolio using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
96
+
97
+ Goal: Build a personal developer portfolio with project showcase, case study pages, and a contact form.
98
+
99
+ Ask the user: What is the developer's name and tagline? What projects should be featured (give 3-5 examples with descriptions, tech stacks, and links)? Do they need a blog section? What social links (GitHub, LinkedIn, Twitter)? Should the theme toggle between dark and light?
100
+
101
+ Build: Project and Skill Prisma models, home page with hero + featured projects grid, individual project pages with description, tech stack badges, and live/repo links, about page with skills and experience, contact form with Zod validation, dark/light theme toggle (use ThemeProvider from @techstream/quark-ui), responsive design.
102
+
103
+ Patterns to follow: Use Card components from @techstream/quark-ui, Badge for tech stack, contact form with ContactInquiry model, SEO metadata per page.`,
104
+ },
105
+ {
106
+ title: "Need an admin panel?",
107
+ description:
108
+ "Manage your app with an internal admin portal featuring CRUD workflows, roles, and audit logs",
109
+ prompt: `You are a senior full-stack engineer building an admin panel using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
110
+
111
+ Goal: Build an internal admin dashboard with role-based access, data tables, and audit logging.
112
+
113
+ Ask the user: What models need admin CRUD (products, orders, users, content)? What roles exist (super-admin, admin, editor)? What actions should be audit-logged? Do they need bulk actions (delete, export)? Should the admin have a dashboard with metric cards?
114
+
115
+ Build: Role-based auth with requireRole() middleware, auto-generated CRUD pages for each model, data tables with sorting/filtering/pagination, bulk actions with confirmation dialogs, AuditLog Prisma model tracking all mutations, admin dashboard with metric cards, custom sidebar with model links.
116
+
117
+ Patterns to follow: Use adminConfig.modelOverrides for model customization, replace individual model pages for custom UI, add AuditLog model with createdAt index, use requireRole('admin') on all admin Server Actions.`,
118
+ },
119
+ {
120
+ title: "Building a real-time app?",
121
+ description:
122
+ "Go live with real-time collaboration using WebSockets, presence indicators, and state sync",
123
+ prompt: `You are a senior full-stack engineer building a real-time collaborative app using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis (for pub/sub and presence), BullMQ for background processing. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
124
+
125
+ Goal: Build a real-time collaborative application with live updates, user presence, and persistent state.
126
+
127
+ Ask the user: What are users collaborating on (documents, whiteboards, tasks, spreadsheets)? How many concurrent users per room? Do they need cursor/presence indicators? Should changes persist to the database? Do they need conflict resolution?
128
+
129
+ Build: Room and Document Prisma models, WebSocket server (use ws or Socket.io with Redis pub/sub for multi-instance), user presence tracking via Redis, live cursor/selection indicators, operational transform or CRDT for conflict resolution, persistent state saved to PostgreSQL via BullMQ debounced writes, room creation/joining flow, user avatars with presence status.
130
+
131
+ Patterns to follow: Use Redis for real-time pub/sub, BullMQ for debounced persistence, Prisma for durable state, WebSocket auth verification against NextAuth session, presence TTL with Redis EXPIRE.`,
132
+ },
133
+ {
134
+ title: "Integrating AI features?",
135
+ description:
136
+ "Add AI capabilities with streaming responses, prompt templates, vector search, and token tracking",
137
+ prompt: `You are a senior full-stack engineer building AI-powered features using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis, BullMQ for background processing. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
138
+
139
+ Goal: Add AI capabilities to an existing Quark application — streaming chat, prompt management, vector search, and usage tracking.
140
+
141
+ Ask the user: Which LLM provider (OpenAI, Anthropic, local model)? What is the primary use case (chat, content generation, search)? Do they need prompt templates management? Should conversations be persisted? Do they need token usage tracking and budget alerts?
142
+
143
+ Build: Conversation and Message Prisma models for chat history, streaming response handler using Server-Sent Events, PromptTemplate model for reusable prompts with versioning, vector embedding storage with pgvector for semantic search, token usage tracking model with daily aggregation via BullMQ, rate limiting per user. Add a settings page for API key management.
144
+
145
+ Patterns to follow: Use Server-Sent Events for streaming, Prisma for conversation persistence, pgvector extension for vector search, BullMQ for async embedding generation and usage aggregation, safe selects to never expose API keys.`,
146
+ },
147
+ {
148
+ title: "Building a community?",
149
+ description:
150
+ "Launch a discussion forum with user profiles, nested comments, upvoting, and moderation",
151
+ prompt: `You are a senior full-stack engineer building a community platform using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis, BullMQ for background jobs. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
152
+
153
+ Goal: Build a discussion forum with user-generated content, threading, voting, and moderation tools.
154
+
155
+ Ask the user: What is the community about? Do they need categories or tags for topics? Should posts support rich text or Markdown? Do they need user reputation/karma? What moderation tools are needed (flag, ban, review queue)?
156
+
157
+ Build: Post, Comment (with parentComment for nesting), Vote, and Category Prisma models, user profiles with avatar and bio, topic listing with sorting (new, top, hot), nested comment threads, upvote/downvote system with optimistic UI, moderation queue with flag/review actions, admin moderation dashboard. Add BullMQ jobs for email notifications on replies.
158
+
159
+ Patterns to follow: Use nested set or parent relation for comment threading, optimistic locking for votes, requireAuth() for mutations, AuditLog for moderation actions, safe user selects excluding sensitive fields.`,
160
+ },
161
+ {
162
+ title: "Creating an API?",
163
+ description:
164
+ "Build a scalable REST or GraphQL API with rate limiting, OpenAPI docs, and secure auth",
165
+ prompt: `You are a senior full-stack engineer building an API layer using Quark (Next.js 16, App Router, Route Handlers, Prisma, Zod, Biome). Stack: PostgreSQL, Redis for rate limiting and caching. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all API routes. Import UI from @techstream/quark-ui where needed.
166
+
167
+ Goal: Build a well-documented, secure API with authentication, rate limiting, and OpenAPI specification.
168
+
169
+ Ask the user: REST or GraphQL? Which resources/endpoints are needed? What auth method (API keys, OAuth2, JWT)? What are the rate limits per tier? Do they need webhook support for third-party integrations?
170
+
171
+ Build: API key management model with scopes and expiry, rate limiting middleware using Redis (sliding window), OpenAPI/Swagger documentation route, Zod-validated request/response schemas for every endpoint, CORS configuration, request logging with correlation IDs, webhook delivery model with retry logic via BullMQ. Add an API playground page for testing.
172
+
173
+ Patterns to follow: Use Next.js Route Handlers for REST endpoints, Redis INCR with TTL for rate limits, Zod schemas as single source of truth for validation and docs, withCsrfProtection on state-changing routes, BullMQ for async webhook delivery with exponential backoff.`,
174
+ },
175
+ {
176
+ title: "Need a marketplace?",
177
+ description:
178
+ "Connect buyers and sellers with vendor onboarding, split payments, and search filters",
179
+ prompt: `You are a senior full-stack engineer building a marketplace platform using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis, BullMQ for background jobs. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
180
+
181
+ Goal: Build a two-sided marketplace connecting buyers and sellers with vendor management and payment splitting.
182
+
183
+ Ask the user: What is being sold (physical goods, digital products, services)? What is the commission structure? Do vendors need onboarding verification? What search and filter capabilities are needed? How should dispute resolution work?
184
+
185
+ Build: Vendor, Listing, Order, and Payout Prisma models, vendor onboarding flow with profile verification, product listing with images and search (full-text with PostgreSQL tsvector), shopping cart and checkout with Stripe Connect for split payments, order management with buyer/seller views, review and rating system, vendor payout scheduling via BullMQ, admin moderation dashboard.
186
+
187
+ Patterns to follow: Use Stripe Connect for marketplace payments, full-text search with PostgreSQL tsvector, Prisma transactions for order creation with inventory deduction, BullMQ for async payout processing, role-based access with vendor/admin/buyer roles.`,
188
+ },
189
+ {
190
+ title: "Building a mobile app?",
191
+ description:
192
+ "Kickstart a cross-platform mobile app with native navigation, offline storage, and push alerts",
193
+ prompt: `You are a senior full-stack engineer building a mobile app backend and web companion using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL, Redis, BullMQ for background jobs. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
194
+
195
+ Goal: Build the backend API and data layer for a cross-platform mobile app, with a responsive web companion.
196
+
197
+ Ask the user: What platform (React Native, Flutter, or web PWA)? What are the core features? Do they need offline sync? What push notification service (Firebase, Expo)? Do they need file/image upload with storage?
198
+
199
+ Build: Device and PushToken Prisma models for push notification management, REST API layer for mobile consumption with JWT auth, file upload endpoint with S3-compatible storage (Railway bucket), offline sync strategy with last-write-wins or conflict detection, push notification delivery via BullMQ job, responsive web companion using the same Prisma models and Server Actions. Add device registration endpoint for push token management.
200
+
201
+ Patterns to follow: Use JWT for mobile auth (separate from NextAuth web session), S3-compatible object storage for file uploads, BullMQ for async push notification delivery, Prisma for all data persistence, API route handlers alongside Server Actions for mobile consumption.`,
202
+ },
203
+ {
204
+ title: "Setting up docs?",
205
+ description:
206
+ "Publish developer documentation with full-text search, code blocks, and version switching",
207
+ prompt: `You are a senior full-stack engineer building a documentation site using Quark (Next.js 16, App Router, Server Actions, Prisma, Tailwind via @techstream/quark-ui, Zod, Biome). Stack: PostgreSQL for search indexing. ESM only, no TypeScript. Use AppError/ValidationError from @techstream/quark-core/errors, createLogger for logging. Zod on all Server Actions. Import UI from @techstream/quark-ui.
208
+
209
+ Goal: Build a developer documentation site with search, code examples, and version management.
210
+
211
+ Ask the user: What is being documented (API, framework, product)? How many versions need to be supported? Do they need a changelog? Should code blocks support multiple languages? Do they need interactive examples (runnable code)?
212
+
213
+ Build: DocPage and DocVersion Prisma models with parent/child nesting, MDX rendering with syntax highlighting and callout blocks, full-text search using PostgreSQL tsvector with ranking, version switching (v1, v2, latest), sidebar navigation auto-generated from document hierarchy, changelog page with entries linked to versions, copy-to-clipboard on code blocks, dark/light theme toggle.
214
+
215
+ Patterns to follow: Use PostgreSQL tsvector for full-text search with GIN index, MDX for content authoring, Prisma for document hierarchy and versioning, static generation where possible for performance, SEO metadata per documentation page.`,
216
+ },
217
+ ];
218
+
219
+ export default function Home() {
220
+ return (
221
+ <main className="quark-home-main min-h-screen flex flex-col items-center justify-center">
222
+ <QuarkAnimation />
223
+
224
+ <div className="quark-home-footer flex flex-col items-center gap-6 pt-8 pb-8">
225
+ <SyncedPanels panels={PANELS} />
226
+
227
+ <nav className="flex items-center gap-2">
228
+ <a
229
+ href="https://github.com/usequark/quark"
230
+ target="_blank"
231
+ rel="noopener noreferrer"
232
+ className="quark-home-link"
233
+ >
234
+ github
235
+ </a>
236
+ <span className="quark-home-sep">·</span>
237
+ <a
238
+ href="https://www.npmjs.com/package/@techstream/quark-create-app"
239
+ target="_blank"
240
+ rel="noopener noreferrer"
241
+ className="quark-home-link"
242
+ >
243
+ npm
244
+ </a>
245
+ <span className="quark-home-sep">·</span>
246
+ <a
247
+ href="https://quark.dev"
248
+ target="_blank"
249
+ rel="noopener noreferrer"
250
+ className="quark-home-link"
251
+ >
252
+ quark
253
+ </a>
254
+ <span className="quark-home-sep">·</span>
255
+ <HomeThemeToggle />
256
+ </nav>
257
+
258
+ <HealthIndicator />
259
+ </div>
260
+ </main>
261
+ );
262
+ }
@@ -0,0 +1,23 @@
1
+ import { getAppUrl } from "@techstream/quark-config";
2
+ import { isWebsiteIndexable } from "../lib/seo/indexing.js";
3
+
4
+ export const revalidate = 3600;
5
+
6
+ export default function robots() {
7
+ const appUrl = getAppUrl();
8
+ const indexable = isWebsiteIndexable();
9
+
10
+ return {
11
+ rules: indexable
12
+ ? {
13
+ userAgent: "*",
14
+ allow: "/",
15
+ disallow: ["/api/", "/admin/", "/auth/"],
16
+ }
17
+ : {
18
+ userAgent: "*",
19
+ disallow: "/",
20
+ },
21
+ sitemap: indexable ? `${appUrl}/sitemap.xml` : undefined,
22
+ };
23
+ }
@@ -0,0 +1,14 @@
1
+ import { getAppUrl } from "@techstream/quark-config";
2
+ import { STATIC_SITEMAP_ROUTES, buildSitemapEntries } from "../lib/sitemap-entries.js";
3
+ import { isWebsiteIndexable } from "../lib/seo/indexing.js";
4
+
5
+ export const revalidate = 3600;
6
+
7
+ export default async function sitemap() {
8
+ if (!isWebsiteIndexable()) {
9
+ return [];
10
+ }
11
+
12
+ const appUrl = getAppUrl();
13
+ return buildSitemapEntries({ appUrl });
14
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pure decision helper for the Umami data-before-send hook.
3
+ * Returns true when the payload should be dropped.
4
+ *
5
+ * @param {{ url?: string, roleCookie?: string | null }} input
6
+ * @returns {boolean}
7
+ */
8
+ export function shouldBlockUmamiPayload({ url, roleCookie } = {}) {
9
+ if (roleCookie) return true;
10
+ if (!url) return false;
11
+ try {
12
+ const pathname = new URL(url, "http://localhost").pathname;
13
+ return pathname.startsWith("/admin");
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Build a self-contained inline script that defines window.__umamiBeforeSend.
21
+ * Embeds shouldBlockUmamiPayload via .toString() so the inline handler and
22
+ * unit-tested pure function cannot drift.
23
+ *
24
+ * @returns {string}
25
+ */
26
+ export function buildUmamiBeforeSendScript() {
27
+ // shouldBlockUmamiPayload.toString() already includes the "function name(){...}" form.
28
+ return `window.__umamiBeforeSend=function(t,p){var r=(document.cookie.match(/umami_user_role=([^;]+)/)||[])[1];if(shouldBlockUmamiPayload({url:p.url,roleCookie:r}))return false;return p};${shouldBlockUmamiPayload.toString()}`;
29
+ }
@@ -0,0 +1,95 @@
1
+ const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
2
+ const UMAMI_WEBSITE_ID_PATTERN =
3
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4
+
5
+ export function normalizeUmamiUrl(value) {
6
+ if (typeof value !== "string") return "";
7
+
8
+ const trimmed = value.trim();
9
+ if (!trimmed) return "";
10
+
11
+ let parsedUrl;
12
+ try {
13
+ parsedUrl = new URL(trimmed);
14
+ } catch {
15
+ return "";
16
+ }
17
+
18
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
19
+ return "";
20
+ }
21
+
22
+ parsedUrl.search = "";
23
+ parsedUrl.hash = "";
24
+ parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, "") || "/";
25
+
26
+ const normalized = parsedUrl.toString();
27
+ return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
28
+ }
29
+
30
+ export function getUmamiOrigin(value) {
31
+ const normalizedUrl = normalizeUmamiUrl(value);
32
+ if (!normalizedUrl) return "";
33
+
34
+ return new URL(normalizedUrl).origin;
35
+ }
36
+
37
+ export function getUmamiDnsPrefetchHref(value) {
38
+ const normalizedUrl = normalizeUmamiUrl(value);
39
+ if (!normalizedUrl) return "";
40
+
41
+ return `//${new URL(normalizedUrl).host}`;
42
+ }
43
+
44
+ export function getUmamiScriptUrl(value) {
45
+ const normalizedUrl = normalizeUmamiUrl(value);
46
+ if (!normalizedUrl) return "";
47
+
48
+ return `${normalizedUrl}/script.js`;
49
+ }
50
+
51
+ export function parseUmamiBooleanFlag(value) {
52
+ if (typeof value !== "string") return false;
53
+ return TRUE_VALUES.has(value.trim().toLowerCase());
54
+ }
55
+
56
+ export function isUmamiWebsiteId(value) {
57
+ if (typeof value !== "string") return false;
58
+ return UMAMI_WEBSITE_ID_PATTERN.test(value.trim());
59
+ }
60
+
61
+ export function getUmamiConfig(env = process.env) {
62
+ const url = normalizeUmamiUrl(env.NEXT_PUBLIC_UMAMI_URL);
63
+ const rawWebsiteId =
64
+ typeof env.NEXT_PUBLIC_UMAMI_WEBSITE_ID === "string"
65
+ ? env.NEXT_PUBLIC_UMAMI_WEBSITE_ID.trim()
66
+ : "";
67
+ const websiteId = isUmamiWebsiteId(rawWebsiteId) ? rawWebsiteId : "";
68
+ const enabled = Boolean(url && websiteId);
69
+
70
+ return {
71
+ url,
72
+ origin: getUmamiOrigin(url),
73
+ dnsPrefetchHref: getUmamiDnsPrefetchHref(url),
74
+ scriptUrl: getUmamiScriptUrl(url),
75
+ websiteId,
76
+ enabled,
77
+ replayEnabled:
78
+ enabled && parseUmamiBooleanFlag(env.NEXT_PUBLIC_UMAMI_REPLAY_ENABLED),
79
+ };
80
+ }
81
+
82
+ export function getUmamiCspOrigins(env = process.env) {
83
+ const config = getUmamiConfig(env);
84
+ if (!config.enabled || !config.origin) {
85
+ return {
86
+ scriptSrc: [],
87
+ connectSrc: [],
88
+ };
89
+ }
90
+
91
+ return {
92
+ scriptSrc: [config.origin],
93
+ connectSrc: [config.origin],
94
+ };
95
+ }
@@ -0,0 +1,24 @@
1
+ import { getUmamiCspOrigins } from "./umami-config.js";
2
+
3
+ export function buildContentSecurityPolicy(env = process.env) {
4
+ const isProduction = env.NODE_ENV === "production";
5
+ const { connectSrc: umamiConnectSrc, scriptSrc: umamiScriptSrc } =
6
+ getUmamiCspOrigins(env);
7
+
8
+ const scriptSrc = [
9
+ "script-src",
10
+ "'self'",
11
+ "'unsafe-inline'",
12
+ ...(isProduction ? [] : ["'unsafe-eval'"]),
13
+ ...umamiScriptSrc,
14
+ ].join(" ");
15
+
16
+ const connectSrc = [
17
+ "connect-src",
18
+ "'self'",
19
+ ...(isProduction ? [] : ["ws:", "wss:"]),
20
+ ...umamiConnectSrc,
21
+ ].join(" ");
22
+
23
+ return `default-src 'self'; ${scriptSrc}; ${connectSrc}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self';`;
24
+ }