mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
docs: convert Copy page button into dropdown menu with Open markdown and Edit page options (#6763)
* docs: convert Copy page button to dropdown menu with Open markdown and Edit page options * docs: fix button border highlight and add page footer with actions - Fix split-button hover: highlight entire button group (both main button and chevron) when hovering any part of the group, using .page-actions:hover parent selector - Add PageFooter component with horizontal layout showing Copy page, Open markdown, and Edit page actions at the bottom of every docs page - Footer uses same API endpoints (raw-markdown, resolve-path) and GitHub URL construction as the dropdown button * fix: remove flex class from article-content to fix PageFooter layout The article-content div had Tailwind's 'flex' class which set display:flex with default row direction. The 'column' class was not a valid Tailwind utility (should be 'flex-col'), so the PageFooter rendered to the right of page content instead of below it. Removing 'flex column' restores block layout so children stack vertically. --------- Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
6139456449
commit
168685d988
@@ -1,17 +1,26 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import React, { useState, useEffect, useRef } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
const GITHUB_REPO = "Kilo-Org/kilocode"
|
||||
const GITHUB_BRANCH = "main"
|
||||
|
||||
interface CopyPageButtonProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
function getRoutePath(asPath: string) {
|
||||
const path = asPath.split("#")[0].split("?")[0]
|
||||
return path === "/" ? "/index" : path
|
||||
}
|
||||
|
||||
export function CopyPageButton({ className }: CopyPageButtonProps) {
|
||||
const router = useRouter()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Reset copied/error state after 3 seconds
|
||||
useEffect(() => {
|
||||
if (copied || error) {
|
||||
const timer = setTimeout(() => {
|
||||
@@ -22,18 +31,30 @@ export function CopyPageButton({ className }: CopyPageButtonProps) {
|
||||
}
|
||||
}, [copied, error])
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener("mousedown", handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (copied || error || isLoading) return
|
||||
|
||||
setIsLoading(true)
|
||||
setOpen(false)
|
||||
|
||||
try {
|
||||
// Fetch the raw markdown file based on current route
|
||||
// The route path maps to pages/<path>.md
|
||||
const path = router.asPath.split("#")[0].split("?")[0] // Remove hash and query params
|
||||
const mdPath = path === "/" ? "/index" : path
|
||||
|
||||
// Fetch the raw markdown content from the API route
|
||||
const mdPath = getRoutePath(router.asPath)
|
||||
const response = await fetch(`/docs/api/raw-markdown?path=${encodeURIComponent(mdPath)}`)
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -41,7 +62,6 @@ export function CopyPageButton({ className }: CopyPageButtonProps) {
|
||||
}
|
||||
|
||||
const markdown = await response.text()
|
||||
|
||||
await navigator.clipboard.writeText(markdown)
|
||||
setCopied(true)
|
||||
} catch (err) {
|
||||
@@ -52,33 +72,79 @@ export function CopyPageButton({ className }: CopyPageButtonProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const openGitHubUrl = async (mode: "raw" | "edit") => {
|
||||
setOpen(false)
|
||||
|
||||
try {
|
||||
const mdPath = getRoutePath(router.asPath)
|
||||
const response = await fetch(`/docs/api/resolve-path?path=${encodeURIComponent(mdPath)}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to resolve file path")
|
||||
}
|
||||
|
||||
const { filePath } = await response.json()
|
||||
|
||||
const url =
|
||||
mode === "raw"
|
||||
? `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}/${filePath}`
|
||||
: `https://github.com/${GITHUB_REPO}/edit/${GITHUB_BRANCH}/${filePath}`
|
||||
|
||||
window.open(url, "_blank", "noopener,noreferrer")
|
||||
} catch (err) {
|
||||
console.error(`Failed to open ${mode} URL:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
const label = copied ? "Copied" : error ? "Copy failed" : "Copy page"
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={copied || error || isLoading}
|
||||
className={`copy-page-button ${copied ? "copied" : ""} ${error ? "errored" : ""} ${className || ""}`}
|
||||
aria-label={copied ? "Copied" : error ? "Copy failed" : "Copy page markdown"}
|
||||
title="Copy page as markdown for use with LLMs"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckIcon />
|
||||
<span>Copied</span>
|
||||
</>
|
||||
) : error ? (
|
||||
<>
|
||||
<span>Copy failed</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CopyIcon />
|
||||
<span>Copy page</span>
|
||||
</>
|
||||
<div ref={ref} className={`page-actions ${className || ""}`}>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={copied || error || isLoading}
|
||||
className={`action-button ${copied ? "copied" : ""} ${error ? "errored" : ""}`}
|
||||
aria-label={label}
|
||||
title="Copy page as markdown for use with LLMs"
|
||||
>
|
||||
{copied ? <CheckIcon /> : <CopyIcon />}
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className={`chevron-button ${open ? "active" : ""}`}
|
||||
aria-label="More actions"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="dropdown">
|
||||
<button className="dropdown-item" onClick={handleCopy} disabled={isLoading}>
|
||||
<CopyIcon />
|
||||
<span>Copy page</span>
|
||||
</button>
|
||||
<button className="dropdown-item" onClick={() => openGitHubUrl("raw")}>
|
||||
<FileIcon />
|
||||
<span>Open markdown</span>
|
||||
</button>
|
||||
<button className="dropdown-item" onClick={() => openGitHubUrl("edit")}>
|
||||
<EditIcon />
|
||||
<span>Edit page</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.copy-page-button {
|
||||
.page-actions {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -89,39 +155,103 @@ export function CopyPageButton({ className }: CopyPageButtonProps) {
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
border-radius: 0.5rem 0 0 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.copy-page-button:hover:not(:disabled) {
|
||||
.page-actions:hover .action-button:not(:disabled),
|
||||
.page-actions:hover .chevron-button {
|
||||
background: var(--bg-tertiary, var(--bg-secondary));
|
||||
color: var(--text-brand);
|
||||
border-color: var(--text-brand);
|
||||
}
|
||||
|
||||
.copy-page-button:disabled {
|
||||
.action-button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.copy-page-button.copied {
|
||||
.action-button.copied,
|
||||
.page-actions:hover .action-button.copied {
|
||||
color: var(--success-color, #22c55e);
|
||||
border-color: var(--success-color, #22c55e);
|
||||
background: var(--success-bg, rgba(34, 197, 94, 0.1));
|
||||
}
|
||||
|
||||
.copy-page-button.errored {
|
||||
.action-button.errored,
|
||||
.page-actions:hover .action-button.errored {
|
||||
color: var(--error-color, #ef4444);
|
||||
border-color: var(--error-color, #ef4444);
|
||||
background: var(--error-bg, rgba(239, 68, 68, 0.1));
|
||||
}
|
||||
|
||||
.chevron-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem 0.35rem;
|
||||
font-family: inherit;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: none;
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.chevron-button.active {
|
||||
color: var(--text-brand);
|
||||
border-color: var(--text-brand);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.375rem);
|
||||
left: 0;
|
||||
min-width: 180px;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 50;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: var(--text-secondary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dropdown-item:hover:not(:disabled) {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.dropdown-item:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Copy icon (two overlapping rectangles)
|
||||
function CopyIcon() {
|
||||
return (
|
||||
<svg
|
||||
@@ -140,7 +270,6 @@ function CopyIcon() {
|
||||
)
|
||||
}
|
||||
|
||||
// Check icon for copied state
|
||||
function CheckIcon() {
|
||||
return (
|
||||
<svg
|
||||
@@ -157,3 +286,52 @@ function CheckIcon() {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ChevronDownIcon() {
|
||||
return (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.5 4.5L6 8L9.5 4.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function FileIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M9 1H4a1.5 1.5 0 0 0-1.5 1.5v11A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5V5.5L9 1Z" />
|
||||
<path d="M9 1v5h4.5" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function EditIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M11.5 1.5a2.121 2.121 0 0 1 3 3L5 14l-4 1 1-4 9.5-9.5Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
const GITHUB_REPO = "Kilo-Org/kilocode"
|
||||
const GITHUB_BRANCH = "main"
|
||||
|
||||
function getRoutePath(asPath: string) {
|
||||
const path = asPath.split("#")[0].split("?")[0]
|
||||
return path === "/" ? "/index" : path
|
||||
}
|
||||
|
||||
export function PageFooter() {
|
||||
const router = useRouter()
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (copied || error) {
|
||||
const timer = setTimeout(() => {
|
||||
setCopied(false)
|
||||
setError(false)
|
||||
}, 3000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [copied, error])
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (copied || error || isLoading) return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const mdPath = getRoutePath(router.asPath)
|
||||
const response = await fetch(`/docs/api/raw-markdown?path=${encodeURIComponent(mdPath)}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch markdown")
|
||||
}
|
||||
|
||||
const markdown = await response.text()
|
||||
await navigator.clipboard.writeText(markdown)
|
||||
setCopied(true)
|
||||
} catch (err) {
|
||||
console.error("Failed to copy page:", err)
|
||||
setError(true)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openGitHubUrl = async (mode: "raw" | "edit") => {
|
||||
try {
|
||||
const mdPath = getRoutePath(router.asPath)
|
||||
const response = await fetch(`/docs/api/resolve-path?path=${encodeURIComponent(mdPath)}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to resolve file path")
|
||||
}
|
||||
|
||||
const { filePath } = await response.json()
|
||||
|
||||
const url =
|
||||
mode === "raw"
|
||||
? `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}/${filePath}`
|
||||
: `https://github.com/${GITHUB_REPO}/edit/${GITHUB_BRANCH}/${filePath}`
|
||||
|
||||
window.open(url, "_blank", "noopener,noreferrer")
|
||||
} catch (err) {
|
||||
console.error(`Failed to open ${mode} URL:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
const copyLabel = copied ? "Copied" : error ? "Copy failed" : "Copy page"
|
||||
|
||||
return (
|
||||
<>
|
||||
<footer className="page-footer">
|
||||
<div className="footer-divider" />
|
||||
<div className="footer-actions">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={copied || error || isLoading}
|
||||
className={`footer-action ${copied ? "copied" : ""} ${error ? "errored" : ""}`}
|
||||
title="Copy page as markdown for use with LLMs"
|
||||
>
|
||||
{copied ? <CheckIcon /> : <CopyIcon />}
|
||||
<span>{copyLabel}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openGitHubUrl("raw")}
|
||||
className="footer-action"
|
||||
title="Open raw markdown file on GitHub"
|
||||
>
|
||||
<FileIcon />
|
||||
<span>Open markdown</span>
|
||||
</button>
|
||||
<button onClick={() => openGitHubUrl("edit")} className="footer-action" title="Edit this page on GitHub">
|
||||
<EditIcon />
|
||||
<span>Edit page</span>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<style jsx>{`
|
||||
.page-footer {
|
||||
margin-top: 3rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.footer-divider {
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: var(--text-secondary);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer-action:hover:not(:disabled) {
|
||||
color: var(--text-brand);
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.footer-action:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.footer-action.copied {
|
||||
color: var(--success-color, #22c55e);
|
||||
}
|
||||
|
||||
.footer-action.errored {
|
||||
color: var(--error-color, #ef4444);
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function CopyIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="5" y="5" width="9" height="9" rx="1.5" />
|
||||
<path d="M2 10V3.5A1.5 1.5 0 0 1 3.5 2H10" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CheckIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 8.5L6.5 12L13 4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function FileIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M9 1H4a1.5 1.5 0 0 0-1.5 1.5v11A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5V5.5L9 1Z" />
|
||||
<path d="M9 1v5h4.5" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function EditIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M11.5 1.5a2.121 2.121 0 0 1 3 3L5 14l-4 1 1-4 9.5-9.5Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * from "./Heading"
|
||||
export * from "./Icon"
|
||||
export * from "./Image"
|
||||
export * from "./KiloCodeIcon"
|
||||
export * from "./PageFooter"
|
||||
export * from "./SideNav"
|
||||
export * from "./Table"
|
||||
export * from "./TableOfContents"
|
||||
|
||||
@@ -3,7 +3,7 @@ import Head from "next/head"
|
||||
import { useRouter } from "next/router"
|
||||
import posthog from "posthog-js"
|
||||
|
||||
import { CopyPageButton, SideNav, TableOfContents, TopNav } from "../components"
|
||||
import { CopyPageButton, PageFooter, SideNav, TableOfContents, TopNav } from "../components"
|
||||
|
||||
import "prismjs"
|
||||
import "prismjs/components/prism-bash.min"
|
||||
@@ -178,8 +178,9 @@ export default function MyApp({ Component, pageProps }: AppProps<MyAppProps>) {
|
||||
<SideNav isMobileOpen={isMobileMenuOpen} onMobileClose={handleMobileMenuClose} />
|
||||
<main className="main-content">
|
||||
<div className="content-wrapper">
|
||||
<div className="article-content flex column mt-5">
|
||||
<div className="article-content mt-5">
|
||||
<Component {...pageProps} />
|
||||
{markdoc && <PageFooter />}
|
||||
</div>
|
||||
<div className="right-sidebar" key={router.asPath}>
|
||||
{markdoc && <CopyPageButton />}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" })
|
||||
}
|
||||
|
||||
const { path: mdPath } = req.query
|
||||
|
||||
if (!mdPath || typeof mdPath !== "string") {
|
||||
return res.status(400).json({ error: "Missing path parameter" })
|
||||
}
|
||||
|
||||
const sanitizedPath = path.normalize(mdPath).replace(/^\/+/, "")
|
||||
const pagesDir = path.join(process.cwd(), "pages")
|
||||
const resolvedPagesDir = path.resolve(pagesDir)
|
||||
|
||||
const candidatePath = path.resolve(pagesDir, `${sanitizedPath}.md`)
|
||||
const candidateIndexPath = path.resolve(pagesDir, sanitizedPath, "index.md")
|
||||
|
||||
if (!candidatePath.startsWith(resolvedPagesDir) || !candidateIndexPath.startsWith(resolvedPagesDir)) {
|
||||
return res.status(403).json({ error: "Access denied" })
|
||||
}
|
||||
|
||||
if (fs.existsSync(candidatePath)) {
|
||||
return res.status(200).json({ filePath: `packages/kilo-docs/pages/${sanitizedPath}.md` })
|
||||
}
|
||||
|
||||
if (fs.existsSync(candidateIndexPath)) {
|
||||
return res.status(200).json({ filePath: `packages/kilo-docs/pages/${sanitizedPath}/index.md` })
|
||||
}
|
||||
|
||||
return res.status(404).json({ error: "File not found" })
|
||||
}
|
||||
Reference in New Issue
Block a user