Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 1 addition & 40 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
155 changes: 48 additions & 107 deletions frontends/main/Dockerfile.web
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -83,110 +70,64 @@ 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"
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

Comment thread
blarghmatey marked this conversation as resolved.
# 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 ./

Comment thread
blarghmatey marked this conversation as resolved.
# 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"
Comment thread
blarghmatey marked this conversation as resolved.
Comment thread
blarghmatey marked this conversation as resolved.

# CMD ["node", "/app/frontends/main/.next/standalone/frontends/main/server.js"]
CMD ["yarn", "start"]
CMD ["node", "frontends/main/server.js"]
87 changes: 64 additions & 23 deletions frontends/main/next.config.js
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -81,22 +78,30 @@ 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
* content. Some routes are dynamically rendered, so NextJS by default
* 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" },
],
},

Expand Down Expand Up @@ -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")
Expand Down
3 changes: 2 additions & 1 deletion frontends/main/src/app-pages/AboutPage/AboutPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client"

import { env } from "@/env"
import {
Breadcrumbs,
Container,
Expand All @@ -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,
Expand Down
Loading
Loading