diff --git a/src/components/docs/DocsLayout.tsx b/src/components/docs/DocsLayout.tsx
index f3c67042..42756a57 100644
--- a/src/components/docs/DocsLayout.tsx
+++ b/src/components/docs/DocsLayout.tsx
@@ -5,9 +5,9 @@ import { DocsSidebar } from './DocsSidebar';
import { TableOfContents } from './TableOfContents';
import { MobileTOC } from './MobileTOC';
import { MobileHeader } from './MobileSidebarToggle';
-import { EditPageLink } from './EditPageLink';
import { useDocsMenu } from './DocsProvider';
import type { ProjectId } from '@/config/versions';
+import { DocsSourceActions } from '@/components/docs/DocsSourceActions';
interface TOCItem {
id: string;
@@ -71,7 +71,11 @@ export function DocsLayout({ children, pageMap, toc, metadata, filePath, project
{/* Edit page icon - top right */}
{filePath && projectId && (
-
+
)}
diff --git a/src/components/docs/DocsSourceActions.tsx b/src/components/docs/DocsSourceActions.tsx
new file mode 100644
index 00000000..e17600ef
--- /dev/null
+++ b/src/components/docs/DocsSourceActions.tsx
@@ -0,0 +1,120 @@
+"use client";
+
+import { useSharedConfig } from "@/hooks/useSharedConfig";
+import type { ProjectId } from "@/config/versions";
+
+const STATIC_EDIT_BASE_URLS: Record = {
+ kubestellar: "https://github.com/kubestellar/docs/edit/main/docs/content",
+ a2a: "https://github.com/kubestellar/a2a/edit/main/docs",
+ kubeflex: "https://github.com/kubestellar/kubeflex/edit/main/docs",
+ "multi-plugin": "https://github.com/kubestellar/kubectl-multi-plugin/edit/main/docs",
+ klaude: "https://github.com/kubestellar/kubectl-claude/edit/main/docs",
+ console: 'https://github.com/kubestellar/console/edit/main/docs',
+};
+
+// Prevent path traversal
+function sanitizeFilePath(filePath: string): string {
+ return filePath.replace(/\.\./g, "").replace(/^\/+/, "");
+}
+
+// Force fork-based editing for ALL users (including admins)
+function buildGitHubEditUrl(
+ filePath: string,
+ projectId: ProjectId,
+ editBaseUrls?: Record
+): string | null {
+ const baseUrl =
+ editBaseUrls?.[projectId] ?? STATIC_EDIT_BASE_URLS[projectId];
+
+ if (!baseUrl) return null;
+
+ return `${baseUrl}/${sanitizeFilePath(filePath)}?fork=true`;
+}
+
+// Validate GitHub edit URL
+function isValidGitHubEditUrl(url: string): boolean {
+ try {
+ const parsed = new URL(url);
+ return (
+ parsed.protocol === "https:" &&
+ parsed.hostname === "github.com" &&
+ parsed.pathname.includes("/edit/")
+ );
+ } catch {
+ return false;
+ }
+}
+
+// Convert edit → blob
+function buildSourceUrl(editUrl: string): string {
+ const url = new URL(editUrl);
+ url.search = "";
+ url.pathname = url.pathname.replace("/edit/", "/blob/");
+ return url.toString();
+}
+
+// Build GitHub issue link
+function buildIssueUrl(pageTitle: string, sourceUrl: string): string {
+ return `https://github.com/kubestellar/docs/issues/new?title=${encodeURIComponent(
+ `Docs: ${pageTitle}`
+ )}&body=${encodeURIComponent(`Source file:\n${sourceUrl}`)}`;
+}
+
+type DocsSourceActionsProps = {
+ filePath: string;
+ projectId: ProjectId;
+ pageTitle: string;
+};
+
+export function DocsSourceActions({
+ filePath,
+ projectId,
+ pageTitle,
+}: DocsSourceActionsProps) {
+ const { config } = useSharedConfig();
+
+ const editUrl = buildGitHubEditUrl(
+ filePath,
+ projectId,
+ config?.editBaseUrls
+ );
+
+ if (!editUrl || !isValidGitHubEditUrl(editUrl)) return null;
+
+ const safeEditUrl = new URL(editUrl).href;
+ const sourceUrl = buildSourceUrl(safeEditUrl);
+ const issueUrl = buildIssueUrl(pageTitle, sourceUrl);
+
+ return (
+
+
Compose a PR
+
View Source
+
Open Issue
+
+ );
+}
+
+function ActionLink({
+ href,
+ children,
+}: {
+ href: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/docs/EditPageLink.tsx b/src/components/docs/EditPageLink.tsx
index e7c58e3d..100e029d 100644
--- a/src/components/docs/EditPageLink.tsx
+++ b/src/components/docs/EditPageLink.tsx
@@ -1,101 +1,23 @@
"use client";
-import { useState, useEffect } from 'react';
-import { useSharedConfig, getVersionsForProject, VersionInfo } from '@/hooks/useSharedConfig';
-import { getProjectVersions as getStaticProjectVersions } from '@/config/versions';
+import { useSharedConfig } from '@/hooks/useSharedConfig';
import type { ProjectId } from '@/config/versions';
+const STATIC_EDIT_BASE_URLS: Record = {
+ kubestellar: 'https://github.com/kubestellar/docs/edit/main/docs/content',
+ a2a: 'https://github.com/kubestellar/a2a/edit/main/docs',
+ kubeflex: 'https://github.com/kubestellar/kubeflex/edit/main/docs',
+ 'multi-plugin': 'https://github.com/kubestellar/kubectl-multi-plugin/edit/main/docs',
+ klaude: 'https://github.com/kubestellar/klaude/edit/main/docs',
+ console: 'https://github.com/kubestellar/console/edit/main/docs',
+};
+
interface EditPageLinkProps {
filePath: string;
projectId: ProjectId;
variant?: 'full' | 'icon';
}
-// Version entry with key from the versions config
-type VersionEntry = { key: string } & VersionInfo;
-
-// Convert branch name to Netlify slug format (e.g., docs/0.29.0 -> docs-0-29-0)
-function branchToSlug(branch: string): string {
- return branch.replace(/\//g, '-').replace(/\./g, '-');
-}
-
-// Detect current branch from hostname (for kubestellar docs repo)
-function detectCurrentBranch(versions: VersionEntry[]): string {
- if (typeof window === 'undefined') return 'main';
-
- const hostname = window.location.hostname;
-
- // Production site uses the "latest" version's branch
- if (hostname === 'kubestellar.io' || hostname === 'www.kubestellar.io') {
- const latestVersion = versions.find(v => v.key === 'latest');
- return latestVersion?.branch || 'main';
- }
-
- // Netlify branch deploys: {branch-slug}--{site-name}.netlify.app
- const branchDeployMatch = hostname.match(/^(.+)--[\w-]+\.netlify\.app$/);
- if (branchDeployMatch) {
- const branchSlug = branchDeployMatch[1];
-
- // Main branch deploy
- if (branchSlug === 'main') {
- return 'main';
- }
-
- // Deploy previews go to main
- if (branchSlug.startsWith('deploy-preview-')) {
- return 'main';
- }
-
- // Match branch slug to version branch (e.g., docs-0-29-0 -> docs/0.29.0)
- for (const version of versions) {
- if (branchSlug === branchToSlug(version.branch)) {
- return version.branch;
- }
- }
- }
-
- return 'main';
-}
-
-// Source repos for each project (used when on main branch)
-// Projects not listed here have their docs in the docs repo itself
-const SOURCE_REPOS: Record = {
- a2a: { repo: 'kubestellar/a2a', docsPath: 'docs' },
- kubeflex: { repo: 'kubestellar/kubeflex', docsPath: 'docs' },
- 'multi-plugin': { repo: 'kubestellar/kubectl-multi-plugin', docsPath: 'docs' },
- 'klaude': { repo: 'kubestellar/klaude', docsPath: 'docs' },
-};
-
-// Projects whose docs live in the docs repo itself (not a separate source repo)
-const DOCS_REPO_PROJECTS = ['console'];
-
-// Build edit URL for a project, using correct branch
-function buildEditBaseUrl(projectId: ProjectId, branch: string): string {
- // KubeStellar docs always live in docs repo
- if (projectId === 'kubestellar') {
- return `https://github.com/kubestellar/docs/edit/${branch}/docs/content`;
- }
-
- // Projects whose docs live in the docs repo itself (e.g., console)
- if (DOCS_REPO_PROJECTS.includes(projectId)) {
- return `https://github.com/kubestellar/docs/edit/${branch}/docs/content/${projectId}`;
- }
-
- // For other projects: version branches are in docs repo, main goes to source repo
- if (branch !== 'main' && branch.startsWith('docs/')) {
- // Version branch in docs repo (e.g., docs/klaude/0.6.0)
- return `https://github.com/kubestellar/docs/edit/${branch}/docs/content/${projectId}`;
- }
-
- // Main branch - link to source repo
- const source = SOURCE_REPOS[projectId];
- if (source) {
- return `https://github.com/${source.repo}/edit/main/${source.docsPath}`;
- }
-
- return '';
-}
-
// Validate that URL is a safe GitHub edit URL to prevent XSS
function isValidGitHubEditUrl(url: string): boolean {
try {
@@ -111,31 +33,32 @@ function isValidGitHubEditUrl(url: string): boolean {
}
}
-export function EditPageLink({ filePath, projectId, variant = 'full' }: EditPageLinkProps) {
- const { config } = useSharedConfig();
- const [currentBranch, setCurrentBranch] = useState('main');
+//refactoring helper function to build the GitHub edit URL
+export function buildGitHubEditUrl(
+ filePath: string,
+ projectId: ProjectId,
+ editBaseUrls?: Record
+): string | null {
+ const baseUrl =
+ editBaseUrls?.[projectId] ?? STATIC_EDIT_BASE_URLS[projectId];
- // Get versions to detect current branch
- const versions = config
- ? getVersionsForProject(config, projectId)
- : getStaticProjectVersions(projectId);
+ if (!baseUrl) return null;
- // Detect current branch from hostname on client-side mount
- useEffect(() => {
- const detected = detectCurrentBranch(versions);
- setCurrentBranch(detected);
- }, [versions]);
+ const sanitizedFilePath = filePath.replace(/\.\./g, "").replace(/^\/+/, "");
+ return `${baseUrl}/${sanitizedFilePath}`;
+}
- // Build edit URL with correct branch
- const editBaseUrl = buildEditBaseUrl(projectId, currentBranch);
+export function EditPageLink({ filePath, projectId, variant = 'full' }: EditPageLinkProps) {
+ const { config } = useSharedConfig();
- if (!editBaseUrl) return null;
+ const editUrl = buildGitHubEditUrl(
+ filePath,
+ projectId,
+ config?.editBaseUrls
+ );
- // Sanitize filePath to prevent path traversal
- const sanitizedFilePath = filePath.replace(/\.\./g, '').replace(/^\/+/, '');
+ if (!editUrl) return null;
- // Construct the full edit URL
- const editUrl = `${editBaseUrl}/${sanitizedFilePath}`;
// Validate URL before rendering to prevent XSS
if (!isValidGitHubEditUrl(editUrl)) return null;
diff --git a/src/components/master-page/AboutSection.tsx b/src/components/master-page/AboutSection.tsx
index d017fe2d..b259e553 100644
--- a/src/components/master-page/AboutSection.tsx
+++ b/src/components/master-page/AboutSection.tsx
@@ -3,6 +3,7 @@
import { useEffect } from "react";
import { GridLines, StarField } from "../index";
import { useTranslations } from "next-intl";
+import { DocsSourceActions } from "@/components/docs/DocsSourceActions";
import Link from "next/link";
export default function AboutSection() {
@@ -127,7 +128,7 @@ export default function AboutSection() {
-
+
{/* Icon with animation */}
+
+
+
@@ -183,7 +191,7 @@ export default function AboutSection() {
-
+
{/* Icon with animation */}
+
+
+
@@ -239,7 +254,7 @@ export default function AboutSection() {
-
+
{/* Icon with animation */}
+
+
+
@@ -294,4 +316,4 @@ export default function AboutSection() {
);
-}
+}
\ No newline at end of file