Skip to content

feat: swizzable MarkdownActionsDropdown component#10

Open
guicara wants to merge 1 commit into
FlyNumber:mainfrom
guicara:feature/swizzlable-markdown-actions-dropdown
Open

feat: swizzable MarkdownActionsDropdown component#10
guicara wants to merge 1 commit into
FlyNumber:mainfrom
guicara:feature/swizzlable-markdown-actions-dropdown

Conversation

@guicara
Copy link
Copy Markdown

@guicara guicara commented May 11, 2026

Hi @FlyNumber,

Thank you for this great plugin!
Please find this PR to improve it by adding more customization options.

Summary

Refactor the plugin so consumers can swizzle MarkdownActionsDropdown directly, instead of having to eject Root, and make the swizzled component self-contained so it builds correctly inside consumer Docusaurus sites.

This improves the plugin's customization story while preserving the existing runtime behavior.

In my specific use case, I’m working with a Design System that already provides a dropdown component. Relying solely on CSS to customize it was not sufficient, so I swizzled the component and replaced the original JSX with the one from our Design System. I also added i18n support to handle multiple languages.


Problem

The plugin currently exposes theme/Root.js as the effective theme entry point and injects the dropdown from there. In practice, this creates two problems:

  1. The wrong swizzle target is exposed

    • Users who want to customize the dropdown UI are pushed toward:
      npm run swizzle docusaurus-markdown-source-plugin Root -- --eject
    • But Root is not the real customization surface.
    • What users actually want to customize is the dropdown itself:
      • button label
      • icons
      • menu items
      • analytics hooks
      • copy/open behavior
      • custom conditional rendering
  2. The first refactor still had a portability issue

    • After moving the dropdown into theme/MarkdownActionsDropdown, the component still imported:
      import { getMarkdownUrl } from '../../lib/markdown-path';
    • That works inside the plugin repo, but breaks after swizzling because Docusaurus copies the file into the consumer site:
      src/theme/MarkdownActionsDropdown/index.js
      
    • At that point, ../../lib/markdown-path no longer exists, so the consumer build fails.

Goals

  • Make MarkdownActionsDropdown the correct, first-class swizzle target
  • Preserve the existing DOM injection behavior handled by Root
  • Keep the runtime behavior unchanged for existing users
  • Ensure the swizzled component works when copied into a consumer site's src/theme/
  • Avoid introducing unnecessary dependencies or large architectural changes

What changed

1. Exposed MarkdownActionsDropdown as a theme component

Added a new canonical theme component:

theme/MarkdownActionsDropdown/index.js

This makes the dropdown discoverable by Docusaurus' theme/swizzle system.

image

2. Updated Root to use @theme/MarkdownActionsDropdown

Changed theme/Root.js so it renders the dropdown through the theme alias:

import MarkdownActionsDropdown from '@theme/MarkdownActionsDropdown';

instead of importing it from a plugin-local path.

This is the key change that allows consumer overrides to work automatically.


3. Kept backward compatibility for the old component path

The old file:

components/MarkdownActionsDropdown/index.js

was preserved as a compatibility re-export to the new theme component.

That avoids breaking any internal or consumer code that may have been importing from the old location.


4. Made the swizzled dropdown self-contained

The swizzlable component no longer imports plugin-local utility code like:

../../lib/markdown-path

Instead, the tiny getMarkdownUrl() helper is defined locally inside theme/MarkdownActionsDropdown/index.js.

This ensures that when Docusaurus ejects the component into a consumer project, the copied file still builds correctly on its own.


5. Updated documentation

Updated README.md to:

  • document MarkdownActionsDropdown as the supported swizzle target
  • replace the old Root swizzle guidance
  • explain the intended customization workflow more clearly

Example new workflow:

npm run swizzle docusaurus-markdown-source-plugin MarkdownActionsDropdown -- --eject

Why this approach

Why keep Root at all?

Root still serves an important purpose:

  • it handles runtime page-level behavior
  • it detects eligible docs pages
  • it injects the dropdown into the article header
  • it manages the existing lifecycle/injection logic

That behavior is internal plugin infrastructure, not the ideal consumer customization surface.

So the design keeps:

  • Root for injection/orchestration
  • MarkdownActionsDropdown for user customization

This separation is a better fit for how Docusaurus theme APIs are intended to be used.


Why use @theme/MarkdownActionsDropdown?

Because @theme/... is the extension point Docusaurus resolves through its theme override system.

Using @theme/MarkdownActionsDropdown means:

  • the plugin provides a default implementation
  • consumers can swizzle/eject it
  • their local override is picked up automatically
  • no special plugin configuration is needed

Why inline the helper instead of importing from lib/?

Swizzled theme components are physically copied into the consumer site.

That means relative imports to plugin internals are fragile unless those internals are also exposed as stable public modules. In this case, getMarkdownUrl() is tiny and stable, so inlining it is the simplest and most reliable solution.

Benefits:

  • zero extra public API surface
  • no consumer-side module resolution issues
  • no extra packaging complexity
  • no change in behavior

Trade-off:

  • a tiny bit of duplicated logic

This trade-off is worth it here because portability of the swizzled file is more important than centralizing a 3-line helper.


User-facing result

After this change, consumers can now do:

npm run swizzle docusaurus-markdown-source-plugin MarkdownActionsDropdown -- --eject

and Docusaurus will generate:

src/theme/MarkdownActionsDropdown/index.js

They can then customize the dropdown directly without needing to eject Root.

Also, the swizzled file now builds correctly because it no longer depends on plugin-internal relative imports.


Backward compatibility

This change is intended to be backward compatible:

  • existing plugin users still get the same dropdown behavior
  • Root continues to manage injection as before
  • the old component path remains as a compatibility re-export
  • no config changes are required for existing consumers

The only behavior change is improved customization support.


Testing / Validation

Plugin test suite

Ran:

npm test

Result:

  • 36/36 tests passing

Package validation

Ran:

npm pack --dry-run

Confirmed the package includes:

  • theme/Root.js
  • theme/MarkdownActionsDropdown/index.js

Swizzle portability validation

Verified the root cause of the consumer error:

  • a swizzled component cannot rely on plugin-local relative imports

Adjusted the implementation so the generated consumer file is self-contained.


Example consumer workflow after this PR

  1. Install/update the plugin

  2. Swizzle the dropdown:

    npm run swizzle docusaurus-markdown-source-plugin MarkdownActionsDropdown -- --eject
  3. Edit:

    src/theme/MarkdownActionsDropdown/index.js
    
  4. Customize labels, icons, menu items, analytics, or behavior as needed


To sum up / Changelog summary

Changed

  • Exposed MarkdownActionsDropdown as a first-class Docusaurus theme component.
  • Updated Root to render @theme/MarkdownActionsDropdown so consumer overrides are picked up automatically.
  • Updated documentation to present MarkdownActionsDropdown as the supported swizzle target instead of Root.

Fixed

  • Made theme/MarkdownActionsDropdown self-contained so swizzled copies do not fail to build in consumer sites due to plugin-local relative imports.

I've also bumped the version number to 2.3.0 since it adds a new feature.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants