Description
The footer in frontend-base lacks a full-replacement seam analogous to what the header already has, which makes whole-footer replacements (for example, swapping in tutor-indigo's IndigoFooter) awkward or impossible without forking.
shell/footer/Footer.tsx composes the footer inline: a <footer> wrapper, a column flex container, four column slots with hardcoded inner layouts (LeftLinks, CenterLinks, etc.), and a <PoweredBy> component all live outside any <Slot>, so no slot operation can replace them. The only full-width slot, desktopTop.v1, hardcodes a RevealLinks layout that wraps children in a Collapsible, so a wholesale-replacement plugin gets hidden behind a "more" toggle. The four column slots are too narrow and each carry their own hardcoded layouts, so they are not "drop your footer here" targets either.
The header already solved this. Its desktop/mobile layouts are registered as plain widgets on dedicated slots in shell/header/app.tsx, and Header.tsx is just a two-<Slot> shell. Replacing the layout widget is a one-op operation. The footer should work the same way.
Proposed direction
Introduce one new top-level slot (e.g. org.openedx.frontend.slot.footer.desktop.v1) and one new layout widget (e.g. org.openedx.frontend.widget.footer.desktopLayout.v1), following the header's convention. The existing JSX from Footer.tsx (the <footer> element, the column composition, <PoweredBy>) moves verbatim into a new DesktopFooterLayout component registered as the default widget for that slot. Footer.tsx then collapses to a single <Slot>, symmetric with Header.tsx.
This is purely additive. Every existing slot id, every default widget id, and the column composition stay verbatim. Plugins targeting the existing column slots keep working unchanged. The DOM tree for default deployments is identical aside from one extra <Slot> rendering pass. The net new public surface is one slot id and one widget id, both following the reverse-DNS-versioned convention from ADR 0009.
After the change, replacing the whole footer becomes a single WidgetOperationTypes.REPLACE (or LayoutOperationTypes.REPLACE) targeting the new id, mirroring the header's desktopLayout.v1 / mobileLayout.v1 pattern. A short ADR documenting the symmetry ("layout widgets host shell composition; top-level slots are layout-replacement seams") and a docs page demonstrating both replacement paths for header and footer side by side would round out the change.
Out of scope
Splitting desktop and mobile footer layouts (the current responsive single-layout design works; defer until a real reason emerges). Wrapping <PoweredBy> in its own slot so operators can remove just that piece without replacing the whole layout (reasonable follow-up, but widens scope and is not blocking). Generic "layout replacement" abstractions beyond what LayoutOperationTypes.REPLACE already provides.
Description
The footer in
frontend-baselacks a full-replacement seam analogous to what the header already has, which makes whole-footer replacements (for example, swapping in tutor-indigo'sIndigoFooter) awkward or impossible without forking.shell/footer/Footer.tsxcomposes the footer inline: a<footer>wrapper, a column flex container, four column slots with hardcoded inner layouts (LeftLinks,CenterLinks, etc.), and a<PoweredBy>component all live outside any<Slot>, so no slot operation can replace them. The only full-width slot,desktopTop.v1, hardcodes aRevealLinkslayout that wraps children in a Collapsible, so a wholesale-replacement plugin gets hidden behind a "more" toggle. The four column slots are too narrow and each carry their own hardcoded layouts, so they are not "drop your footer here" targets either.The header already solved this. Its desktop/mobile layouts are registered as plain widgets on dedicated slots in
shell/header/app.tsx, andHeader.tsxis just a two-<Slot>shell. Replacing the layout widget is a one-op operation. The footer should work the same way.Proposed direction
Introduce one new top-level slot (e.g.
org.openedx.frontend.slot.footer.desktop.v1) and one new layout widget (e.g.org.openedx.frontend.widget.footer.desktopLayout.v1), following the header's convention. The existing JSX fromFooter.tsx(the<footer>element, the column composition,<PoweredBy>) moves verbatim into a newDesktopFooterLayoutcomponent registered as the default widget for that slot.Footer.tsxthen collapses to a single<Slot>, symmetric withHeader.tsx.This is purely additive. Every existing slot id, every default widget id, and the column composition stay verbatim. Plugins targeting the existing column slots keep working unchanged. The DOM tree for default deployments is identical aside from one extra
<Slot>rendering pass. The net new public surface is one slot id and one widget id, both following the reverse-DNS-versioned convention from ADR 0009.After the change, replacing the whole footer becomes a single
WidgetOperationTypes.REPLACE(orLayoutOperationTypes.REPLACE) targeting the new id, mirroring the header'sdesktopLayout.v1/mobileLayout.v1pattern. A short ADR documenting the symmetry ("layout widgets host shell composition; top-level slots are layout-replacement seams") and a docs page demonstrating both replacement paths for header and footer side by side would round out the change.Out of scope
Splitting desktop and mobile footer layouts (the current responsive single-layout design works; defer until a real reason emerges). Wrapping
<PoweredBy>in its own slot so operators can remove just that piece without replacing the whole layout (reasonable follow-up, but widens scope and is not blocking). Generic "layout replacement" abstractions beyond whatLayoutOperationTypes.REPLACEalready provides.