Can't use predefined plugin-slots (sidebar) in frontend-app-learner-dashboard in Production

I’m currently trying to understand how the plugin-slots work. Therefore I followed the 2025-frontend-plugin-workshop (which is btw. a very useful and informative ressource!) and achieved successful the implementation in tutor dev. Now I want to implement the changes in production. Therefore I followed this documentation which explains the procedure how to use a plugin slot. But following it I could not achieve any results.

The problem: The default sidebar was NOT hidden and the new custom sidebar was NOT added. Im sure that Im missing something, but I cant get my head around it…

All detailed steps for easy reproduction of this problem are described at the bottom. Thank you in advance for your support and any helpful answers!

Versions

  • Tutor version: 21.0.4
  • Im currently trying it out on my local machine (MACOS 15.7.3, 24G419) in production like setup (see the production-setup below via tutor local launch) before running it on the real production ubuntu server.

Similiar problems found and what I have tried

  • Example instruction how to hide the default footer and add own custom footer. This did not work for me either. The footer could not be hidden and the new one could not be inserted.

Steps for easy reproduction

These are all steps for reproducing the problem.

  1. Create virtual environment and install tutor.
python3 -m venv .venv
source .venv/bin/activate
pip install "tutor[full]==21.0.4"
  1. launch and configure tutor
tutor local launch 

production platform -> Enter: y
students domain (LMS) -> Enter: local.openedx.io
teachers domain (CMS) -> Enter: studio.local.openedx.io
...
Activate SSL/TLS certs -> Enter: n
  1. Create a user so that the frontend changes can be verified later by logging in to the learner dashboard.
tutor local do createuser --staff --superuser admin admin@admin.com --password admin
  1. This documentation explains how to use a plugin slot. The slot which I wanted to change was the sidebar of the learner-dashboard. the documention for frontend-app-learner-dashboardis documented here and specificly the information of its sidebar is mentioned here. There is the slot ID alias widget_sidebar_slot defined and the service name learner_dashboard which were used during the plugin implemenation below (step 4).
  2. This documentation shows an example codesnippet how a plugin is implemented to hide default content and insert a custom content. Following it and combined with these instructions on how to create plugin the following steps where taken:
mkdir -p "$(tutor plugins printroot)"
"$(tutor plugins printroot)/customize_learner_dashboard_sidebar.py"

and the customize_learner_dashboard_sidebar.py contains the following plugin implementation to hide the default sidebar content and insert the custom content (just a string for testing purposes).

from tutormfe.hooks import PLUGIN_SLOTS

PLUGIN_SLOTS.add_items([
    # Hide the default sidebar
    (
        "learner_dashboard",
        "widget_sidebar_slot",
        """
        {
          op: PLUGIN_OPERATIONS.Hide,
          widgetId: 'widget_sidebar_slot',
        }"""
    ),
    # Insert a custom sidebar
    (
        "learner_dashboard",
        "widget_sidebar_slot",
        """
        {
          op: PLUGIN_OPERATIONS.Insert,
          widget: {
            id: 'custom_sidebar',
            type: DIRECT_PLUGIN,
            RenderWidget: () => (
              <h1>This is the sidebar.</h1>
            ),
          },
        }"""
    )
])

after that I enabled the plugin by runnning tutor plugins enable customize_learner_dashboard_sidebar and verified it with tutor plugins list.

  1. As the doc describes, I rebuild & restarted the mfe image:
tutor images build mfe
tutor local stop mfe && tutor local start -d mfe
  1. Now the custom sidebar should be visible, but after logging in and visiting the learner-dashboard at TODO its still displays the default sidebar.

Thank you in advance for your support and helpful answers!

I see a couple of issues here:

  • The MFE name is learner-dashboard not learner_dashboard so the slots aren’t applying at all.
  • If you want to hide the default contents you need to use the widgetID default_contents

Additionally, while widget_sidebar_slot will work for backwards compatibility, the new slot names are recommendedl: org.openedx.frontend.learner_dashboard.widget_sidebar.v1

If you’re aiming for quick iteration, you should try to clone the MFE locally, mount it and then add the slots via the env.config.jsx file. This will let you edit the plugin and see changes in real time.

Thank you very much for your helpful reply.

I adjusted the plugin based on your suggestions and it looks like this now:

from tutormfe.hooks import PLUGIN_SLOTS

PLUGIN_SLOTS.add_items([
    # Hide the default sidebar
    (
        "learner-dashboard",
        "org.openedx.frontend.learner_dashboard.widget_sidebar.v1",
        """
        {
          op: PLUGIN_OPERATIONS.Hide,
          widgetId: 'default_contents',
        }"""
    ),
    # Insert a custom sidebar
    (
        "learner-dashboard",
        "org.openedx.frontend.learner_dashboard.widget_sidebar.v1",
        """
        {
          op: PLUGIN_OPERATIONS.Insert,
          widget: {
            id: 'custom_sidebar',
            type: DIRECT_PLUGIN,
            RenderWidget: () => (
              <h1>This is the sidebar.</h1>
            ),
          },
        }"""
    )
])

After that I ran

tutor images build mfe
tutor local stop mfe && tutor local start -d mfe

to rebuild & restart the mfe image.
But still it did not hide/insert the default/new content in the sidebar slot.

Do I need to do something additional, or is the plugin implementation still incorrect ?

edit:
At the end of your post you mentioned the quick local mount approach. I did it and it worked for with this env.config.jsx file:

import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';

const config = {
  pluginSlots: {
     'org.openedx.frontend.learner_dashboard.widget_sidebar.v1': {
      keepDefault: true,
      plugins: [
        {
          op: PLUGIN_OPERATIONS.Hide,
          widgetId: 'default_contents',
        },
        {
          op: PLUGIN_OPERATIONS.Insert,
          widget: {
            id: 'custom_sidebar',
            type: DIRECT_PLUGIN,
            RenderWidget: () => (
              <h1>This is the sidebar.</h1>
            ),
          },
        },
      ],
    },
  },
}

export default config;

As mentioned in my first post, it did work for me in the dev environment (tutor dev with local mount …), but it’s not applicable in prod (tutor local with plugin).
As you can see, its almost the same as the above implemented plugin (with syntactical changes).

Did you run tutor config save after editing the plugin? If not the changes won’t be picked up during the image rebuild. Running that regenerates the build files needed to inject the plugin.