diff --git a/docs/.env b/docs/.env index 16605cdde..aaa5ff6e3 100644 --- a/docs/.env +++ b/docs/.env @@ -1 +1,2 @@ STACKFRAME_URL=https://stackframe.co +STACK_HEAD_TAGS=[] diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index d49c4950b..ffd5807bd 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -26,6 +26,8 @@ const config = { onBrokenLinks: "throw", onBrokenMarkdownLinks: "warn", + headTags: JSON.parse((console.log(process.env.STACK_HEAD_TAGS), process.env.STACK_HEAD_TAGS) || "[]"), + // Even if you don't use internationalization, you can use this field to set // useful metadata like html lang. For example, if your site is Chinese, you // may want to replace "en" with "zh-Hans". diff --git a/packages/stack-server/.env b/packages/stack-server/.env index d2124acc8..fea17caf6 100644 --- a/packages/stack-server/.env +++ b/packages/stack-server/.env @@ -20,4 +20,7 @@ DIRECT_DATABASE_CONNECTION_STRING=# enter your direct (unpooled or session mode) STACK_ACCESS_TOKEN_EXPIRATION_TIME=# enter the expiration time for the access token here -NEXT_PUBLIC_DOC_URL=http://localhost:8104 +NEXT_PUBLIC_STACK_HEAD_TAGS=[{ "tagName": "script", "attributes": {}, "innerHTML": "// insert head tags here" }] + +NEXT_PUBLIC_DOC_URL=https://docs.stackframe.co + diff --git a/packages/stack-server/package.json b/packages/stack-server/package.json index a0679a9b1..e58fe91c1 100644 --- a/packages/stack-server/package.json +++ b/packages/stack-server/package.json @@ -31,6 +31,7 @@ "node": ">=20.0.0" }, "dependencies": { + "@vercel/analytics": "^1.2.2", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@mdx-js/loader": "^3", diff --git a/packages/stack-server/src/app/layout.tsx b/packages/stack-server/src/app/layout.tsx index bd0ddd6e9..e7dc5e52c 100644 --- a/packages/stack-server/src/app/layout.tsx +++ b/packages/stack-server/src/app/layout.tsx @@ -2,10 +2,13 @@ import type { Metadata } from 'next'; import {GeistSans} from 'geist/font/sans'; import {GeistMono} from "geist/font/mono"; import { SnackbarProvider } from '@/hooks/use-snackbar'; +import { Analytics } from "@vercel/analytics/react"; import './globals.css'; import ThemeProvider from '@/theme'; import { StyleLink } from '@/components/style-link'; +import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env'; +import React from 'react'; export const metadata: Metadata = { title: { @@ -15,17 +18,34 @@ export const metadata: Metadata = { description: 'Some frontend with auth built by N2D4', }; +type TagConfigJson = { + tagName: string, + attributes: { [key: string]: string }, + innerHTML: string, +}; + export default function RootLayout({ children, }: { children: React.ReactNode, }) { + const headTags: TagConfigJson[] = JSON.parse(getEnvVariable('NEXT_PUBLIC_STACK_HEAD_TAGS')); + return ( + + {headTags.map((tag, index) => { + const { tagName, attributes, innerHTML } = tag; + return React.createElement(tagName, { + key: index, + dangerouslySetInnerHTML: { __html: innerHTML ?? "" }, + ...attributes, + }); + })} + {children} diff --git a/packages/stack-shared/src/utils/env.tsx b/packages/stack-shared/src/utils/env.tsx index 1bd3a0013..d15c29134 100644 --- a/packages/stack-shared/src/utils/env.tsx +++ b/packages/stack-shared/src/utils/env.tsx @@ -1,8 +1,17 @@ import { throwErr } from "./errors"; +import { deindent } from "./strings"; /** * Returns the environment variable with the given name, throwing an error if it's undefined or the empty string. */ export function getEnvVariable(name: string): string { + if (typeof window !== 'undefined') { + throw new Error(deindent` + Can't use getEnvVariable on the client because Next.js transpiles expressions of the kind process.env.XYZ at build-time on the client. + + Use process.env.XYZ directly instead. + `); + } + return (process.env[name] ?? throwErr(`Missing environment variable: ${name}`)) || throwErr(`Empty environment variable: ${name}`); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 476a5813b..affb1a4a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -340,6 +340,9 @@ importers: '@types/mdx': specifier: ^2 version: 2.0.11 + '@vercel/analytics': + specifier: ^1.2.2 + version: 1.2.2(next@14.1.0)(react@18.2.0) bcrypt: specifier: ^5.1.1 version: 5.1.1 @@ -6202,6 +6205,22 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + /@vercel/analytics@1.2.2(next@14.1.0)(react@18.2.0): + resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==} + peerDependencies: + next: '>= 13' + react: ^18 || ^19 + peerDependenciesMeta: + next: + optional: true + react: + optional: true + dependencies: + next: 14.1.0(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + server-only: 0.0.1 + dev: false + /@vitest/expect@1.2.2: resolution: {integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==} dependencies: