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
13 changes: 12 additions & 1 deletion documentation/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ const config: Config = {
projectName: "goose", // Usually your repo name.

onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",

markdown: {
hooks: {
onBrokenMarkdownLinks: "warn",
},
},

// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
Expand Down Expand Up @@ -315,6 +320,12 @@ const config: Config = {
},
],
tailwindPlugin,
[
require.resolve("./plugins/markdown-export.cjs"),
{
enabled: true,
},
],
],
themes: ["@inkeep/docusaurus/chatButton", "@inkeep/docusaurus/searchBar"],
themeConfig: {
Expand Down
3 changes: 2 additions & 1 deletion documentation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc",
"generate-detail-pages": "node scripts/generate-detail-pages.js"
"generate-detail-pages": "node scripts/generate-detail-pages.js",
"serve-static": "node scripts/serve-static.js"
},
"dependencies": {
"@docusaurus/core": "^3.9.2",
Expand Down
61 changes: 61 additions & 0 deletions documentation/plugins/markdown-export.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const fs = require('fs');
const path = require('path');
const globby = require('globby');

module.exports = function markdownExportPlugin(context, options) {
const pluginOptions = {
enabled: true,
...options,
};

return {
name: 'markdown-export',

async postBuild({ outDir }) {
if (!pluginOptions.enabled) {
return;
}

console.log('[markdown-export] Starting markdown export...');

const docsDir = path.join(context.siteDir, 'docs');
const outputDir = path.join(outDir, 'docs');

// Get all markdown files
const files = await globby('**/*.{md,mdx}', { cwd: docsDir });

for (const file of files) {
const inputPath = path.join(docsDir, file);
const outputPath = path.join(outputDir, file.replace('.mdx', '.md'));

// Ensure output subdirectory exists
fs.mkdirSync(path.dirname(outputPath), { recursive: true });

const content = fs.readFileSync(inputPath, 'utf-8');

// Strip frontmatter and clean up
const cleaned = stripFrontmatter(content);

// Write the cleaned markdown alongside HTML files
fs.writeFileSync(outputPath, cleaned);
}

console.log(`[markdown-export] Successfully exported ${files.length} markdown files to ${outputDir}`);
},
};
};

function stripFrontmatter(content) {
// Remove YAML frontmatter (everything between --- at the start)
const withoutFrontmatter = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');

// Clean up any remaining import statements (for .mdx files)
const withoutImports = withoutFrontmatter.replace(/^import .+$/gm, '');

// Remove excessive empty lines and trim
return withoutImports
.replace(/\n{3,}/g, '\n\n')
.trim();
}


51 changes: 51 additions & 0 deletions documentation/scripts/serve-static.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env node

/**
* Simple static file server for testing markdown exports locally.
* Unlike `docusaurus serve`, this serves files as-is without routing logic.
*/

const http = require('http');
const serveStatic = require('serve-static');
const path = require('path');

const buildDir = path.join(__dirname, '..', 'build');
const port = process.env.PORT || 3001;

const serve = serveStatic(buildDir, {
index: ['index.html'],
setHeaders: (res, filePath) => {
// Set proper content type for markdown files
if (filePath.endsWith('.md')) {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
}
}
});

const server = http.createServer((req, res) => {
// Handle requests to /goose/ by serving from the build directory
if (req.url.startsWith('/goose/')) {
// Strip /goose/ prefix and serve the file
req.url = req.url.substring(6); // Remove '/goose'
serve(req, res, () => {
res.statusCode = 404;
res.end('Not found');
});
} else if (req.url === '/') {
// Redirect root to /goose/
res.writeHead(302, { Location: '/goose/' });
res.end();
} else {
// For any other path, return 404
res.statusCode = 404;
res.end('Not found - try /goose/');
}
});

server.listen(port, () => {
console.log(`\n🚀 Static file server running at http://localhost:${port}`);
console.log(`\n🏠 Homepage: http://localhost:${port}/goose/`);
console.log(`\n📝 Test markdown exports:`);
console.log(` http://localhost:${port}/goose/docs/quickstart.md`);
console.log(` http://localhost:${port}/goose/docs/getting-started/installation.md\n`);
});
96 changes: 91 additions & 5 deletions documentation/src/theme/DocItem/Layout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {type ReactNode, useState, useEffect} from 'react';
import React, {type ReactNode, useState, useEffect, useRef} from 'react';
import type LayoutType from '@theme/DocItem/Layout';
import type {WrapperProps} from '@docusaurus/types';
import {useDoc} from '@docusaurus/plugin-content-docs/client';
Expand All @@ -14,7 +14,7 @@ import DocBreadcrumbs from '@theme/DocBreadcrumbs';
import ContentVisibility from '@theme/ContentVisibility';
import Heading from '@theme/Heading';
import MDXContent from '@theme/MDXContent';
import {Copy, Check} from 'lucide-react';
import {Copy, Check, ChevronDown, FileText, ExternalLink, Eye, Code, FileCode} from 'lucide-react';
import layoutStyles from './styles.module.css';
import TurndownService from 'turndown';

Expand Down Expand Up @@ -357,6 +357,92 @@ function CopyPageButton(): ReactNode {
);
}

// New wrapper component that adds dropdown menu to copy button
function PageActionsMenu(): ReactNode {
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setDropdownOpen(false);
}
};

if (dropdownOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [dropdownOpen]);

// Handle keyboard navigation (Escape to close)
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setDropdownOpen(false);
}
};

if (dropdownOpen) {
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}
}, [dropdownOpen]);

const handleViewMarkdown = () => {
const currentPath = window.location.pathname;
const mdPath = currentPath.endsWith('/')
? `${currentPath.slice(0, -1)}.md`
: `${currentPath}.md`;
window.open(mdPath, '_blank');
setDropdownOpen(false);
};

return (
<div className="relative inline-flex" ref={dropdownRef}>
{/* Button group container - unified appearance */}
<div className="flex items-center bg-black dark:bg-white rounded-md">
{/* Original Copy Page Button - keep its original styling but remove right border radius */}
<div className="[&>button]:rounded-r-none">
<CopyPageButton />
</div>

{/* Divider */}
<div className="w-px h-4 bg-gray-700 dark:bg-gray-300"></div>

{/* Chevron Dropdown Trigger - attached to copy button */}
<button
onClick={() => setDropdownOpen(!dropdownOpen)}
className="px-2 py-1.5 bg-black dark:bg-white text-white dark:text-black rounded-l-none rounded-r-md text-sm font-medium transition-all duration-200 ease-in-out hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-black dark:focus:ring-white focus:ring-offset-2 flex items-center justify-center"
aria-label="More page actions"
aria-expanded={dropdownOpen}
aria-haspopup="true"
>
<ChevronDown size={16} className={`transition-transform duration-200 ${dropdownOpen ? 'rotate-180' : ''}`} />
</button>
</div>

{/* Dropdown Menu */}
{dropdownOpen && (
<div className="absolute right-0 top-full mt-1 w-56 bg-black dark:bg-white rounded-md shadow-lg border border-gray-700 dark:border-gray-300 z-50">
<button
onClick={handleViewMarkdown}
className="w-full flex items-center justify-between gap-1.5 px-3 py-1.5 text-sm text-white dark:text-black hover:opacity-90 hover:-translate-y-px active:translate-y-px transition-all duration-200 ease-in-out font-medium bg-transparent rounded-md"
>
<div className="flex items-center gap-1.5">
<FileCode size={16} className="flex-shrink-0" />
<span>View as Markdown</span>
</div>
<ExternalLink size={16} className="flex-shrink-0" />
</button>
{/* Future menu items can be added here */}
</div>
)}
</div>
);
}

// Hook to determine if we should show the copy button
function useShouldShowCopyButton(): boolean {
const {metadata} = useDoc();
Expand Down Expand Up @@ -395,7 +481,7 @@ function useDocTOC() {
};
}

// Custom Content component that includes the copy button
// Custom Content component that includes the page actions menu
function CustomDocItemContent({children}: {children: ReactNode}): ReactNode {
const shouldShowCopyButton = useShouldShowCopyButton();
const {metadata, frontMatter, contentTitle} = useDoc();
Expand All @@ -409,12 +495,12 @@ function CustomDocItemContent({children}: {children: ReactNode}): ReactNode {
{syntheticTitle && (
<header className="flex justify-between items-start mb-4 flex-col md:flex-row gap-2 md:gap-0">
<Heading as="h1" className="m-0 flex-1">{syntheticTitle}</Heading>
{shouldShowCopyButton && <CopyPageButton />}
{shouldShowCopyButton && <PageActionsMenu />}
</header>
)}
{!syntheticTitle && shouldShowCopyButton && (
<div className="flex justify-end mb-4">
<CopyPageButton />
<PageActionsMenu />
</div>
)}
<MDXContent>{children}</MDXContent>
Expand Down
Loading