mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-20 10:00:58 +00:00
docs: vendor the documentation site into the monorepo
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to docs.sanaei.dev) into docs/ so the panel and its documentation share a single source of truth, the way sing-box keeps its docs in-tree. The old repo becomes redundant and can be retired. - Import the full site under docs/ (app, components, content, lib, public, scripts, config). The self-contained pnpm project sits alongside the existing engineering notes with no filename collisions. - Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx). - Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to docs/** and run with working-directory: docs, since GitHub only runs workflows from the repo-root .github/. deploy-static.yml's GitHub Pages publish (CNAME docs.sanaei.dev) carries over unchanged. Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to this repository's Pages (or set the Vercel project's root directory to docs), confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { HomeLanguageSwitcher } from '@/components/home/language-switcher';
|
||||
|
||||
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
const options = baseOptions(lang);
|
||||
return (
|
||||
<HomeLayout
|
||||
{...options}
|
||||
// Disable fumadocs' built-in popover language switcher here: nested in the
|
||||
// home navbar's Radix NavigationMenu its item clicks don't fire. We inject
|
||||
// an anchor-based one instead (see HomeLanguageSwitcher). Docs keep the
|
||||
// built-in switcher (its sidebar isn't a NavigationMenu, so it works).
|
||||
i18n={false}
|
||||
links={[
|
||||
...(options.links ?? []),
|
||||
{
|
||||
type: 'custom',
|
||||
secondary: true,
|
||||
children: <HomeLanguageSwitcher current={lang} />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight, BookOpen, Heart } from 'lucide-react';
|
||||
import { GitHubIcon, TelegramIcon } from '@/components/icons';
|
||||
import { Logo } from '@/components/logo';
|
||||
import { Features } from '@/components/home/features';
|
||||
import { GitHubStatsRow } from '@/components/home/github-stats';
|
||||
import { InstallCommand } from '@/components/home/install-command';
|
||||
import { getGitHubStats } from '@/lib/github-stats';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { appName, productRepoUrl, deepWikiUrl, telegramChannelUrl, donateUrl } from '@/lib/shared';
|
||||
import { getSiteMessages, type SiteMessages } from '@/lib/site-i18n';
|
||||
|
||||
export function generateStaticParams() {
|
||||
return i18n.languages.map((lang) => ({ lang }));
|
||||
}
|
||||
|
||||
const INSTALL_COMMAND =
|
||||
'bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)';
|
||||
|
||||
export default async function HomePage({ params }: PageProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
const prefix = lang === 'en' ? '' : `/${lang}`;
|
||||
const m = getSiteMessages(lang);
|
||||
const stats = await getGitHubStats();
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 flex-col">
|
||||
{/* Hero */}
|
||||
<section className="relative overflow-hidden border-b">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 -top-40 h-80 bg-gradient-to-b from-brand/15 to-transparent blur-3xl"
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col items-center px-4 py-20 text-center sm:py-28">
|
||||
<Logo className="h-20 drop-shadow-sm" />
|
||||
<h1 className="mt-6 text-4xl font-bold tracking-tight sm:text-6xl">
|
||||
<span className="bg-gradient-to-r from-cyan-500 to-sky-600 bg-clip-text text-transparent dark:from-cyan-300 dark:to-sky-400">
|
||||
{appName}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-lg text-fd-muted-foreground sm:text-xl">{m.tagline}</p>
|
||||
|
||||
<div className="mt-8 flex flex-col items-center gap-3 sm:flex-row">
|
||||
<Link
|
||||
href={`${prefix}/docs`}
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-fd-primary px-5 py-3 font-medium text-fd-primary-foreground transition-opacity hover:opacity-90"
|
||||
>
|
||||
{m.getStarted}
|
||||
<ArrowRight className="size-4 rtl:rotate-180" aria-hidden />
|
||||
</Link>
|
||||
<a
|
||||
href={productRepoUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="inline-flex items-center gap-2 rounded-xl border px-5 py-3 font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
|
||||
>
|
||||
<GitHubIcon className="size-4" />
|
||||
{m.viewOnGitHub}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<InstallCommand
|
||||
command={INSTALL_COMMAND}
|
||||
copyLabel={m.copyCommand}
|
||||
copiedLabel={m.copied}
|
||||
className="mt-8 w-full max-w-2xl"
|
||||
/>
|
||||
|
||||
{/* Build-time stats as the initial render; refreshed live on the client. */}
|
||||
<GitHubStatsRow
|
||||
initial={stats}
|
||||
labels={{ stars: m.stars, forks: m.forks, latest: m.latest }}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Features heading={m.featuresHeading} subtitle={m.featuresSubtitle} items={m.features} />
|
||||
|
||||
<Footer prefix={prefix} m={m} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function Footer({ prefix, m }: { prefix: string; m: SiteMessages }) {
|
||||
return (
|
||||
<footer className="border-t">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col items-center justify-between gap-4 px-4 py-8 text-sm text-fd-muted-foreground sm:flex-row">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Logo className="h-6" />
|
||||
<span>
|
||||
{appName} — {m.licenseBefore}
|
||||
<a
|
||||
href={`${productRepoUrl}/blob/main/LICENSE`}
|
||||
className="underline hover:text-fd-foreground"
|
||||
>
|
||||
GPL-3.0
|
||||
</a>
|
||||
{m.licenseAfter}
|
||||
</span>
|
||||
</div>
|
||||
<nav className="flex items-center gap-4">
|
||||
<Link href={`${prefix}/docs`} className="hover:text-fd-foreground">
|
||||
{m.docs}
|
||||
</Link>
|
||||
<a
|
||||
href={productRepoUrl}
|
||||
className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
|
||||
>
|
||||
<GitHubIcon className="size-4" />
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href={deepWikiUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
|
||||
>
|
||||
<BookOpen className="size-4" />
|
||||
DeepWiki
|
||||
</a>
|
||||
<a
|
||||
href={telegramChannelUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
|
||||
>
|
||||
<TelegramIcon className="size-4" />
|
||||
Telegram
|
||||
</a>
|
||||
<a
|
||||
href={donateUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
|
||||
>
|
||||
<Heart className="size-4" />
|
||||
{m.donate}
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
|
||||
import {
|
||||
DocsBody,
|
||||
DocsDescription,
|
||||
DocsPage,
|
||||
DocsTitle,
|
||||
MarkdownCopyButton,
|
||||
ViewOptionsPopover,
|
||||
} from 'fumadocs-ui/layouts/docs/page';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { getMDXComponents } from '@/components/mdx';
|
||||
import { OpenAPIPage as BaseOpenAPIPage } from '@/components/openapi-page';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import type { Metadata } from 'next';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
import { gitConfig } from '@/lib/shared';
|
||||
|
||||
export default async function Page(props: PageProps<'/[lang]/docs/[[...slug]]'>) {
|
||||
const { lang, slug } = await props.params;
|
||||
const page = source.getPage(slug, lang);
|
||||
if (!page) notFound();
|
||||
|
||||
const MDX = page.data.body;
|
||||
const markdownUrl = getPageMarkdownUrl(page).url;
|
||||
const editUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/${gitConfig.docsDir}/${page.path}`;
|
||||
|
||||
// Generated API reference pages carry `_openapi` metadata. Preload the spec
|
||||
// on the server (highlighting included) so the client OpenAPIPage doesn't have
|
||||
// to load it at render time.
|
||||
const isOpenAPI = Boolean((page.data as { _openapi?: unknown })._openapi);
|
||||
const extraComponents: Record<string, unknown> = {};
|
||||
if (isOpenAPI) {
|
||||
const preloaded = await openapi.preloadOpenAPIPage(page);
|
||||
function PreloadedOpenAPIPage(p: ComponentProps<typeof BaseOpenAPIPage>) {
|
||||
return <BaseOpenAPIPage {...p} {...preloaded} />;
|
||||
}
|
||||
extraComponents.OpenAPIPage = PreloadedOpenAPIPage;
|
||||
}
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
|
||||
<div className="flex flex-row items-center gap-2 border-b pb-6">
|
||||
<MarkdownCopyButton markdownUrl={markdownUrl} />
|
||||
<ViewOptionsPopover markdownUrl={markdownUrl} githubUrl={editUrl} />
|
||||
</div>
|
||||
<DocsBody>
|
||||
<MDX
|
||||
components={getMDXComponents({
|
||||
// allows linking to other pages with relative file paths
|
||||
a: createRelativeLink(source, page),
|
||||
...extraComponents,
|
||||
})}
|
||||
/>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return source.generateParams();
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
props: PageProps<'/[lang]/docs/[[...slug]]'>,
|
||||
): Promise<Metadata> {
|
||||
const { lang, slug } = await props.params;
|
||||
const page = source.getPage(slug, lang);
|
||||
if (!page) notFound();
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
openGraph: {
|
||||
images: getPageImage(page).url,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default async function Layout({ params, children }: LayoutProps<'/[lang]/docs'>) {
|
||||
const { lang } = await params;
|
||||
return (
|
||||
<DocsLayout tree={source.getPageTree(lang)} {...baseOptions(lang)}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import '../global.css';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import { Inter, Vazirmatn } from 'next/font/google';
|
||||
import { i18n, localeDirection } from '@/lib/i18n';
|
||||
import { provider } from '@/lib/i18n-ui';
|
||||
import SearchDialog from '@/components/search-dialog';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'], display: 'swap' });
|
||||
// Persian UI font; covers Arabic + Latin glyphs so mixed content renders well.
|
||||
const vazirmatn = Vazirmatn({ subsets: ['arabic'], display: 'swap' });
|
||||
|
||||
export function generateStaticParams() {
|
||||
return i18n.languages.map((lang) => ({ lang }));
|
||||
}
|
||||
|
||||
export default async function LangLayout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
const dir = localeDirection(lang);
|
||||
const fontClassName = lang === 'fa' ? vazirmatn.className : inter.className;
|
||||
|
||||
return (
|
||||
<html lang={lang} dir={dir} className={fontClassName} suppressHydrationWarning>
|
||||
<body className="flex min-h-screen flex-col" suppressHydrationWarning>
|
||||
<RootProvider i18n={provider(lang)} search={{ SearchDialog }}>
|
||||
{children}
|
||||
</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user