-
-
Notifications
You must be signed in to change notification settings - Fork 8.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extend existing content plugins (CMS integration, middleware, doc gen...) #4138
Comments
Hi Mark,
You probably mean using Docusaurus as the UI of a headless CMS :) It's nice to see work being done integrating Docusaurus with WordPress or any other CMS, afaik it's not something many have done so far. What I understand is that your plugin downloads the files as MDX, writes then to the disc, and they are expected to be found by the official blog plugin. So basically the blog plugin (or all others with undefined priority) would somehow need to wait for the completion of your plugin's I don't think it's a good idea to introduce this plugin ordering logic. We really want plugins to work in isolation and not interfere with each other. This ordering logic is also something that was often requested on Gatsby and they pushed hard against implementing it. My suggestion to this problem is that you should not actually write a new Docusaurus plugin for this, but instead enhance the existing blog plugin. If we created a blog plugin In an ideal world, I'd like to find a better solution, so that you could use the blog plugin without even having to write .mdx files to the file-system, but we are not there yet. |
Hi Sebastien Good to speak again.. I took the priority approach to avoid creating too big a change, but what you suggest would also work. I did wonder whether we would want to change the lifecycle, which is a more fundamental change?
I could then just create my docs during This is just a simple example of how the current plugin works Might be useful to see the blog, I've written for our spike. Part 1, is the setup of Wordpress with Docusaurus My thoughts, were to start simple and re-generate the MDX on build. Creating React components in Gutenberg and then exporting them to Docusaurus, works quite well as a demo, our next steps are to see how well this works for a real use-case. Keen to hear your thoughts. |
We could add this to docs and pages as well. My goal here is to find a good enough workaround to unlock your use case without introducing a new public API that we should not need in the first place. If we can avoid a new core priority API or core lifecycle APIs I think it's preferable, and I'd rather push this feature to our 3 official content plugins, and find a solution for content plugins to be somehow extensible. And each plugin should probably decide by itself how it can be extended. This is probably not the most straightforward code, but I think the following could be a pattern we could push forward and support/document better in the future, as it basically does not increase any API surface but let you solve this problem. const blogPluginExports = require('@docusaurus/plugin-content-blog');
const blogPlugin = blogPluginExports.default;
function blogPluginEnhanced(...pluginArgs) {
const blogPluginInstance = blogPlugin(...pluginArgs);
return {
...blogPluginInstance,
loadContent: async function (...loadContentArgs) {
console.log('will load some WordPress files');
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('WordPress files loaded');
return blogPluginInstance.loadContent(...loadContentArgs);
},
};
}
module.exports = {
...blogPluginExports,
default: blogPluginEnhanced,
}; Somehow you "intercept" the existing plugin lifecycle methods and wrap them with custom logic. I tested it and it works fine. Maybe the most generic feature we could implement to support this is a "plugin middleware" feature? That would permit to somehow reduce the boilerplate necessary to write the code above to something more simple like: [
'@docusaurus/plugin-content-blog',
{
... other options
middleware: plugin => ({
...plugin,
loadContent: async function (...loadContentArgs) {
console.log('will load some WordPress files');
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('WordPress files loaded');
return plugin.loadContent(...loadContentArgs);
},
}),
},
], We could also create an official helper function to actually provide this plugin lifecycle wrapping feature Does it make sense? |
A nice solution, that will work. I don't think we need any middleware at this stage of things. If you come across anyone attempting to do anything similar (or anyone reads this post and wants to contribute) please let me know. In the meantime, keep you posted on our progress. Thanks for your help. |
Great, let me know if this works for you. I'll rename this issue, as we agree it's preferable to extend a plugin that to implement a priority api. We'll see how much people need this and try to get some feedback on an RFC before implementing it.
I'm not 100% sure it will work easily for the preset, but you can disable the preset content plugins with |
I'm going to rebuild the plugin loader to allow plugins to interact with each other. |
@slorber I am trying to extend the content-docs plugin for API doc generation, and my use-case involves setting a new name for the plugin in order to keep the ability to use the original content-docs plugin for other sections of the site. Unfortunately, the plugin cache path is hardcoded here
And I'm stuck with ENOENT errors... Perhaps the plugin function could take optional arguments for customization. |
@jsamr it's hard for me to understand your usecase, you'd rather open another issue with all the details and a simplified example of what you are trying to achieve, that I could run locally
Maybe using a different pluginId could solve the problem?
Sure we want to do that but this requires a proper API design |
An example integration of the Kentico Kontent CMS with Docusaurus, as a plugin CLI extension ( |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@kgajera Are you using TypeScript? If so, you don't need the |
|
Ah yes, it's because we have a custom module declaration that only exports a bunch of types and doesn't declare the actual plugin function. You need to write a declaration yourself, using our implementation as a reference |
Note we don't support yet TS config files / plugin files. If you want to write a plugin in TS, you will have to compile it to JS first before being able to run it. |
Whats the state on this? It would be a cool feature. I just copy and pasted and changed alot in order to create my own plugin with extended pluginData. |
I came here to say the same thing, I'm currently working on syncing content from a headless CMS. |
My top priority atm is to upgrade some infra (mdx2, React 18). Once it's done I'll work on a few issues including this one to keep increasing the flexibility of Docusaurus |
I jumped on this issue and just want to add my use case here in order to give you a concrete scenario on what a possible upgrade would be cool to cover: My use case is that I want a landing Page that should display all doc Tags alongside clickable card components that link to 2 docs that have that Tag. Those cards should contain content from the docs, e.g. title, description, a link of course. The only way I was possible to solve this is to make a copy of the docs plugin and add the functionality there, which was a good learning excercise, and gives me options to change a lot more. Thanks for looking into this |
I am looking into this issue and would like to add my use case to my site: I want to filter my blogs with multiple tags supported( Thanks for making Docusaurus more accessible and easy to add customized features. |
Some useful comments to consider:
|
Hey 👋 I know it's particularly challenging for the community to understand how to overcome this Docusaurus limitation of plugins not seeing each other's data. For this reason I created a good example you can take inspiration from, by displaying the recent blog posts on your homepage. The idea is that you extend the blog plugin to create your own blog plugin. Then you uses the plugin loaded data, and create an additional page (possibly your homepage), injecting into it the props and data you need. Note: the data you inject as props has to be converted to JSON module bundles first, that's why we call ImplementationA runnable example is available here: The implementation has been a bit inspired by this community blog post: https://kgajera.com/blog/display-recent-blog-posts-on-home-page-with-docusaurus/ I hope you'll find it useful. The implementation relies on these 3 parts: custom-blog-plugin.js// ./custom-blog-plugin.js
const blogPluginExports = require('@docusaurus/plugin-content-blog');
const defaultBlogPlugin = blogPluginExports.default;
async function blogPluginExtended(...pluginArgs) {
const blogPluginInstance = await defaultBlogPlugin(...pluginArgs);
const pluginOptions = pluginArgs[1];
return {
// Add all properties of the default blog plugin so existing functionality is preserved
...blogPluginInstance,
/**
* Override the default `contentLoaded` hook to access blog posts data
*/
contentLoaded: async function (params) {
const { content, actions } = params;
// Get the 5 latest blog posts
const recentPostsLimit = 5;
const recentPosts = [...content.blogPosts].splice(0, recentPostsLimit);
async function createRecentPostModule(blogPost, index) {
console.log({ blogPost });
return {
// Inject the metadata you need for each recent blog post
metadata: await actions.createData(
`home-page-recent-post-metadata-${index}.json`,
JSON.stringify({
title: blogPost.metadata.title,
description: blogPost.metadata.description,
frontMatter: blogPost.metadata.frontMatter,
})
),
// Inject the MDX excerpt as a JSX component prop
// (what's above the <!-- truncate --> marker)
Preview: {
__import: true,
// The markdown file for the blog post will be loaded by webpack
path: blogPost.metadata.source,
query: {
truncated: true,
},
},
};
}
actions.addRoute({
// Add route for the home page
path: '/',
exact: true,
// The component to use for the "Home" page route
component: '@site/src/components/Home/index.js',
// These are the props that will be passed to our "Home" page component
modules: {
homePageBlogMetadata: await actions.createData(
'home-page-blog-metadata.json',
JSON.stringify({
blogTitle: pluginOptions.blogTitle,
blogDescription: pluginOptions.blogDescription,
totalPosts: content.blogPosts.length,
totalRecentPosts: recentPosts.length,
})
),
recentPosts: await Promise.all(
recentPosts.map(createRecentPostModule)
),
},
});
// Call the default overridden `contentLoaded` implementation
return blogPluginInstance.contentLoaded(params);
},
};
}
module.exports = {
...blogPluginExports,
default: blogPluginExtended,
}; docusaurus.config.js{
// ...
plugins: [
// Use custom blog plugin
[
'./custom-blog-plugin',
{
id: 'blog',
routeBasePath: 'blog',
path: './blog',
blogTitle: 'My Awesome Blog',
blogDescription: 'A great blog with homepage Docusaurus integration',
},
],
],
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
},
blog: false,
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],
// ...
} src/components/Home/index.jsimport React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import HomepageFeatures from '@site/src/components/HomepageFeatures';
import styles from './index.module.css';
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div>
<Link
className="button button--secondary button--lg"
to="/docs/intro"
>
Docusaurus Tutorial - 5min ⏱️
</Link>
</div>
</div>
</header>
);
}
function RecentBlogPostCard({ recentPost }) {
const { Preview, metadata } = recentPost;
return (
<article style={{ padding: 20, marginTop: 20, border: 'solid thick red' }}>
<h2>{metadata.title}</h2>
<p>{metadata.description}</p>
<p>FrontMatter: {JSON.stringify(metadata.frontMatter)}</p>
<hr />
<Preview />
</article>
);
}
export default function Home({ homePageBlogMetadata, recentPosts }) {
const { siteConfig } = useDocusaurusContext();
console.log({ homePageBlogMetadata, recentPosts });
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />"
>
<HomepageHeader />
<main style={{ padding: 30 }}>
<h1>{homePageBlogMetadata.blogTitle}</h1>
<p>{homePageBlogMetadata.blogDescription}</p>
<p>
Displaying some sample posts:
{homePageBlogMetadata.totalRecentPosts} /{' '}
{homePageBlogMetadata.totalPosts}
</p>
<section>
{recentPosts.map((recentPost, index) => (
<RecentBlogPostCard key={index} recentPost={recentPost} />
))}
</section>
<hr />
<HomepageFeatures />
</main>
</Layout>
);
} |
Hi @slorber apologies if this has already been answered elsewhere I could not find the answer and just found this thread from a couple of years ago. I am currently working on a documentation site for a client, who will also have non-technical users editing the product/platform side of the documentation. I am currently using spinalcms.com (a file based / git cms similar to netlify cms) however it has limitations on using MDX and markdown tables which is not ideal as I want the documentation to be interactive and amazing in all ways :) Anyways, I've used headless cms like storyblok/contentful/sanity before etc which can be really flexible to whatever you want to do with functions in next.js like getStaticPaths and getStaticProps. I wondered if there was any solution to use a headless cms for docs content like this or advice you could give. My only core idea so far was I could create a pre-build script in my package.json which would run before the docusarus build and generate the markdown files from the headless cms api. I don't know if this is a better idea than creating a plugin. Thanks in advance. p.s. my use case is more related to documentation pages as opposed to rendering a blog or custom page. |
@samducker that's what most CMS + Docusaurus users do today: as long as you get the markdown files on the filesystem it should work. We'll explore how to provide a way to integrate with CMS without this intermediate build step in the future but for now it's only only way that does not involve retro-engineering our plugin's code. |
I really like this middleware idea proposed by @slorber above. This would also fix the "infinite loop on refresh" issue for custom plugins that provide data to put in Docs. I managed to find a workaround for the issue (see this StackOverflow answer I just wrote), but this proposed feature would make the problem much easier to solve. My personal use case is not with a CMS as a source, but another subproject in the same monorepo. The monorepo has three subprojects - a JS API library, a static site for testing and demoing the library, and a Docusaurus site with API documentation. I wanted to reuse the "demo site" code as "example API usage code" in the Docusaurus site, so I wrote a plugin to copy the source code from the other subproject and write it to the |
💥 Proposal
I've written a couple of blog posts (which I will shortly publish) on how to use Docusaurus as a headless CMS, with Wordpress's Gutenberg.
I've also written a sample plugin,
wordpress-to-docusaurus-plugin
which uses GraphQL to pull Blog posts from Wordpress, which I then use to build Docusaurus posts with.All looks good but one problem remains. Plugins currently load in parallel, so new blog posts are not seen until I reload. I need my plugin's
loadContent
to run before the blog plugin runs, so it sees the new posts, I've created.I would like to add an optional
loadingPriority
to plugin options, which defines the order they are loaded.This will enable us to still load plugins in parallel or without any priority defined but for certain use-cases, we can prioritise the order of loading.
I've found, I only need to prioritise,
loadContent
, so we could have a single numericloadingPriority
option which is available to any plugin.It's small change that is required to the existing code to make this possible.
Is this a PR you would be interested in reviewing or do you have an alternative preference ?
Have you read the Contributing Guidelines on issues?
Yes
The text was updated successfully, but these errors were encountered: