Help wiring complex slots (shared components) into Tutor MFE with plugin slots

Hi all,

I’m working on a branded deployment on Tutor 20.0.2 (Teak) and I’m trying to find the proper way to deploy a set of custom React components via the Frontend Plugin Framework / slots, using Tutor’s PLUGIN_SLOTS in a tutor local start -d setup.

I’d love feedback on whether my approach makes sense, and what the recommended pattern is for “complex” slot replacements (components that themselves import a bunch of stuff) shared across multiple MFEs.


What I have so far

1. Forked MFEs

I’ve created forks of several MFEs, all based on the tag that matches my Tutor version:

  • authn (for this one I’m not using SLOTS as they are not available)

  • learning (touching header and footer)

  • learner-dashboard (header, footer and widget)

Each fork has a custom branch. In the forks, I’ve added a folder under src/ with my own React components (for branding, header/footer, etc.), and they are exported normally from those modules.

Tutor plugin for overriding MFE repos:

from tutormfe.hooks import MFE_APPS

@MFE_APPS.add()
def _override_default_mfes(mfes):
    mfes["authn"]["repository"] = "https://github.com/institutohumai/mfe-frontend-app-authn-fork.git"
    mfes["authn"]["version"] = "newfront-lauti-26112025"

    mfes["learning"]["repository"] = "https://github.com/institutohumai/mfe-frontend-app-learning-fork.git"
    mfes["learning"]["version"] = "NewCampus-LH-0212"

    mfes["learner-dashboard"]["repository"] = "https://github.com/institutohumai/mfe-frontend-app-learner-dashboard-fork.git"
    mfes["learner-dashboard"]["version"] = "NewCampus-LH-0212"

    return mfes

2. Custom components inside the MFE repo

In the MFE forks I have components like:

  • src/humai/CustomHeader.jsx
  • src/humai/CustomFooter.jsx

Each of these is a “complex” component: it imports Paragon, FontAwesome, router hooks, getAuthenticatedUser, etc. I want to use them as slot replacements for:

  • Header slots in MFEs that use frontend-component-header
  • Footer slots in MFEs that use frontend-component-footer

Ideally in a way that I can target several MFEs at once (using app=“all” in PLUGIN_SLOTS).

3. Tutor plugins

I currently have these Tutor plugins:

from tutormfe.hooks import PLUGIN_SLOTS
from tutor import hooks

# Try to import React components at build time into env.config.jsx
hooks.Filters.ENV_PATCHES.add_item(
    ("mfe-env-config-buildtime-imports", """
import CustomHeader from '@src/humai/CustomHeader';
import CustomFooter from '@src/humai/CustomFooter';
    """)
)

PLUGIN_SLOTS.add_items([
    (
        "all",
        "org.openedx.frontend.layout.header_desktop.v1",
        """
        {
          keepDefault: false,
          plugins: [
            {
              op: PLUGIN_OPERATIONS.Insert,
              widget: {
                id: 'humai_custom_header',
                type: DIRECT_PLUGIN,
                RenderWidget: CustomHeader,
              },
            },
          ],
        }
        """,
    ),
    (
        "all",
        "org.openedx.frontend.layout.footer.v1",
        """
        {
          keepDefault: false,
          plugins: [
            {
              op: PLUGIN_OPERATIONS.Insert,
              widget: {
                id: 'humai_custom_footer',
                type: DIRECT_PLUGIN,
                RenderWidget: CustomFooter,
              },
            },
          ],
        }
        """,
    ),
    (
        "learner-dashboard",
        "org.openedx.frontend.learner_dashboard.widget_sidebar.v1",
        """
        {
          keepDefault: false,
          plugins: [
            {
              op: PLUGIN_OPERATIONS.Hide,
              widgetId: 'default_contents',
            },
          ],
        }
        """,
    ),
])

4. What works in local MFE dev mode

If I run the MFE locally (outside Tutor) with an env.config.jsx like this, it works as expected:

import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
import CustomHeader from './src/humai/CustomHeader';
import CustomFooter from './src/humai/CustomFooter';

const config = {
  pluginSlots: {
    'org.openedx.frontend.learner_dashboard.widget_sidebar.v1': {
      plugins: [
        {
          op: PLUGIN_OPERATIONS.Hide,
          widgetId: 'default_contents',
        },
      ],
    },
    'org.openedx.frontend.layout.header_desktop.v1': {
      keepDefault: false,
      plugins: [
        {
          op: PLUGIN_OPERATIONS.Insert,
          widget: {
            id: 'custom_desktop_header_component',
            type: DIRECT_PLUGIN,
            RenderWidget: () => <CustomHeader />,
          },
        },
      ],
    },
    'org.openedx.frontend.layout.footer.v1': {
      plugins: [
        {
          op: PLUGIN_OPERATIONS.Hide,
          widgetId: 'default_contents',
        },
        {
          op: PLUGIN_OPERATIONS.Insert,
          widget: {
            id: 'custom_footer',
            type: DIRECT_PLUGIN,
            RenderWidget: () => <CustomFooter />,
          },
        },
      ],
    },
  },
};

export default config;

So in webpack dev mode, with a “normal” env.config.jsx living inside the repo, everything is fine. The imports work, the slots are applied, and my custom header/footer render.


The problem / confusion

In the Tutor build (tutor images build mfe + tutor local start -d), there are a few differences:

  • Tutor generates its own env.config.jsx at build time, based on PLUGIN_SLOTS and ENV_PATCHES.
  • That file lives outside src/ in the build context (/openedx/app/env.config.jsx), so things like import CustomHeader from ‘@src/humai/CustomHeader’ need to be wired correctly.
  • The build has complained with errors like “Cannot resolve @src/humai/CustomHeader” and, depending on what I try, failure to resolve @openedx/frontend-plugin-framework inside that injected env.config.jsx.

Given that:

  1. I want to keep my “complex” components in the MFE repo (src/humai/*.jsx), with their own imports, not inline huge JSX blobs in slot definitions; and
  2. I would like to use app=“all” in PLUGIN_SLOTS.add_items to target header/footer slots across multiple MFEs that use frontend-component-header / frontend-component-footer,

…I’m not sure what the canonical / recommended pattern is in Tutor 20 / Teak.


Docs I’ve followed

I’ve read and tried to follow:

These helped, but they mostly show inline RenderWidget examples (simple components in the slot config), whereas I’m trying to reuse more complex components declared in the MFE repo.


Questions

  1. What is the best practice to reference “real” React components from the MFE repo (e.g. src/humai/CustomHeader.jsx ) inside Tutor’s generated env.config.jsx ?
  • Is mfe-env-config-buildtime-imports the right place to do something like:
    import CustomHeader from '@src/humai/CustomHeader';
    
    or is there a better pattern?
  • Is it more recommended to use mfe-env-config-runtime-definitions- with dynamic import(‘@src/…’) and stash those on window?
  1. Can I safely use (“all”, “org.openedx.frontend.layout.header_desktop.v1”, …) to apply a header replacement across multiple MFEs, when the components themselves live only in some forks?
  • For example, is it OK that my components live only in the authn / learning / learner-dashboard forks, but not in every MFE?
  1. Is there a reference example somewhere of:
  • Forked MFE repo with custom components in src/…,
  • A Tutor plugin that uses PLUGIN_SLOTS to replace header/footer slots in multiple MFEs,
  • And imports those components without inlining them inside the slot config?

Any pointers, patterns, or working snippets from people who have successfully done “complex” header/footer overrides with forked MFEs + Tutor would be super appreciated.

Thanks in advance!

@arbrandes your help will be much appreciated!

cc @brian.smith

Is there a specific reason you want to keep the components in the MFE repos? That seems to be where you’re going “off the beaten path.”

I understand the desire to not have

but for something like a custom header following the pattern from the “Using Frontend Plugin Slots” section of the tutor-mfe README

from tutormfe.hooks import PLUGIN_SLOTS
from tutor import hooks

hooks.Filters.ENV_PATCHES.add_item(
    (
        "mfe-dockerfile-post-npm-install",
        """
# npm package
RUN npm install react-loader-spinner
""",
    )
)

hooks.Filters.ENV_PATCHES.add_item(
    (
        "mfe-env-config-buildtime-imports",
        """
import { FidgetSpinner } from 'react-loader-spinner';
""",
    )
)

PLUGIN_SLOTS.add_items(
    [
        (
            "learner-dashboard",
            "org.openedx.frontend.learner_dashboard.no_courses_view.v1",
            """
            {
              op: PLUGIN_OPERATIONS.Hide,
              widgetId: 'default_contents',
            }"""
        ),
        (
            "learner-dashboard",
            "org.openedx.frontend.learner_dashboard.no_courses_view.v1",
            """
            {
              op: PLUGIN_OPERATIONS.Insert,
              widget: {
                id: 'no_courses_fidget_spinner',
                type: DIRECT_PLUGIN,
                RenderWidget: FidgetSpinner,
              },
            }""",
        ),
    ]
)

might be a smoother experience.

You could, instead of having your CustomHeader component live in forks of MFEs, create a new repo specifically for your custom header.

Then you could npm install it from there even without publishing to npm (npm install docs).

Following the

npm install github:mygithubuser/myproject

example you could have something like:

hooks.Filters.ENV_PATCHES.add_item(
    (
        "mfe-dockerfile-post-npm-install",
        """
# npm package
RUN npm install github:institutohumai/my-custom-header-repo
""",
    )
)

Hopefully this helps!

If you still are encountering issues sharing more details about the

might help narrow down the problem a bit more.

1 Like