diff --git a/deployments/mit-ol/mfe_slot_config/frontend/mitx/public/index.html b/deployments/mit-ol/mfe_slot_config/frontend/mitx/public/index.html
index 870979a8..12903b85 100644
--- a/deployments/mit-ol/mfe_slot_config/frontend/mitx/public/index.html
+++ b/deployments/mit-ol/mfe_slot_config/frontend/mitx/public/index.html
@@ -1,6 +1,8 @@
+
+
MITx
diff --git a/deployments/mit-ol/mfe_slot_config/frontend/mitxonline/public/index.html b/deployments/mit-ol/mfe_slot_config/frontend/mitxonline/public/index.html
index 99a98add..cb104b2e 100644
--- a/deployments/mit-ol/mfe_slot_config/frontend/mitxonline/public/index.html
+++ b/deployments/mit-ol/mfe_slot_config/frontend/mitxonline/public/index.html
@@ -1,6 +1,8 @@
+
+
MIT OpenLearning
diff --git a/deployments/mit-ol/mfe_slot_config/frontend/shared/src/footer/index.tsx b/deployments/mit-ol/mfe_slot_config/frontend/shared/src/footer/index.tsx
index 03a4a0c5..6686530d 100644
--- a/deployments/mit-ol/mfe_slot_config/frontend/shared/src/footer/index.tsx
+++ b/deployments/mit-ol/mfe_slot_config/frontend/shared/src/footer/index.tsx
@@ -1,10 +1,6 @@
-import {
- useSiteConfig,
- WidgetOperationTypes,
- LayoutOperationTypes,
-} from "@openedx/frontend-base";
+import { Slot, useSiteConfig, WidgetOperationTypes } from "@openedx/frontend-base";
import type { App, SlotOperation } from "@openedx/frontend-base";
-import { Hyperlink } from "@openedx/paragon";
+import { Hyperlink, Image } from "@openedx/paragon";
/**
* Shape of the MIT OL footer config expected in
@@ -19,6 +15,8 @@ export interface MITOLFooterConfig {
supportUrl?: string;
accessibilityUrl?: string;
copyrightText?: string;
+ footerLogoUrl?: string;
+ footerLogoDestination?: string;
}
function useMITOLFooterConfig(): MITOLFooterConfig {
@@ -29,109 +27,256 @@ function useMITOLFooterConfig(): MITOLFooterConfig {
function CopyrightNotice() {
const { copyrightText } = useMITOLFooterConfig();
- if (!copyrightText) return null;
- return {copyrightText}
;
+ // Support a `{year}` placeholder in the configured copyright text so the year
+ // stays current without a code change (e.g. "© {year} MIT xPRO. All rights
+ // reserved."). Text without the placeholder is rendered verbatim.
+ const text = copyrightText?.replace(
+ "{year}",
+ String(new Date().getFullYear()),
+ );
+ return (
+
+ {text &&
{text}
}
+
+ edX and Open edX are registered trademarks of edX LLC.
+
+
+ );
}
-function AboutLink() {
- const { aboutUrl } = useMITOLFooterConfig();
- if (!aboutUrl) return null;
- return About Us;
+/**
+ * Footer logo (left side). Reads footerLogoUrl from the runtime config;
+ * falls back to the shell's default (headerLogoImageUrl) if not set.
+ */
+function FooterLogo() {
+ const { footerLogoUrl, footerLogoDestination } = useMITOLFooterConfig();
+ const { headerLogoImageUrl, siteName } = useSiteConfig();
+ const src = footerLogoUrl || headerLogoImageUrl;
+ if (!src) return null;
+ const img = (
+
+ );
+ if (footerLogoDestination) {
+ return {img};
+ }
+ return img;
}
-function SupportLink() {
- const { supportUrl } = useMITOLFooterConfig();
- if (!supportUrl) return null;
- return Contact;
+/** Recreation of the shell's PoweredBy widget (it is not exported from the root). */
+function PoweredBy() {
+ return (
+
+
+
+ );
}
-function AccessibilityLink() {
- const { accessibilityUrl } = useMITOLFooterConfig();
- if (!accessibilityUrl) return null;
- return Accessibility;
-}
+/**
+ * Custom desktop footer layout replacing the shell's DesktopFooterLayout so we
+ * match the legacy learning MFE at every width: a 3-part row of logo (left),
+ * centered links, and the "Powered by Open edX" logo (right), with the
+ * copyright/trademark notice on a full-width centered row below. The row is
+ * intentionally NOT collapsed to a vertical stack on mobile so the placement
+ * mirrors legacy (the links themselves still stack — see MITOLFooterLinks).
+ * The shell layout used `justify-content-between` (a paragon-layer `!important`
+ * utility we cannot override from our site layer), which forced a large gap
+ * between the links and copyright and pushed the "Powered by" logo to the bottom.
+ */
+function MITOLDesktopFooterLayout() {
+ return (
+
+ );
}
-function TermsOfServiceLink() {
- const { termsOfServiceUrl } = useMITOLFooterConfig();
- if (!termsOfServiceUrl) return null;
+/**
+ * Footer link types the MIT OL footer can render, each mapped to its display
+ * label and the runtime-config field holding its URL.
+ */
+export type FooterLinkKey =
+ | "about"
+ | "privacy"
+ | "honor"
+ | "tos"
+ | "accessibility"
+ | "help";
+
+const FOOTER_LINK_DEFS: Record<
+ FooterLinkKey,
+ { label: string; field: keyof MITOLFooterConfig }
+> = {
+ about: { label: "About Us", field: "aboutUrl" },
+ privacy: { label: "Privacy Policy", field: "privacyPolicyUrl" },
+ honor: { label: "Honor Code", field: "honorCodeUrl" },
+ tos: { label: "Terms of Service", field: "termsOfServiceUrl" },
+ accessibility: { label: "Accessibility", field: "accessibilityUrl" },
+ help: { label: "Help", field: "supportUrl" },
+};
+
+/**
+ * Default footer link set/order — the legacy learning MFE footer used by mitx
+ * and mitxonline (About Us · Terms of Service · Accessibility · Help).
+ * Deployments needing a different set pass `linkOrder` to createMITOLFooterApp
+ * (e.g. xPRO adds Privacy Policy + Honor Code).
+ */
+const DEFAULT_FOOTER_LINK_ORDER: FooterLinkKey[] = [
+ "about",
+ "tos",
+ "accessibility",
+ "help",
+];
+
+/**
+ * Single centered horizontal row of footer links (no column labels), rendered
+ * in the given order. Each link is omitted if its URL is missing from the
+ * runtime config.
+ */
+function MITOLFooterLinks({ order }: { order: FooterLinkKey[] }) {
+ const config = useMITOLFooterConfig();
+ const links = order
+ .map((key) => ({
+ url: config[FOOTER_LINK_DEFS[key].field],
+ label: FOOTER_LINK_DEFS[key].label,
+ }))
+ .filter((link): link is { url: string; label: string } =>
+ Boolean(link.url),
+ );
+ if (links.length === 0) return null;
return (
- Terms of Service
+
+ {links.map((link) => (
+ -
+ {link.label}
+
+ ))}
+
);
}
-function HonorCodeLink() {
- const { honorCodeUrl } = useMITOLFooterConfig();
- if (!honorCodeUrl) return null;
- return Honor Code;
-}
-
/**
* Returns an App that injects MIT OL footer content into footerApp's slots.
* All link URLs are read at render time from SiteConfig.commonAppConfig.mitolFooter,
* populated via FRONTEND_SITE_CONFIG in the LMS Django settings. A widget renders
* nothing if its URL is absent from the runtime config.
*/
-export function createMITOLFooterApp(): App {
+export function createMITOLFooterApp(options?: {
+ /** Ordered footer link keys to render. Defaults to the mitx/mitxonline set. */
+ linkOrder?: FooterLinkKey[];
+}): App {
+ const linkOrder = options?.linkOrder ?? DEFAULT_FOOTER_LINK_ORDER;
+ // Close over the resolved order so the slot widget (which the shell renders
+ // without props) shows this deployment's link set.
+ const FooterLinks = () => ;
const slots: SlotOperation[] = [
+ // Replace the shell's desktop footer layout with our own so we control the
+ // link/copyright spacing and place "Powered by Open edX" at the top right.
{
- slotId: "org.openedx.frontend.slot.footer.desktopLegalNotices.v1",
- id: "mitol.footer.copyright",
- op: WidgetOperationTypes.APPEND,
- component: CopyrightNotice,
+ slotId: "org.openedx.frontend.slot.footer.desktop.v1",
+ relatedId: "org.openedx.frontend.widget.footer.desktopLayout.v1",
+ id: "mitol.footer.desktopLayout",
+ op: WidgetOperationTypes.REPLACE,
+ component: MITOLDesktopFooterLayout,
},
- // Column 1: Resources
+ // Remove the shell's default copyright line (e.g. "© 2026 MIT Learn (dev).")
+ // so only the MIT OL copyright + trademark notice remain, matching the
+ // legacy learning MFE footer.
{
- slotId: "org.openedx.frontend.slot.footer.desktopCenterLink1.v1",
- op: LayoutOperationTypes.OPTIONS,
- options: { label: "Resources" },
+ slotId: "org.openedx.frontend.slot.footer.desktopLegalNotices.v1",
+ relatedId: "org.openedx.frontend.widget.footer.desktopCopyrightNotice.v1",
+ op: WidgetOperationTypes.REMOVE,
},
{
- slotId: "org.openedx.frontend.slot.footer.desktopCenterLink1.v1",
- id: "mitol.footer.col1.about",
+ slotId: "org.openedx.frontend.slot.footer.desktopLegalNotices.v1",
+ id: "mitol.footer.copyright",
op: WidgetOperationTypes.APPEND,
- component: AboutLink,
+ component: CopyrightNotice,
},
+ // Replace the shell's default logo (which uses headerLogoImageUrl) with
+ // our FooterLogo component that reads footerLogoUrl from the config.
{
- slotId: "org.openedx.frontend.slot.footer.desktopCenterLink1.v1",
- id: "mitol.footer.col1.contact",
- op: WidgetOperationTypes.APPEND,
- component: SupportLink,
+ slotId: "org.openedx.frontend.slot.footer.desktopLeftLinks.v1",
+ relatedId: "org.openedx.frontend.widget.footer.desktopLeftLinksLogo.v1",
+ id: "mitol.footer.logo",
+ op: WidgetOperationTypes.REPLACE,
+ component: FooterLogo,
},
+ // Single centered horizontal row of links (no column labels) matching
+ // the legacy learning MFE footer. Append directly to the center-links
+ // container (CenterLinks layout) instead of a desktopCenterLinkN.v1 slot
+ // so the links are NOT wrapped in frontend-base's LabeledLinkColumn
+ // (which forces `small` font + a flex column). Remove the 4 default
+ // column slots so only our row renders.
{
- slotId: "org.openedx.frontend.slot.footer.desktopCenterLink1.v1",
- id: "mitol.footer.col1.accessibility",
- op: WidgetOperationTypes.APPEND,
- component: AccessibilityLink,
+ slotId: "org.openedx.frontend.slot.footer.desktopCenterLinks.v1",
+ relatedId: "org.openedx.frontend.widget.footer.desktopCenterLink1.v1",
+ op: WidgetOperationTypes.REMOVE,
},
- // Column 2: Policies
{
- slotId: "org.openedx.frontend.slot.footer.desktopCenterLink2.v1",
- op: LayoutOperationTypes.OPTIONS,
- options: { label: "Policies" },
+ slotId: "org.openedx.frontend.slot.footer.desktopCenterLinks.v1",
+ relatedId: "org.openedx.frontend.widget.footer.desktopCenterLink2.v1",
+ op: WidgetOperationTypes.REMOVE,
},
{
- slotId: "org.openedx.frontend.slot.footer.desktopCenterLink2.v1",
- id: "mitol.footer.col2.privacy",
- op: WidgetOperationTypes.APPEND,
- component: PrivacyPolicyLink,
+ slotId: "org.openedx.frontend.slot.footer.desktopCenterLinks.v1",
+ relatedId: "org.openedx.frontend.widget.footer.desktopCenterLink3.v1",
+ op: WidgetOperationTypes.REMOVE,
},
{
- slotId: "org.openedx.frontend.slot.footer.desktopCenterLink2.v1",
- id: "mitol.footer.col2.tos",
- op: WidgetOperationTypes.APPEND,
- component: TermsOfServiceLink,
+ slotId: "org.openedx.frontend.slot.footer.desktopCenterLinks.v1",
+ relatedId: "org.openedx.frontend.widget.footer.desktopCenterLink4.v1",
+ op: WidgetOperationTypes.REMOVE,
},
{
- slotId: "org.openedx.frontend.slot.footer.desktopCenterLink2.v1",
- id: "mitol.footer.col2.honor-code",
+ slotId: "org.openedx.frontend.slot.footer.desktopCenterLinks.v1",
+ id: "mitol.footer.links",
op: WidgetOperationTypes.APPEND,
- component: HonorCodeLink,
+ component: FooterLinks,
},
];
diff --git a/deployments/mit-ol/mfe_slot_config/frontend/shared/src/header/index.tsx b/deployments/mit-ol/mfe_slot_config/frontend/shared/src/header/index.tsx
index 2c014438..36eba5ad 100644
--- a/deployments/mit-ol/mfe_slot_config/frontend/shared/src/header/index.tsx
+++ b/deployments/mit-ol/mfe_slot_config/frontend/shared/src/header/index.tsx
@@ -3,6 +3,7 @@ import {
useSiteConfig,
useAuthenticatedUser,
WidgetOperationTypes,
+ Slot,
} from "@openedx/frontend-base";
import type { App, SlotOperation } from "@openedx/frontend-base";
import { Dropdown, Hyperlink, Image } from "@openedx/paragon";
@@ -13,18 +14,28 @@ import { isLearnCourse, isMITxOnlineCourse } from "../utils/courseContext";
// ---------------------------------------------------------------------------
const SLOT = {
+ desktop: "org.openedx.frontend.slot.header.desktop.v1",
+ mobile: "org.openedx.frontend.slot.header.mobile.v1",
desktopLeft: "org.openedx.frontend.slot.header.desktopLeft.v1",
desktopRight: "org.openedx.frontend.slot.header.desktopRight.v1",
mobileCenter: "org.openedx.frontend.slot.header.mobileCenter.v1",
+ mobileRight: "org.openedx.frontend.slot.header.mobileRight.v1",
secondaryLinks: "org.openedx.frontend.slot.header.secondaryLinks.v1",
authenticatedMenu: "org.openedx.frontend.slot.header.authenticatedMenu.v1",
} as const;
const WIDGET = {
+ desktopLayout: "org.openedx.frontend.widget.header.desktopLayout.v1",
+ mobileLayout: "org.openedx.frontend.widget.header.mobileLayout.v1",
desktopLogo: "org.openedx.frontend.widget.header.desktopLogo.v1",
mobileLogo: "org.openedx.frontend.widget.header.mobileLogo.v1",
desktopPrimaryLinks:
"org.openedx.frontend.widget.header.desktopPrimaryLinks.v1",
+ desktopAuthenticatedMenu:
+ "org.openedx.frontend.widget.header.desktopAuthenticatedMenu.v1",
+ mobileAuthenticatedMenu:
+ "org.openedx.frontend.widget.header.mobileAuthenticatedMenu.v1",
+ help: "org.openedx.frontend.widget.header.help.v1",
menuProfile:
"org.openedx.frontend.widget.header.desktopAuthenticatedMenuProfile.v1",
menuAccount:
@@ -57,11 +68,18 @@ function useMITOLHeaderConfig(): MITOLHeaderConfig {
const UserMenuToggle: FC = () => {
const authenticatedUser = useAuthenticatedUser();
if (!authenticatedUser) return null;
+ const displayName = authenticatedUser.name || authenticatedUser.username;
return (
(not a div) so the toggle is focusable and
+ // keyboard-activatable. aria-label gives it an accessible name in the
+ // icon-only state (the username is hidden below 992px — see the
+ // mitxonline.scss media query).
+ as="button"
+ type="button"
+ aria-label={displayName}
id="user-nav-dropdown-custom"
- className="d-flex align-items-center gap-2 cursor-pointer"
+ className="d-flex align-items-center gap-2 cursor-pointer bg-transparent"
>
{/* Person icon */}
-
- {authenticatedUser.name || authenticatedUser.username}
-
+ {displayName}