diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32724533c5..e55813f4c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,50 +146,11 @@ jobs: - name: Build the Docker image env: - ORIGIN: http://fakelearn.odl.local:8062 - MITOL_API_BASE_URL: http://api.fakelearn.odl.local:8065 - MITX_ONLINE_LEGACY_BASE_URL: https://cifake.mitxonline.mit.edu - SITE_NAME: MIT Learn - SUPPORT_EMAIL: mitlearn-support@mit.edu - EMBEDLY_KEY: fake-embedly-key - MITOL_AXIOS_WITH_CREDENTIALS: true - CSRF_COOKIE_NAME: learn_csrftoken_ci - POSTHOG_API_HOST: https://app.posthog.com - POSTHOG_UI_HOST: https://us.posthog.com - POSTHOG_PROJECT_ID: fake-posthog-project-id - POSTHOG_API_KEY: fake-posthog-api-key - SENTRY_DSN: fake-sentry-dsn - SENTRY_ENV: fake-sentry-env - SENTRY_PROFILES_SAMPLE_RATE: 0.1 - SENTRY_TRACES_SAMPLE_RATE: 0.1 - LEARN_AI_RECOMMENDATION_ENDPOINT: http://api.fakelearn.odl.local:8065/ai/http/recommendation_agent - LEARN_AI_SYLLABUS_ENDPOINT: http://api.fakelearn.odl.local:8065/ai/http/syllabus_agent - HUBSPOT_PORTAL_ID: fake-hubspot-portal-id VERSION: ${{ github.sha }} run: | docker build \ -f frontends/main/Dockerfile.web \ - --build-arg NEXT_PUBLIC_ORIGIN=$ORIGIN \ - --build-arg NEXT_PUBLIC_MITOL_API_BASE_URL=$MITOL_API_BASE_URL \ - --build-arg NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL=$MITX_ONLINE_LEGACY_BASE_URL \ - --build-arg NEXT_PUBLIC_SITE_NAME="$SITE_NAME" \ - --build-arg NEXT_PUBLIC_MITOL_SUPPORT_EMAIL=$SUPPORT_EMAIL \ - --build-arg NEXT_PUBLIC_EMBEDLY_KEY=$EMBEDLY_KEY \ - --build-arg NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS=$MITOL_AXIOS_WITH_CREDENTIALS \ - --build-arg NEXT_PUBLIC_CSRF_COOKIE_NAME=$CSRF_COOKIE_NAME \ - --build-arg NEXT_PUBLIC_POSTHOG_API_HOST=$POSTHOG_API_HOST \ - --build-arg NEXT_PUBLIC_POSTHOG_UI_HOST=$POSTHOG_UI_HOST \ - --build-arg NEXT_PUBLIC_POSTHOG_PROJECT_ID=$POSTHOG_PROJECT_ID \ - --build-arg NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ - --build-arg NEXT_PUBLIC_SENTRY_DSN=$SENTRY_DSN \ - --build-arg NEXT_PUBLIC_SENTRY_ENV=$SENTRY_ENV \ - --build-arg NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE=$SENTRY_PROFILES_SAMPLE_RATE \ - --build-arg NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$SENTRY_TRACES_SAMPLE_RATE \ - --build-arg NEXT_PUBLIC_APPZI_URL=$APPZI_URL \ - --build-arg NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT=$LEARN_AI_RECOMMENDATION_ENDPOINT \ - --build-arg NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT=$LEARN_AI_SYLLABUS_ENDPOINT \ - --build-arg NEXT_PUBLIC_HUBSPOT_PORTAL_ID=$HUBSPOT_PORTAL_ID \ - --build-arg NEXT_PUBLIC_VERSION=$VERSION \ + --build-arg GIT_REF=$VERSION \ -t mitodl/mit-learn-frontend:$VERSION . build-storybook: diff --git a/frontends/main/Dockerfile.web b/frontends/main/Dockerfile.web index 2fa3a6961d..1295a2b354 100644 --- a/frontends/main/Dockerfile.web +++ b/frontends/main/Dockerfile.web @@ -2,25 +2,12 @@ # Build: \ # docker build \ # -f frontends/main/Dockerfile.web \ -# --build-arg NEXT_PUBLIC_ORIGIN=http://api.open.odl.local:8062 \ -# --build-arg NEXT_PUBLIC_MITOL_API_BASE_URL=http://open.odl.local:8063 \ -# --build-arg NEXT_PUBLIC_SITE_NAME="MIT Learn" \ -# --build-arg NEXT_PUBLIC_MITOL_SUPPORT_EMAIL=mitlearn-support@mit.edu \ -# --build-arg NEXT_PUBLIC_EMBEDLY_KEY= \ -# --build-arg NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS=true \ -# --build-arg NEXT_PUBLIC_CSRF_COOKIE_NAME=csrftoken-local \ -# --build-arg NEXT_PUBLIC_POSTHOG_API_HOST= \ -# --build-arg NEXT_PUBLIC_POSTHOG_PROJECT_ID= \ -# --build-arg NEXT_PUBLIC_POSTHOG_API_KEY= \ -# --build-arg NEXT_PUBLIC_POSTHOG_ENABLE_SESSION_RECORDING= \ -# --build-arg NEXT_PUBLIC_SENTRY_DSN= \ -# --build-arg NEXT_PUBLIC_SENTRY_ENV= \ -# --build-arg NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE= \ -# --build-arg NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE= \ -# --build-arg NEXT_PUBLIC_APPZI_URL= \ -# --build-arg NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT= \ -# --build-arg NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT= \ --build-arg NEXT_PUBLIC_HUBSPOT_PORTAL_ID= \# --build-arg NEXT_PUBLIC_VERSION= \ +# --build-arg GIT_REF=$(git rev-parse HEAD) \ # -t mitodl/mit-learn-frontend:latest . +# +# NEXT_PUBLIC_* vars are NOT passed as build args — they are injected at +# runtime by Kubernetes and surfaced to the browser via PublicEnvScript. +# Only GIT_REF is needed at build time (for generateBuildId in next.config.js). # Run: @@ -83,77 +70,12 @@ WORKDIR /app/frontends/main ENV NODE_ENV=production -ARG NEXT_PUBLIC_ORIGIN -ENV NEXT_PUBLIC_ORIGIN=$NEXT_PUBLIC_ORIGIN - -ARG NEXT_PUBLIC_MITOL_API_BASE_URL -ENV NEXT_PUBLIC_MITOL_API_BASE_URL=$NEXT_PUBLIC_MITOL_API_BASE_URL - -ARG NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL -ENV NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL=$NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL - -ARG NEXT_PUBLIC_SITE_NAME -ENV NEXT_PUBLIC_SITE_NAME=$NEXT_PUBLIC_SITE_NAME - -ARG NEXT_PUBLIC_MITOL_SUPPORT_EMAIL -ENV NEXT_PUBLIC_MITOL_SUPPORT_EMAIL=$NEXT_PUBLIC_MITOL_SUPPORT_EMAIL - -ARG NEXT_PUBLIC_EMBEDLY_KEY=None -ENV NEXT_PUBLIC_EMBEDLY_KEY=$NEXT_PUBLIC_EMBEDLY_KEY - -ARG NEXT_PUBLIC_SENTRY_DSN -ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN - -ARG NEXT_PUBLIC_SENTRY_ENV -ENV NEXT_PUBLIC_SENTRY_ENV=$NEXT_PUBLIC_SENTRY_ENV - -ARG NEXT_PUBLIC_VERSION -ENV NEXT_PUBLIC_VERSION=$NEXT_PUBLIC_VERSION - -ARG NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE -ENV NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE=$NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE - -ARG NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE -ENV NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE - -ARG NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS=true -ENV NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS=$NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS - -ARG NEXT_PUBLIC_CSRF_COOKIE_NAME -ENV NEXT_PUBLIC_CSRF_COOKIE_NAME=$NEXT_PUBLIC_CSRF_COOKIE_NAME - -ARG NEXT_PUBLIC_POSTHOG_API_HOST -ENV NEXT_PUBLIC_POSTHOG_API_HOST=$NEXT_PUBLIC_POSTHOG_API_HOST -ARG NEXT_PUBLIC_POSTHOG_UI_HOST -ENV NEXT_PUBLIC_POSTHOG_UI_HOST=$NEXT_PUBLIC_POSTHOG_UI_HOST -ARG NEXT_PUBLIC_POSTHOG_PROJECT_ID -ENV NEXT_PUBLIC_POSTHOG_PROJECT_ID=$NEXT_PUBLIC_POSTHOG_PROJECT_ID -ARG NEXT_PUBLIC_POSTHOG_API_KEY -ENV NEXT_PUBLIC_POSTHOG_API_KEY=$NEXT_PUBLIC_POSTHOG_API_KEY - -ARG NEXT_PUBLIC_SENTRY_DSN -ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN -ARG NEXT_PUBLIC_SENTRY_ENV -ENV NEXT_PUBLIC_SENTRY_ENV=$NEXT_PUBLIC_SENTRY_ENV -ARG NEXT_PUBLIC_VERSION -ENV NEXT_PUBLIC_VERSION=$NEXT_PUBLIC_VERSION -ARG NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE -ENV NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE=$NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE -ARG NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE -ENV NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE - -ARG NEXT_PUBLIC_APPZI_URL -ENV NEXT_PUBLIC_APPZI_URL=$NEXT_PUBLIC_APPZI_URL - -ARG NEXT_PUBLIC_LEARN_AI_CSRF_COOKIE_NAME -ENV NEXT_PUBLIC_LEARN_AI_CSRF_COOKIE_NAME=$NEXT_PUBLIC_LEARN_AI_CSRF_COOKIE_NAME -ARG NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT -ENV NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT=$NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT -ARG NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT -ENV NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT=$NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT - -ARG NEXT_PUBLIC_HUBSPOT_PORTAL_ID -ENV NEXT_PUBLIC_HUBSPOT_PORTAL_ID=$NEXT_PUBLIC_HUBSPOT_PORTAL_ID +# GIT_REF is the full commit SHA, passed by Concourse as BUILD_ARG_GIT_REF. +# It is used by next.config.js generateBuildId to produce a stable, unique +# build manifest filename that is the same across identical builds. +# All NEXT_PUBLIC_* vars are injected at runtime — NOT at build time. +ARG GIT_REF +ENV GIT_REF=$GIT_REF ENV NEXT_PUBLIC_DEFAULT_SEARCH_MODE="phrase" ENV NEXT_PUBLIC_DEFAULT_SEARCH_SLOP="6" @@ -161,32 +83,51 @@ ENV NEXT_PUBLIC_DEFAULT_SEARCH_STALENESS_PENALTY="2.5" ENV NEXT_PUBLIC_DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF="0" ENV NEXT_PUBLIC_DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY="90" -# NEW STAGE: build_skip_yarn -# This stage is intended for scenarios where the 'yarn build' step -# is not needed (e.g., build artifacts are provided externally or -# for a development setup where 'yarn dev' might be used). -# Note: 'yarn start' in the CMD requires a prior build to have occurred -# and the '.next' directory to be present. -FROM base AS build_skip_yarn +# STAGE: build +# Runs `next build` to compile the application. With output: "standalone" in +# next.config.js this emits a minimal server at .next/standalone/ that includes +# only the required runtime files and can be run with plain node. +# +# NEXT_BUILD_CI=1 skips validateEnv() in next.config.js, which would fail +# because NEXT_PUBLIC_* vars are not available at build time — they are +# injected at runtime by Kubernetes and surfaced to the browser via +# PublicEnvScript. Validation runs at server startup in instrumentation-node.ts. +FROM base AS build -EXPOSE 3000 +ENV NEXT_BUILD_CI=1 -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" +RUN yarn build -CMD ["yarn", "start"] +# STAGE: runner (default) +# Copies only the standalone server, static assets, and public directory from +# the build stage into a clean image. No workspace install is needed; the +# standalone bundle ships with the minimal node_modules required to run the +# server. No source code or build tooling is included. +# +# The standalone server is started directly with node (not `yarn start`), which +# avoids yarn overhead and is the pattern recommended by Next.js for Docker: +# https://github.com/vercel/next.js/tree/canary/examples/with-docker +FROM node:24-alpine AS runner -# DEFAULT STAGE: build -# This stage performs the 'yarn build' step and is the default target -# if no --target is specified during the Docker build command. -FROM base AS build +WORKDIR /app -RUN yarn build +# sharp (Next.js image optimisation) requires glibc-compatible libs on Alpine. +RUN apk add --no-cache libc6-compat + +# Copy the standalone server (includes a minimal node_modules) +COPY --from=build /app/frontends/main/.next/standalone ./ + +# Copy static assets served by the Next.js server under /_next/static/ +COPY --from=build /app/frontends/main/.next/static ./frontends/main/.next/static + +# Copy the public directory (robots.txt, favicon.ico, etc.) +COPY --from=build /app/frontends/main/public ./frontends/main/public EXPOSE 3000 +ENV NODE_ENV=production +ENV TZ=UTC ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -# CMD ["node", "/app/frontends/main/.next/standalone/frontends/main/server.js"] -CMD ["yarn", "start"] +CMD ["node", "frontends/main/server.js"] diff --git a/frontends/main/next.config.js b/frontends/main/next.config.js index 303a8b29f4..584418f332 100644 --- a/frontends/main/next.config.js +++ b/frontends/main/next.config.js @@ -1,7 +1,13 @@ // @ts-check const { validateEnv } = require("./validateEnv") -validateEnv() +// In CI Docker builds (NEXT_BUILD_CI=1), NEXT_PUBLIC_* vars are not available +// at build time — they are injected at runtime via PublicEnvScript. Skip +// build-time validation; validateEnv() runs at server startup instead (see +// src/instrumentation-node.ts). +if (!process.env.NEXT_BUILD_CI) { + validateEnv() +} const NEXT_PUBLIC_OPTIMIZE_IMAGES = Boolean( (process.env.NEXT_PUBLIC_OPTIMIZE_IMAGES ?? "true") === "true", @@ -12,25 +18,16 @@ const NEXT_CACHE_S_MAXAGE_SECONDS = process.env.NEXT_CACHE_S_MAXAGE_SECONDS || "1800" const PAGE_CACHE_CONTROL = `s-maxage=${NEXT_CACHE_S_MAXAGE_SECONDS}, stale-if-error=86400, stale-while-revalidate=86400` -const processFeatureFlags = () => { - const featureFlagPrefix = - process.env.NEXT_PUBLIC_POSTHOG_FEATURE_PREFIX || "FEATURE_" - const bootstrapFeatureFlags = {} - - for (const [key, value] of Object.entries(process.env)) { - if (key.startsWith(`NEXT_PUBLIC_${featureFlagPrefix}`)) { - bootstrapFeatureFlags[ - key.replace(`NEXT_PUBLIC_${featureFlagPrefix}`, "").replaceAll("_", "-") - ] = value === "True" ? true : JSON.stringify(value) - } - } - - return bootstrapFeatureFlags -} - /** @type {import('next').NextConfig} */ const nextConfig = { productionBrowserSourceMaps: true, + /** + * Standalone output emits a minimal self-contained server at + * .next/standalone/ with only the required runtime files. The resulting + * Docker image requires no node_modules and no yarn at startup. + * See: https://nextjs.org/docs/app/getting-started/deploying#docker + */ + output: "standalone", async rewrites() { return [ /* Static assets moved from /static, though image paths are sometimes @@ -81,6 +78,9 @@ const nextConfig = { key: "Cache-Control", value: PAGE_CACHE_CONTROL, }, + // Tag sitemaps with "html-pages" so Fastly can purge them on deploy + // without also purging immutable /_next/static/ chunks. + { key: "Surrogate-Key", value: "html-pages" }, ], }, /* This is intended to target the base HTML responses and streamed RSC @@ -88,15 +88,20 @@ const nextConfig = { * sets no-cache. However we are currently serving public content that is * cacheable. * - * Excludes everything with a file extension so we're matching only on routes. + * Excludes everything with a file extension (so /_next/static/*.js is + * never matched) and also excludes /healthcheck, which returns JSON and + * should not be tagged as an HTML page for Fastly surrogate-key purges. */ { - source: "/((?!.*\\.[a-zA-Z0-9]{2,4}$).*)", + source: "/((?!.*\\.[a-zA-Z0-9]+$)(?!healthcheck$).*)", headers: [ { key: "Cache-Control", value: PAGE_CACHE_CONTROL, }, + // Tag all HTML/page routes so Fastly can purge them on deploy + // without also purging immutable /_next/static/ chunks. + { key: "Surrogate-Key", value: "html-pages" }, ], }, @@ -137,15 +142,51 @@ const nextConfig = { qualities: [25, 50, 75, 100], }, - env: { - FEATURE_FLAGS: JSON.stringify(processFeatureFlags()), - }, - experimental: { // Turbopack filesystem caching is enabled by default in Next.js 16.1+ // Explicitly enable it for clarity (optional - already default) turbopackFileSystemCacheForDev: true, }, + + /** + * Stable, deterministic build ID based on the git SHA / version tag. + * + * Next.js embeds the build ID in manifest filenames (_buildManifest.js, + * _ssgManifest.js) and in page-data paths. Using the same ID across + * rebuilds of the same commit reduces inter-build hash drift. + * + * NEXT_PUBLIC_VERSION is set as a Kubernetes env var (and as a Docker + * build arg in future standalone builds). GIT_REF is the full commit SHA + * passed by Concourse. The 'dev' fallback is for local builds. + */ + generateBuildId: async () => + process.env.NEXT_PUBLIC_VERSION || process.env.GIT_REF || "dev", + + /** + * Replace webpack's [chunkhash] with [contenthash]. + * + * [chunkhash] is influenced by module ordering and internal IDs, so two + * builds of identical code can produce different filenames. [contenthash] + * is derived purely from the file's content, making chunk names stable + * across rebuilds when code is unchanged. + * + * See: https://github.com/vercel/next.js/discussions/65856 + */ + webpack: (config) => { + if (config.output.filename) { + config.output.filename = config.output.filename.replace( + "[chunkhash]", + "[contenthash]", + ) + } + if (config.output.chunkFilename) { + config.output.chunkFilename = config.output.chunkFilename.replace( + "[chunkhash]", + "[contenthash]", + ) + } + return config + }, } const { withSentryConfig } = require("@sentry/nextjs") diff --git a/frontends/main/src/app-pages/AboutPage/AboutPage.tsx b/frontends/main/src/app-pages/AboutPage/AboutPage.tsx index 5fd4196961..0d154dcdc3 100644 --- a/frontends/main/src/app-pages/AboutPage/AboutPage.tsx +++ b/frontends/main/src/app-pages/AboutPage/AboutPage.tsx @@ -1,5 +1,6 @@ "use client" +import { env } from "@/env" import { Breadcrumbs, Container, @@ -15,7 +16,7 @@ import Image from "next/image" const WHAT_IS_MIT_OPEN_FRAGMENT_IDENTIFIER = "what-is-mit-learn" const ACADEMIC_AND_PROFESSIONAL_CONTENT = "kinds-of-content" -const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME +const SITE_NAME = env("NEXT_PUBLIC_SITE_NAME") const PageContainer = styled(Container)({ color: theme.custom.colors.darkGray2, diff --git a/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx b/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx index f93d7b8c0f..24311f47f3 100644 --- a/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx +++ b/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx @@ -1,5 +1,6 @@ "use client" +import { env } from "@/env" import React, { useRef, useEffect, useCallback, useState } from "react" import { notFound } from "next/navigation" import Image from "next/image" @@ -556,7 +557,7 @@ const Certificate = ({ src={ signatory.signature_image.startsWith("http") ? signatory.signature_image - : `${process.env.NEXT_PUBLIC_MITX_ONLINE_BASE_URL}${signatory.signature_image}` + : `${env("NEXT_PUBLIC_MITX_ONLINE_BASE_URL")}${signatory.signature_image}` } alt={signatory.name} crossOrigin="anonymous" diff --git a/frontends/main/src/app-pages/ChannelPage/TopicChannelTemplate.tsx b/frontends/main/src/app-pages/ChannelPage/TopicChannelTemplate.tsx index e30ecd4727..c7bd475c1f 100644 --- a/frontends/main/src/app-pages/ChannelPage/TopicChannelTemplate.tsx +++ b/frontends/main/src/app-pages/ChannelPage/TopicChannelTemplate.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" import React from "react" import { styled, @@ -110,7 +111,7 @@ const TopicChipsInternal: React.FC = (props) => { key={topic.id} href={topic.channel_url ?? ""} onClick={() => { - if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) { posthog.capture(posthogEvent, { topic }) } }} diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/HomeEnrollmentsDisplay.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/HomeEnrollmentsDisplay.tsx index 2dc6755ba1..f1026ffb01 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/HomeEnrollmentsDisplay.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/HomeEnrollmentsDisplay.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" import React from "react" import { Collapse, @@ -89,7 +90,7 @@ const ShowAllContainer = styled.div(({ theme }) => ({ }, })) -const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_MITOL_SUPPORT_EMAIL || "" +const SUPPORT_EMAIL = env("NEXT_PUBLIC_MITOL_SUPPORT_EMAIL") || "" const getResourceKey = (resource: DashboardResource): string => { if (resource.type === DashboardType.ProgramEnrollment) { diff --git a/frontends/main/src/app-pages/DashboardPage/EnrollmentRedirectAlert.tsx b/frontends/main/src/app-pages/DashboardPage/EnrollmentRedirectAlert.tsx index 77f73ac1ff..4a57990a6f 100644 --- a/frontends/main/src/app-pages/DashboardPage/EnrollmentRedirectAlert.tsx +++ b/frontends/main/src/app-pages/DashboardPage/EnrollmentRedirectAlert.tsx @@ -1,5 +1,6 @@ "use client" +import { env } from "@/env" import React from "react" import { useQuery } from "@tanstack/react-query" import { Alert } from "@mitodl/smoot-design" @@ -170,7 +171,7 @@ const parseAlertRequest = ( const EnrollmentRedirectAlert: React.FC = () => { const request = useConsumeSearchParamsOnce(parseAlertRequest) - const supportEmail = process.env.NEXT_PUBLIC_MITOL_SUPPORT_EMAIL || "" + const supportEmail = env("NEXT_PUBLIC_MITOL_SUPPORT_EMAIL") || "" const mitxOnlineUserQuery = useQuery({ ...mitxUserQueries.me(), diff --git a/frontends/main/src/app-pages/HomePage/BrowseTopicsSection.tsx b/frontends/main/src/app-pages/HomePage/BrowseTopicsSection.tsx index 88ef1a5fb5..244af42986 100644 --- a/frontends/main/src/app-pages/HomePage/BrowseTopicsSection.tsx +++ b/frontends/main/src/app-pages/HomePage/BrowseTopicsSection.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" import React from "react" import Link from "next/link" import { @@ -123,7 +124,7 @@ const BrowseTopicsSection: React.FC = () => { key={id} href={channelUrl ? new URL(channelUrl!).pathname : ""} onClick={() => { - if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) { posthog.capture(PostHogEvents.HomeTopicClicked, { topic: name, }) @@ -143,7 +144,7 @@ const BrowseTopicsSection: React.FC = () => { { - if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) { posthog.capture(PostHogEvents.HomeSeeAllTopicsClicked) } }} diff --git a/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx b/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx index 0d8223a9e9..dfb9b0994b 100644 --- a/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx +++ b/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx @@ -1,4 +1,5 @@ "use client" +import { env } from "@/env" import React from "react" import { Typography, styled } from "ol-components" import { ButtonLink } from "@mitodl/smoot-design" @@ -252,7 +253,7 @@ const UAIAnnouncementCard: React.FC = () => { }) const handleCTAClick = () => { - if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) { posthog.capture(PostHogEvents.CallToActionClicked, { label: "Learn about Universal AI", readableId: UAI_PROGRAM_READABLE_ID, diff --git a/frontends/main/src/app-pages/HomePage/VideoShortsModal.tsx b/frontends/main/src/app-pages/HomePage/VideoShortsModal.tsx index f8f3437a51..26ecd21176 100644 --- a/frontends/main/src/app-pages/HomePage/VideoShortsModal.tsx +++ b/frontends/main/src/app-pages/HomePage/VideoShortsModal.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" import React, { useCallback, useEffect, useRef, useState } from "react" import Image from "next/image" import { styled, Typography } from "ol-components" @@ -8,7 +9,7 @@ import { useWindowDimensions } from "ol-utilities" import type { VideoShort } from "api/v0" import MITOpenLearningLogo from "@/public/images/mit-open-learning-logo.svg" -const NEXT_PUBLIC_ORIGIN = process.env.NEXT_PUBLIC_ORIGIN +const NEXT_PUBLIC_ORIGIN = env("NEXT_PUBLIC_ORIGIN") const Overlay = styled.div(({ theme }) => ({ position: "fixed", diff --git a/frontends/main/src/app-pages/HomePage/VideoShortsSection.tsx b/frontends/main/src/app-pages/HomePage/VideoShortsSection.tsx index 0a756689d1..7be83b553d 100644 --- a/frontends/main/src/app-pages/HomePage/VideoShortsSection.tsx +++ b/frontends/main/src/app-pages/HomePage/VideoShortsSection.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" import React, { useState } from "react" import Image from "next/image" import { Container, Typography, Card, styled } from "ol-components" @@ -6,7 +7,7 @@ import { useVideoShortsList } from "api/hooks/videoShorts" import VideoShortsModal from "./VideoShortsModal" import MITOpenLearningLogo from "@/public/images/mit-open-learning-logo.svg" -const NEXT_PUBLIC_ORIGIN = process.env.NEXT_PUBLIC_ORIGIN +const NEXT_PUBLIC_ORIGIN = env("NEXT_PUBLIC_ORIGIN") const Section = styled.section(({ theme }) => ({ padding: "80px 0", diff --git a/frontends/main/src/app-pages/HonorCodePage/HonorCodePage.tsx b/frontends/main/src/app-pages/HonorCodePage/HonorCodePage.tsx index 73d98f13ca..8f8271f54f 100644 --- a/frontends/main/src/app-pages/HonorCodePage/HonorCodePage.tsx +++ b/frontends/main/src/app-pages/HonorCodePage/HonorCodePage.tsx @@ -1,5 +1,6 @@ "use client" +import { env } from "@/env" import { Breadcrumbs, Container, @@ -64,7 +65,7 @@ const UnorderedList = styled.ul(({ theme }) => ({ marginTop: "10px", })) -const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME +const SITE_NAME = env("NEXT_PUBLIC_SITE_NAME") const HonorCodePage: React.FC = () => { return ( diff --git a/frontends/main/src/app-pages/PrivacyPage/PrivacyPage.tsx b/frontends/main/src/app-pages/PrivacyPage/PrivacyPage.tsx index b4df60b38c..5ab03c7388 100644 --- a/frontends/main/src/app-pages/PrivacyPage/PrivacyPage.tsx +++ b/frontends/main/src/app-pages/PrivacyPage/PrivacyPage.tsx @@ -1,5 +1,6 @@ "use client" +import { env } from "@/env" import { Breadcrumbs, Container, @@ -64,8 +65,8 @@ const UnorderedList = styled.ul(({ theme }) => ({ marginTop: "10px", })) -const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME -const MITOL_SUPPORT_EMAIL = process.env.NEXT_PUBLIC_MITOL_SUPPORT_EMAIL +const SITE_NAME = env("NEXT_PUBLIC_SITE_NAME") +const MITOL_SUPPORT_EMAIL = env("NEXT_PUBLIC_MITOL_SUPPORT_EMAIL") const PrivacyPage: React.FC = () => { return ( diff --git a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx index 1973e9c2cd..4c8b04821d 100644 --- a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx +++ b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" import React from "react" import { styled, Stack, LoadingSpinner } from "ol-components" import { useQuery } from "@tanstack/react-query" @@ -94,7 +95,7 @@ const CourseEnrollmentButton: React.FC = ({ if (me.isLoading) { return } - if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) { posthog.capture(PostHogEvents.CallToActionClicked, { readableId: course.readable_id, resourceType: "course", diff --git a/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx b/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx index 2c07c18beb..e93a8f11c5 100644 --- a/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx +++ b/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx @@ -1,5 +1,6 @@ "use client" +import { env } from "@/env" import React from "react" import { usePostHog } from "posthog-js/react" import { BaseLearningResourceCard } from "ol-components" @@ -217,7 +218,7 @@ const MitxOnlineResourceCard: React.FC = ( ariaLabel={`${data.displayType}: ${data.title}`} list={list} onClick={() => { - if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) { posthog.capture(PostHogEvents.CourseCardClicked, { label, resourceId: props.resource?.id, diff --git a/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.tsx b/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.tsx index ad2c32555e..8b8400f512 100644 --- a/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProductPageTemplate.tsx @@ -1,5 +1,6 @@ "use client" +import { env } from "@/env" import React from "react" import { Container, @@ -311,7 +312,7 @@ const ProductPageTemplate: React.FC = ({ const handleStayUpdatedClick = () => { if (!showStayUpdated || !resource) return - if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) { posthog.capture(PostHogEvents.CallToActionClicked, { label: "Stay Updated", readableId: resource.readable_id, diff --git a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx index d3571fe900..8b66fe1adc 100644 --- a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" import React from "react" import { LoadingSpinner, Stack } from "ol-components" import { @@ -84,7 +85,7 @@ const ProgramEnrollmentButton: React.FC = ({ if (enrollments.isLoading || me.isLoading) { return } - if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) { posthog.capture(PostHogEvents.CallToActionClicked, { readableId: program.readable_id, resourceType: "program", diff --git a/frontends/main/src/app-pages/TopicsListingPage/TopicsListingPage.tsx b/frontends/main/src/app-pages/TopicsListingPage/TopicsListingPage.tsx index db6bb0f58b..6540bf8a02 100644 --- a/frontends/main/src/app-pages/TopicsListingPage/TopicsListingPage.tsx +++ b/frontends/main/src/app-pages/TopicsListingPage/TopicsListingPage.tsx @@ -1,5 +1,6 @@ "use client" +import { env } from "@/env" import React, { useMemo } from "react" import { Container, @@ -34,7 +35,7 @@ const captureTopicClicked = ( event: string, topic: string, ) => { - if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) { posthog.capture(event, { topic }) } } diff --git a/frontends/main/src/app-pages/UnitsListingPage/UnitCard.tsx b/frontends/main/src/app-pages/UnitsListingPage/UnitCard.tsx index 6afee621b4..a7ef3c1167 100644 --- a/frontends/main/src/app-pages/UnitsListingPage/UnitCard.tsx +++ b/frontends/main/src/app-pages/UnitsListingPage/UnitCard.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" import React from "react" import type { OfferedByEnum } from "api" import type { UnitChannel } from "api/v0" @@ -98,7 +99,7 @@ const UnitCard: React.FC = (props) => { { - if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) { posthog.capture(PostHogEvents.ProviderLinkClicked, { provider: unit, }) diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx index bd2f3b9084..086850fd6c 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx @@ -1,5 +1,6 @@ "use client" +import { env } from "@/env" import React, { useEffect, useRef, useState } from "react" import Link from "next/link" import Image from "next/image" @@ -19,7 +20,7 @@ import SharePopover from "@/components/SharePopover/SharePopover" import { buildVideoStructuredData } from "./videoStructuredData" import VideoResourcePlayer from "./VideoResourcePlayer" -const NEXT_PUBLIC_ORIGIN = process.env.NEXT_PUBLIC_ORIGIN +const NEXT_PUBLIC_ORIGIN = env("NEXT_PUBLIC_ORIGIN") const PageWrapper = styled.div({ backgroundColor: "#fff", diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/YouTubeIframePlayer.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/YouTubeIframePlayer.tsx index 7913907813..434b028913 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/YouTubeIframePlayer.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/YouTubeIframePlayer.tsx @@ -1,9 +1,10 @@ "use client" +import { env } from "@/env" import React from "react" import invariant from "tiny-invariant" -const NEXT_PUBLIC_ORIGIN = process.env.NEXT_PUBLIC_ORIGIN +const NEXT_PUBLIC_ORIGIN = env("NEXT_PUBLIC_ORIGIN") invariant(NEXT_PUBLIC_ORIGIN, "NEXT_PUBLIC_ORIGIN must be defined") export type YouTubeIframePlayerProps = { diff --git a/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/page.tsx b/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/page.tsx index c0c53d21e5..7c07074586 100644 --- a/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/page.tsx +++ b/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/page.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" import React from "react" import { Metadata } from "next" import CertificatePage from "@/app-pages/CertificatePage/CertificatePage" @@ -9,7 +10,7 @@ import { safeGenerateMetadata, standardizeMetadata } from "@/common/metadata" import { getQueryClient } from "@/app/getQueryClient" import { getCertificateInfo } from "@/common/certificateUtils" -const NEXT_PUBLIC_ORIGIN = process.env.NEXT_PUBLIC_ORIGIN +const NEXT_PUBLIC_ORIGIN = env("NEXT_PUBLIC_ORIGIN") enum CertificateType { Course = "course", diff --git a/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/pdf/route.tsx b/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/pdf/route.tsx index 0dc1b61203..63e991d512 100644 --- a/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/pdf/route.tsx +++ b/frontends/main/src/app/(site)/certificate/[certificateType]/[uuid]/pdf/route.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" /* eslint-disable no-restricted-syntax */ import React from "react" import type { AxiosError } from "axios" @@ -384,7 +385,7 @@ const CertificateDoc = ({ source={ signatory.signature_image.startsWith("http") ? signatory.signature_image - : `${process.env.NEXT_PUBLIC_MITX_ONLINE_BASE_URL}${signatory.signature_image}` + : `${env("NEXT_PUBLIC_MITX_ONLINE_BASE_URL")}${signatory.signature_image}` } style={{ width: "100px", diff --git a/frontends/main/src/app/(site)/layout.tsx b/frontends/main/src/app/(site)/layout.tsx index 982058ea30..32f941e427 100644 --- a/frontends/main/src/app/(site)/layout.tsx +++ b/frontends/main/src/app/(site)/layout.tsx @@ -1,3 +1,4 @@ +import { env } from "@/env" import React from "react" import Script from "next/script" import Header from "@/page-components/Header/Header" @@ -86,13 +87,13 @@ j=d.createElement(s),dl=l!=='dataLayer'?'&l='+l:'';j.async=true;j.src=