Skip to content
Open
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
70 changes: 70 additions & 0 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type * as Preset from "@docusaurus/preset-classic";
import type { Config } from "@docusaurus/types";
import { themes as prismThemes } from "prism-react-renderer";

import markdownExportRegistry from "./src/plugins/markdown-export/registry";
const config: Config = {
title: "Apache Cloudberry (Incubating)",
tagline: "One advanced and mature open-source MPP (Massively Parallel Processing) database. Open source alternative to Greenplum Database.",
Expand All @@ -22,6 +24,33 @@ const config: Config = {
plugins: [
"docusaurus-plugin-sass",
'docusaurus-plugin-matomo',
// Emits a plain-Markdown twin of every page, reachable by appending `.md`
// to its URL. Build-output only; does not affect the rendered site.
[
"./src/plugins/markdown-export",
{
// Both lists are keyed by docs plugin id: the unreleased version of
// *every* instance is named `current`, so a flat list would silently
// take PXF down with `docs/next`.

// Skipped outright. `/docs/1.x/**.md` returns 404 and those pages get
// no Copy page menu; their HTML is untouched. 1.x is legacy and not
// worth the weight it adds to every asf-site commit.
excludeVersions: {
default: ["1.x"],
},

// Exported and linked from the page, but kept out of sitemap.xml --
// the two entry points serve different audiences. A contributor
// reading the dev docs should get the dev docs when they hit Copy
// page; a crawler should not be answering user questions out of an
// unreleased version. Keeping `docs/next` out also spares crawlers 516
// files whose prose is byte-identical to 2.x on 491 of them.
excludeFromSitemap: {
default: ["current"],
},
},
],
[
"@easyops-cn/docusaurus-search-local",
{ hashed: true, indexPages: true, language: ["en"] },
Expand Down Expand Up @@ -69,6 +98,47 @@ const config: Config = {
"Apache Cloudberry (Incubating) is one advanced and mature open-source MPP (Massively Parallel Processing) databases available.",
},
},
sitemap: {
// List the Markdown twins alongside the HTML pages. `<link
// rel="alternate">` already announces them per page, but sitemap.xml
// is the one discovery file AI crawlers reliably fetch, and nothing
// else on the site links to a `.md` URL.
//
// Tradeoff: every page now appears twice, and ASF's static hosting
// gives us no way to send `X-Robots-Tag: noindex` on the Markdown
// half. Delete this block to go back to HTML-only.
createSitemapItems: async ({
defaultCreateSitemapItems,
...params
}) => {
const items = await defaultCreateSitemapItems(params);

// Reuse each page's own lastmod so the twin is never treated as
// fresher (or staler) than the page it mirrors. A no-op today --
// the plugin's `lastmod` option defaults to null, so no entry
// carries one -- but it keeps the two halves in step if that is
// ever switched on.
const lastmodByPath = new Map(
items.map((item) => [
new URL(item.url).pathname.replace(/\/$/, ""),
item.lastmod,
]),
);

// Populated by `markdown-export` in `allContentLoaded`, which
// always runs before any `postBuild`. See registry.js.
const twins = [...markdownExportRegistry.sitemapPermalinks].map(
(permalink) => ({
url: `${params.siteConfig.url}${markdownExportRegistry.markdownPathFor(
permalink,
)}`,
lastmod: lastmodByPath.get(permalink.replace(/\/$/, "")),
}),
);

return [...items, ...twins];
},
},
theme: {
customCss: [
"./src/css/custom.scss",
Expand Down
174 changes: 174 additions & 0 deletions src/components/common/AiActions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useClickAway } from "ahooks";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import clsx from "clsx";

import { markdownPathFor } from "@site/src/components/common/markdownTwin";

import styles from "./styles.module.scss";

function promptFor(markdownUrl: string): string {
return `Read ${markdownUrl} — I have questions about this Apache Cloudberry documentation page.`;
}

type CopyState = "idle" | "busy" | "done" | "error";

const COPY_LABEL: Record<CopyState, string> = {
idle: "Copy page",
busy: "Copying…",
done: "Copied!",
error: "Copy failed",
};

export interface Props {
/** Permalink of the current page, as produced by the content plugin. */
permalink: string;
className?: string;
/**
* Edge the dropdown grows from. Must match how the trigger itself is aligned
* in its container -- otherwise the panel opens past the viewport edge on
* narrow screens.
*/
align?: "start" | "end";
}

export default function AiActions({
permalink,
className,
align = "end",
}: Props): JSX.Element {
const { siteConfig } = useDocusaurusContext();
const [open, setOpen] = useState(false);
const [copyState, setCopyState] = useState<CopyState>("idle");
const containerRef = useRef<HTMLDivElement>(null);

const markdownPath = markdownPathFor(permalink);
// Deep links are resolved by a third party, so they need the canonical
// origin -- a dev-server URL would be unreachable to them anyway.
const prompt = encodeURIComponent(promptFor(`${siteConfig.url}${markdownPath}`));

useClickAway(() => setOpen(false), containerRef);

useEffect(() => {
if (!open) {
return undefined;
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpen(false);
}
};
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [open]);

// Let the transient copy result fall back to the resting label.
useEffect(() => {
if (copyState !== "done" && copyState !== "error") {
return undefined;
}
const timer = window.setTimeout(() => setCopyState("idle"), 2200);
return () => window.clearTimeout(timer);
}, [copyState]);

const handleCopy = useCallback(async () => {
setOpen(false);
setCopyState("busy");
try {
const response = await fetch(markdownPath);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// Unavailable outside secure contexts (plain-HTTP dev hosts); the catch
// below surfaces that rather than failing silently.
await navigator.clipboard.writeText(await response.text());
setCopyState("done");
} catch {
setCopyState("error");
}
}, [markdownPath]);

return (
<div className={clsx(styles.root, className)} ref={containerRef}>
<button
type="button"
className={styles.trigger}
aria-haspopup="menu"
aria-expanded={open}
onClick={() => setOpen((value) => !value)}
>
<span className={styles.triggerLabel}>{COPY_LABEL[copyState]}</span>
<svg
className={clsx(styles.chevron, open && styles.chevronOpen)}
width="10"
height="10"
viewBox="0 0 10 10"
aria-hidden="true"
>
<path
d="M2 3.5L5 6.5L8 3.5"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>

{open && (
<div
className={clsx(styles.menu, align === "start" && styles.menuStart)}
role="menu"
>
<button
type="button"
role="menuitem"
className={styles.item}
onClick={handleCopy}
>
<span className={styles.itemLabel}>Copy as Markdown</span>
<span className={styles.itemHint}>
Clean page source, ready to paste into a chat
</span>
</button>

<a
role="menuitem"
className={styles.item}
href={markdownPath}
target="_blank"
rel="noreferrer"
>
<span className={styles.itemLabel}>View as Markdown</span>
<span className={styles.itemHint}>Open the plain-text source</span>
</a>

<div className={styles.separator} role="separator" />

<a
role="menuitem"
className={styles.item}
href={`https://claude.ai/new?q=${prompt}`}
target="_blank"
rel="noreferrer"
>
<span className={styles.itemLabel}>Open in Claude</span>
<span className={styles.itemHint}>Ask with this page as context</span>
</a>

<a
role="menuitem"
className={styles.item}
href={`https://chatgpt.com/?q=${prompt}`}
target="_blank"
rel="noreferrer"
>
<span className={styles.itemLabel}>Open in ChatGPT</span>
<span className={styles.itemHint}>Ask with this page as context</span>
</a>
</div>
)}
</div>
);
}
Loading
Loading