Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ const config: Config = {
path.resolve(__dirname, './src/plugins/releases/index.ts'),
path.resolve(__dirname, './src/plugins/fiddle/index.ts'),
path.resolve(__dirname, './src/plugins/governance/index.ts'),
path.resolve(__dirname, './src/plugins/markdown-output/index.ts'),
],
presets: [
[
Expand Down
83 changes: 83 additions & 0 deletions src/plugins/markdown-output/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Plugin to output index.md files alongside index.html in the build directory.
* This copies the original markdown source files to their corresponding locations
* in the build output.
*/

import fs from 'node:fs';
import path from 'node:path';
import { Plugin } from '@docusaurus/types';
import logger from '@docusaurus/logger';

/** Route prefixes to copy markdown files within. */
const PREFIXES = ['docs/', 'blog/'];

type Context = Parameters<Plugin['postBuild']>[0];

function findMarkdownSource(
routePath: string,
{ siteDir }: Context,
): string | null {
for (const prefix of PREFIXES) {
const routePrefix = `/${prefix}`;

if (!routePath.startsWith(routePrefix)) continue;
Comment on lines +22 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably doesn't play nice with i18n as those are prefixed by the language code. I think ideally we'd also serve the translated Markdown, so this should be updated to take that into account.


// Since routePrefix is absolute and routePath starts with routePrefix, it too must be absolute
// meaning we can safely use path.relative to get the relative path
const relativePath = path.relative(routePrefix, routePath);
const sourceDir = path.join(siteDir, prefix);

// Try direct file path first (e.g., /docs/tutorial/intro.md)
const directPath = path.join(sourceDir, `${relativePath}.md`);
if (fs.existsSync(directPath)) return directPath;

// Try index.md in directory (e.g., /docs/tutorial/intro/index.md)
const indexPath = path.join(sourceDir, relativePath, 'index.md');
if (fs.existsSync(indexPath)) return indexPath;
}

return null;
}

function copyMarkdownForRoute(routePath: string, context: Context): boolean {
const { outDir } = context;

// Find corresponding markdown source
const mdSourcePath = findMarkdownSource(routePath, context);
if (!mdSourcePath) return false;

// Check if HTML output exists
const htmlPath = path.join(outDir, routePath, 'index.html');
if (!fs.existsSync(htmlPath)) return false;

// Copy markdown to output directory
const outputMdPath = path.join(outDir, routePath, 'index.md');
fs.copyFileSync(mdSourcePath, outputMdPath);

return true;
}

export default function markdownOutputPlugin(): Plugin {
return {
name: 'markdown-output-plugin',
postBuild(context) {
logger.info('Copying markdown files to build directory...');

// Filter to only routes in target prefixes
const routes = context.routesPaths.filter((routePath) =>
PREFIXES.some((prefix) => routePath.startsWith(`/${prefix}`)),
);

// Process all routes
let copiedCount = 0;
for (const routePath of routes) {
if (copyMarkdownForRoute(routePath, context)) {
copiedCount += 1;
}
}

logger.success(`Copied ${copiedCount} markdown files to build directory`);
},
};
}