facebook / docusaurus

Easy to maintain open source documentation websites.
https://docusaurus.io
MIT License
56.28k stars 8.45k forks source link

Allow a single docs folder to be consumed by 2 docs plugin instances (use-case: normal vs embed/iframe docs) #7480

Open alexandernst opened 2 years ago

alexandernst commented 2 years ago

Have you read the Contributing Guidelines on issues?

Prerequisites

Description

If I add the docs plugin to the list of plugins (multi-instance) and I point it to the same folder that the preset is already pointing to (docs by default), the build process stops working and I see the following errors:

SyntaxError: /code/src/......../index.mdx: Identifier 'React' has already been declared. (46:184)

I believe the bug might be caused by the way each mdx file is being processed. Because a mdx file is (basically) a JSX file, import React from 'react'; is being prepended at the top of each file. But since I have two instances of the docs plugin pointing to the same folder, the import statement is being prepended twice (hence the error).

I made a minimal repro codesandbox.

Reproducible demo

https://codesandbox.io/s/trusting-rosalind-sc4s15

Steps to reproduce

  1. Open the link
  2. Open a new tty and type npm run build
  3. The build will fail.
  4. Comment the plugins section of the docusaurus.config.js
  5. Run npm run build again.
  6. The build will succeed.

Expected behavior

I should be able to point multiple instances of the docs plugin to the same folder.

Actual behavior

The build crashes.

Your environment

Self-service

Josh-Cena commented 2 years ago

Interesting. I get why it fails, but I don't understand what you are trying to achieve—what's the use-case? We do expect plugins to work in isolation and their folder paths do not overlap.

alexandernst commented 2 years ago

I'd like to be able to render the same documentation with different layouts on different URLs. Eg. foo.com/docs/intro should render the navbar, the sidebar, the docs itself, etc..., while foo.com/embedded/intro should render just the intro document, without any other elements (no navbar, no sidebar, no header, no footer, etc...). My use-case is being able to embed a particular document inside an iframe.

Josh-Cena commented 2 years ago

Ah, I see. I don't really think you need multiple plugins to do this, but I don't have a better solution on top of my head right now. Because you have the same set of data and metadata, it should be doable within the same plugin, just passing it to two themes.

alexandernst commented 2 years ago

How could I do that?

Josh-Cena commented 2 years ago

I don't really know. I don't think it's possible with today's setup, which is why I'm still leaving it open :) But it's definitely legitimate and we'll figure out a way to do it. Sorry if you think it's going to be a quick little bug fix—it's probably more complicated than we have thought.

@slorber will be back tomorrow, and maybe he can have some opinions to offer as well.

alexandernst commented 2 years ago

@slorber Hi! Did you have a chance to look into this proposal?

slorber commented 2 years ago

No, I am reviewing existing PRs for now

slorber commented 2 years ago

@alexandernst this definitively makes sense to me, and is something I actually suggested in related discussions:

I'm surprised that it doesn't work actually, it's worth investigating.

We could allow you to do what you initially want: using 2 docs plugin instances and customize the layout accordingly.


Let's also focus on the use case instead of the actual technical details, and see if we can come up with a better technical proposal:

I'm not 100% convinced using 2 docs plugin instances is the ideal solution, because:

Some possible technical solutions:

https://codesandbox.io/s/white-monad-b55vuh?file=/src/pages/testEmbed.js

import React from "react";
import Foo from "@site/docs/foo.mdx";

export default function FooEmbed() {
  return (
    <main>
      <h1>Embedded doc without layout</h1>
      <Foo />
    </main>
  );
}
alexandernst commented 2 years ago
  • why do you want to embed docs?

I have a product with a lot of "elements" (think about it as if the product was a 3D viewer of a car and each element a piece of that car). I want to point users to the documentation site and let them read the entire doc, but I'd also like to be able to show the particular documentation page for each piece.

  • how do you want to embed the docs? (popup, iframe, WebView...)

When each element is clicked a modal window (containing several config options) is shown. Embedding the docs inside an iframe in a tab in that modal window sounds like a reasonably good idea to me.

  • do you need to embed the whole docs folder, or just a few docs?

Each "piece" will have it's own document page, so I'd show only that particular doc page.

  • can you provide a production URL of a real doc that you would like to embed?

No, none of this is publicly available :(

  • do you care much about the embedded doc URL? (ie should it start with /docs/ vs /embed)

Not at all. That would be an iframe, so the url doesn't really matter.

  • what should happen if the user clicks a link on that page?

Hmmm 🤔 haven't really thought about that. Maybe go to whatever link the user clicked, but the "embedded" version?

  • it creates a different canonical URL for a doc that has exactly the same content, which may be bad for SEO (although google may understand this, I'm not sure it does. Note that for docs version, we may be able to add additional metadata in pages in the future to mitigate this)

Indeed. Maybe all of this could be avoided by implementing #5046

  • have you tried injecting custom CSS in the iframe/webview? You may try to manually include it in a hacky way as a post-build step for example, and only add the class if the iframe is loaded with a given querystring ?embed=true)

That really sounds like a hacky solution, not my style I'm afraid.

  • have you tried creating a custom page for the doc you want to embed, and importing the MDX file you want there? You wouldn't need to apply a layout on this page.

That is theoretically possible, but I'll have hundreds of "pieces", which means hundreds of "doc pages". I'm afraid that will grow up to "unmanageable" really fast.

  • Another weird workaround would be to try using 2 distinct Docusaurus sites 😅

I'd rather just copy (on build time) the entire "docs" folder and do it with 2 instances of the docs plugin.

alexandernst commented 2 years ago

I'm very interested in this feature, so I might as well try to do a PR. Have you got any thoughts about how this should be implemented?

slorber commented 2 years ago

First I wonder if it should be a docs feature or a core (blog, pages too) feature? I think it could make sense in core.

The way I see it is that we should have a way to create "variants" of a given route.

Ideally, I'd like this to be flexible:

It would render the same entry component but would put some "variant = xyz" in some React route context (we have a "routeContext" hidden feature that we could use for that), and the component may tweak some things depending on the rendered variant.

Our theme components should be able to render variants undefined | "embed" by default, but you could implement your own variants the way you want or change defaults.

The variants would have the same props and thus the same permalink/canonical URL by default => better for SEO

We should also handle SPA links differently?

I guess it should be possible to create a core API to support all that (or a plugin lifecycle 🤔 )

alexandernst commented 2 years ago

Hi @slorber ! My project's priorities have changed and I must drop this task, I'm so sorry :(

slorber commented 2 years ago

No problem, we'll continue investigating.

I also have the use-case for a single folder being consumed by 2 plugins myself, as I'd like to render MDX blog posts in 2 variants:

dustinlacewell commented 2 years ago

This is driving me nuts. :( Sorry.

slorber commented 2 years ago

This is driving me nuts. :( Sorry.

not sure what you mean here 🤷‍♂️ if you are blocked by this issue please describe what's your use-case and what prevents you to achieve it. Having multiple users reporting their pain helps us choose how we want to solve it

slorber commented 1 year ago

For the embedding use-case, would it be useful if we allowed applying a global CSS class directly from the iframe query string?

For example:

<iframe src="https://docusaurus.io/docs/intro?docusaurus-html-class=my-custom-class"></iframe>

This could allow you to easily plug custom CSS rules depending on the class you inject. For example, you could decide to hide the Docusaurus navbar if the class is present:

html.my-custom-class nav.navbar {
  display: none;
}

The class would be applied to <html> as soon as possible, with static inlined JavaScript (similar to what we do with light/dark theme), so there wouldn't be any flash of unstyled content.

The idea was initially discussed here: https://github.com/facebook/docusaurus/issues/8672#issuecomment-1451691277 where we also allow define the Docusaurus theme from a querystring variable (PR: https://github.com/facebook/docusaurus/pull/8708)

alexandernst commented 1 year ago

I think that this is a good idea! Maybe docusaurus could append that custom docusaurus-html-class query tag to the links that are rendered, so if the user clicks on a link, the frame will load the content with the same styling?

slorber commented 1 year ago

Thanks @alexandernst , that makes sense that the class stays present as the user navigates the embedded document, so that the layout of that element does not switch to the regular layout.

Actually, I'm not sure it would be that easy to add a class name to <html> because of the way react-helmet works: it tends to override classes that are added imperatively afaik. We'll have to test this a bit.

It's possible that allowing users to provide a data-attribute is a safer choice. Once data attributes are set, they should never be overridden. Even if the URL does not contain it anymore (on link click), if we set <html data-embedded="true"/> it should stay there forever (as long as you navigate with SPA/Docusaurus <Link> component).

<iframe src="https://docusaurus.io/docs/intro?docusaurus-data-embedded=true"></iframe>
[data-embedded='true'] nav.navbar {
  display: none;
}

Let's collect a bit more feedback to see what others think.

jbguerraz commented 1 year ago

Hello mates :wave: Hello @slorber

We would also love embedding the documentation within a (react) app.

Let's say we have a (react) low-code app and some documentation for full platform documentation available on a static website docs.platform.domain When we're (using the low-code app) for instance editing some entity, we'd love to have an help button that would then display the documentation for the current entity type in the current window, without loading an iframe but rendering a component like so <Docusaurus route="/docs/entity/someentityttye">

That would really be cool! Maybe then we could add some properties to, for instance, show or hide the frame (header, footer, nav, ...)

We'd be ready to invest time and contribute such feature if somehow it makes sens and if it somehow align with the current project architecture :-)

slorber commented 1 year ago

@jbguerraz

without loading an iframe but rendering a component like so

I don't understand wha you mean here. If it's not an iframe, how can you reference a pathname. You are either rendering a local React component or inside an iframe, I don't see how to do otherwise


That would really be cool! Maybe then we could add some properties to, for instance, show or hide the frame (header, footer, nav, ...)

In https://github.com/facebook/docusaurus/issues/8672 I suggested that we could inject extra classes from querystring params, so that you can decide to show/hide things conditionally.

<iframe src="https://docs.platform.domain/docs/entity?docusaurus-html-class=embedded-iframe`>

Would this help?

Note we don't implement the behavior for .embedded-iframe, but we allow you to implement it without duplicating the page for embedding.

dvdokkum commented 1 year ago

Just want to chime in and say injectable classes via querystring param would be a big help for us. We maintain a full docs site, but also want to embed certain docs directly in our main application. Being able to drop the navbar element would make the execution much cleaner.

slorber commented 1 year ago

@dvdokkum I implemented it, it should be available in canary today or in v3 alpha/beta/rc/stable a bit later

I only implemented the support for data-attribute injection because with classes it was causing issues on React hydration: React Helmet is erasing the classes that we add inline when it takes over. I think it should be good enough for styling purpose and data-attribute CSS selectors.

https://github.com/facebook/docusaurus/pull/9028

image

In the future it's possible that we design our own official "iframe/embedded mode" but for now we give you the flexibility to implement it yourself. Having your early adopter feedback on this can actually help design the official iframe mode, so please report if it works fine for you.

dvdokkum commented 1 year ago

Awesome, thank you! I'll give it a whirl later today.

dvdokkum commented 1 year ago

Yeah, works great, thanks! One thing I am noticing though (probably unrelated?) is that in the Canary release the copy button is no longer appearing on code blocks. Confirmed it wasn't anything I added related this feature, when I pop my version back to @latest the copy buttons come back.

slorber commented 1 year ago

in the Canary release the copy button is no longer appearing on code blocks.

🤷‍♂️ I still see the copy button. Are you sure?