mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-15 14:28:21 +00:00
complete i18n migration to /[locale]/ with EN+ES content
Full rewrite of the docs site under app/[locale]/ with next-intl in localePrefix:"always" mode. Every page now exists at both /en/<path> and /es/<path>; the root / shows a meta-refresh + JS redirect to /<defaultLocale>/ so GitHub Pages serves something on the apex URL. Highlights: - 107 doc pages migrated to file-per-page JSON namespaces under messages/en/ and messages/es/. Spanish content is fully translated (no copy-of-English placeholders). - New documentation for the Active Suppressions section in the Settings tab and the per-event Dismiss dropdown in the Health Monitor modal. - New screenshots: dismiss-duration-dropdown.png and an updated health-suppression-settings.png. - Pagefind integrated for client-side search; index is built on every CI deploy (not committed). - RSS feeds: per-locale at /<locale>/rss.xml plus root /rss.xml for backward compat. - Removed the dead app/[locale]/guides/[slug]/ route — every guide now has its own static page and no markdown source remains. - Fixed orphan link /guides/nvidia -> /guides/nvidia-manual in docs/hardware/nvidia-host. - Removed obsolete components (footer2, calendar, drawer). Verified locally with `npm ci && npm run build`: 2804 files in out/, 231 pages indexed by pagefind, root redirect intact, both locale roots and the new Active Suppressions docs render OK.
This commit is contained in:
263
web/app/[locale]/guides/backup-cloud/page.tsx
Normal file
263
web/app/[locale]/guides/backup-cloud/page.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import type { Metadata } from "next"
|
||||
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
|
||||
import { ExternalLink } from "lucide-react"
|
||||
import { DocHeader } from "@/components/ui/doc-header"
|
||||
import { Callout } from "@/components/ui/callout"
|
||||
import Image from "next/image"
|
||||
import CopyableCode from "@/components/CopyableCode"
|
||||
import Footer from "@/components/footer"
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params
|
||||
const t = await getTranslations({ locale, namespace: "guides.backupCloud.meta" })
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
alternates: { canonical: "https://proxmenux.com/docs/guides/backup-cloud" },
|
||||
openGraph: {
|
||||
title: t("ogTitle"),
|
||||
description: t("ogDescription"),
|
||||
type: "article",
|
||||
url: "https://proxmenux.com/docs/guides/backup-cloud",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function BackupCloudGuide({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
const t = await getTranslations({ locale, namespace: "guides.backupCloud" })
|
||||
|
||||
const messages = (await getMessages({ locale })) as unknown as {
|
||||
guides: { backupCloud: {
|
||||
intro: { steps: string[] }
|
||||
createDir: { configItems: string[] }
|
||||
mount: { mountItems: string[] }
|
||||
retention: { starterItems: string[] }
|
||||
troubleshoot: { items: string[] }
|
||||
} }
|
||||
}
|
||||
const introSteps = messages.guides.backupCloud.intro.steps
|
||||
const configItems = messages.guides.backupCloud.createDir.configItems
|
||||
const mountItems = messages.guides.backupCloud.mount.mountItems
|
||||
const starterItems = messages.guides.backupCloud.retention.starterItems
|
||||
const troubleItems = messages.guides.backupCloud.troubleshoot.items
|
||||
|
||||
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
|
||||
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
|
||||
|
||||
const pbsLink = (chunks: React.ReactNode) => (
|
||||
<a
|
||||
href="https://www.proxmox.com/proxmox-backup-server"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-700 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)
|
||||
const rcloneLink = (chunks: React.ReactNode) => (
|
||||
<a
|
||||
href="https://rclone.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-700 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)
|
||||
const remoteSetupLink = (chunks: React.ReactNode) => (
|
||||
<a
|
||||
href="https://rclone.org/remote_setup/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-700 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)
|
||||
const providerDocsLink = (chunks: React.ReactNode) => (
|
||||
<a
|
||||
href="https://rclone.org/docs/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-700 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-gray-900 pt-16 flex flex-col">
|
||||
<div className="container mx-auto px-4 pt-6 pb-16 flex-grow" style={{ maxWidth: "980px" }}>
|
||||
<DocHeader
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
section={t("header.section")}
|
||||
estimatedMinutes={15}
|
||||
/>
|
||||
|
||||
<Callout variant="info" title={t("intro.pbsCalloutTitle")}>
|
||||
{t.rich("intro.pbsCalloutBody", { strong, code, pbsLink })}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("intro.stepsTitle")}</h2>
|
||||
<ol className="list-decimal pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
|
||||
{introSteps.map((_, idx) => (
|
||||
<li key={idx}>{t(`intro.steps.${idx}`)}</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<Callout variant="warning" title={t("intro.vzdumpCalloutTitle")}>
|
||||
{t.rich("intro.vzdumpCalloutBody", { strong, code })}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("createDir.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("createDir.body", { strong, code })}</p>
|
||||
<CopyableCode code={t.raw("createDir.mkdirCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("createDir.afterMkdir", { strong, code })}</p>
|
||||
<Image
|
||||
src="/guides/backup_cloud/imagen1.png"
|
||||
alt={t("createDir.image1Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("createDir.configIntro")}</p>
|
||||
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
|
||||
{configItems.map((_, idx) => (
|
||||
<li key={idx}>{t.rich(`createDir.configItems.${idx}`, { strong, code })}</li>
|
||||
))}
|
||||
</ul>
|
||||
<Image
|
||||
src="/guides/backup_cloud/imagen2.png"
|
||||
alt={t("createDir.image2Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("createDir.afterConfig", { strong })}</p>
|
||||
<Image
|
||||
src="/guides/backup_cloud/imagen3.png"
|
||||
alt={t("createDir.image3Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("createDir.afterAdd")}</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("installRclone.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installRclone.body", { rcloneLink })}</p>
|
||||
<CopyableCode code={t.raw("installRclone.installCode") as string} className="my-4" />
|
||||
|
||||
<Callout variant="info" title={t("installRclone.newerCalloutTitle")}>
|
||||
<p className="mb-2">{t("installRclone.newerCalloutBody")}</p>
|
||||
<CopyableCode code={t.raw("installRclone.newerCode") as string} className="my-2" />
|
||||
</Callout>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("installRclone.tunnelHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installRclone.tunnelBody", { remoteSetupLink })}</p>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installRclone.tunnelFrom", { strong, code })}</p>
|
||||
<CopyableCode code={t.raw("installRclone.tunnelCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("installRclone.tunnelAfter")}</p>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("installRclone.runHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("installRclone.runBody")}</p>
|
||||
<CopyableCode code={t.raw("installRclone.runCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installRclone.runAfter", { providerDocsLink })}</p>
|
||||
<CopyableCode code={t.raw("installRclone.authPrompt") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installRclone.authAfter", { strong })}</p>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installRclone.nameRemote", { code })}</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("mount.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("mount.body", { code })}</p>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("mount.mountIntro")}</p>
|
||||
<CopyableCode code={t.raw("mount.mountCode") as string} className="my-4" />
|
||||
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
|
||||
{mountItems.map((_, idx) => (
|
||||
<li key={idx}>{t.rich(`mount.mountItems.${idx}`, { code })}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("mount.mountFootnote", { strong })}</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("systemd.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("systemd.body", { code })}</p>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("systemd.createIntro")}</p>
|
||||
<CopyableCode code={t.raw("systemd.createCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("systemd.pasteIntro")}</p>
|
||||
<CopyableCode code={t.raw("systemd.unitCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("systemd.adjust", { code })}</p>
|
||||
<CopyableCode code={t.raw("systemd.enableCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("systemd.verifyIntro")}</p>
|
||||
<CopyableCode code={t.raw("systemd.verifyCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("systemd.verifyAfter", { code })}</p>
|
||||
|
||||
<Callout variant="info" title={t("systemd.vfsCalloutTitle")}>
|
||||
{t.rich("systemd.vfsCalloutBody", { code })}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("configureBackup.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("configureBackup.body", { strong })}</p>
|
||||
<Image
|
||||
src="/guides/backup_cloud/imagen5.png"
|
||||
alt={t("configureBackup.image5Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("configureBackup.after")}</p>
|
||||
<Image
|
||||
src="/guides/backup_cloud/imagen6.png"
|
||||
alt={t("configureBackup.image6Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("retention.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("retention.body", { strong })}</p>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("retention.uiPath", { strong })}</p>
|
||||
<Image
|
||||
src="/guides/backup_cloud/imagen7.png"
|
||||
alt={t("retention.image7Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("retention.starterIntro")}</p>
|
||||
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
|
||||
{starterItems.map((_, idx) => (
|
||||
<li key={idx}>{t(`retention.starterItems.${idx}`)}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("retention.adjust")}</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("troubleshoot.heading")}</h2>
|
||||
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-2">
|
||||
{troubleItems.map((_, idx) => (
|
||||
<li key={idx}>{t.rich(`troubleshoot.items.${idx}`, { code, strong })}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
179
web/app/[locale]/guides/kodi-lxc/page.tsx
Normal file
179
web/app/[locale]/guides/kodi-lxc/page.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import type { Metadata } from "next"
|
||||
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
|
||||
import { ExternalLink } from "lucide-react"
|
||||
import { DocHeader } from "@/components/ui/doc-header"
|
||||
import { Callout } from "@/components/ui/callout"
|
||||
import Image from "next/image"
|
||||
import CopyableCode from "@/components/CopyableCode"
|
||||
import Footer from "@/components/footer"
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params
|
||||
const t = await getTranslations({ locale, namespace: "guides.kodiLxc.meta" })
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
alternates: { canonical: "https://proxmenux.com/docs/guides/kodi-lxc" },
|
||||
openGraph: {
|
||||
title: t("ogTitle"),
|
||||
description: t("ogDescription"),
|
||||
type: "article",
|
||||
url: "https://proxmenux.com/docs/guides/kodi-lxc",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function KodiLxcGuide({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
const t = await getTranslations({ locale, namespace: "guides.kodiLxc" })
|
||||
|
||||
const messages = (await getMessages({ locale })) as unknown as {
|
||||
guides: { kodiLxc: {
|
||||
intro: { steps: string[] }
|
||||
troubleshoot: { items: string[] }
|
||||
} }
|
||||
}
|
||||
const introSteps = messages.guides.kodiLxc.intro.steps
|
||||
const troubleItems = messages.guides.kodiLxc.troubleshoot.items
|
||||
|
||||
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
|
||||
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
|
||||
const gpuLink = (chunks: React.ReactNode) => (
|
||||
<a
|
||||
href="/docs/hardware/igpu-acceleration-lxc"
|
||||
className="text-blue-700 hover:underline"
|
||||
>
|
||||
{chunks}
|
||||
</a>
|
||||
)
|
||||
const authorLink = (chunks: React.ReactNode) => (
|
||||
<a
|
||||
href="https://github.com/mrrudy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-700 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)
|
||||
const konpatLink = (chunks: React.ReactNode) => (
|
||||
<a
|
||||
href="https://blog.konpat.me/dev/2019/03/11/setting-up-lxc-for-intel-gpu-proxmox.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-700 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-gray-900 pt-16 flex flex-col">
|
||||
<div className="container mx-auto px-4 pt-6 pb-16 flex-grow" style={{ maxWidth: "980px" }}>
|
||||
<DocHeader
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
section={t("header.section")}
|
||||
estimatedMinutes={10}
|
||||
/>
|
||||
|
||||
<Callout variant="info" title={t("intro.calloutTitle")}>
|
||||
{t.rich("intro.calloutBody", { strong, code, gpuLink })}
|
||||
</Callout>
|
||||
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">
|
||||
{t.rich("intro.credit", { authorLink })}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("intro.stepsTitle")}</h2>
|
||||
<ol className="list-decimal pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
|
||||
{introSteps.map((_, idx) => (
|
||||
<li key={idx}>{t(`intro.steps.${idx}`)}</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("createCt.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("createCt.body", { strong, code })}</p>
|
||||
<CopyableCode code={t.raw("createCt.code") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("createCt.after", { code })}</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("addInput.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("addInput.body", { code })}</p>
|
||||
<CopyableCode code={t.raw("addInput.listCode") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/kodi/kodi1.png"
|
||||
alt={t("addInput.imageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("addInput.afterList", { code, strong })}</p>
|
||||
<CopyableCode code={t.raw("addInput.editCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("addInput.addLines", { code, strong })}</p>
|
||||
<CopyableCode code={t.raw("addInput.configCode") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/kodi/kodi2.png"
|
||||
alt={t("addInput.imageConfigAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("addInput.save", { code, strong })}</p>
|
||||
<CopyableCode code={t.raw("addInput.restartCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("addInput.plug")}</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("updateKodi.heading")}</h2>
|
||||
<Callout variant="warning" title={t("updateKodi.calloutTitle")}>
|
||||
{t.rich("updateKodi.calloutBody", { strong, code })}
|
||||
</Callout>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("updateKodi.body", { code })}</p>
|
||||
<CopyableCode code={t.raw("updateKodi.code") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("updateKodi.after")}</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("screenshots.heading")}</h2>
|
||||
<Image
|
||||
src="/guides/kodi/kodi3.png"
|
||||
alt={t("screenshots.image1Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<Image
|
||||
src="/guides/kodi/kodi4.jpeg"
|
||||
alt={t("screenshots.image2Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("troubleshoot.heading")}</h2>
|
||||
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-2">
|
||||
{troubleItems.map((_, idx) => (
|
||||
<li key={idx}>{t.rich(`troubleshoot.items.${idx}`, { code, strong })}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("further.heading")}</h2>
|
||||
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
|
||||
<li>{t.rich("further.konpatRich", { konpatLink })}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
223
web/app/[locale]/guides/linux-resources/page.tsx
Normal file
223
web/app/[locale]/guides/linux-resources/page.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import type { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
import { BookOpen, ExternalLink, Shield, Activity, Database, FileCode, ArrowLeft } from "lucide-react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import Footer from "@/components/footer"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title:
|
||||
"Linux & Proxmox Learning Resources — Cheatsheets, Security, ZFS, Monitoring | ProxMenux",
|
||||
description:
|
||||
"Curated catalogue of external Linux and Proxmox VE learning resources: command-line cheatsheets (TLDR, Explainshell, Cheat.sh), security hardening references, monitoring tools and ZFS documentation. Complements the ProxMenux command catalog.",
|
||||
keywords: [
|
||||
"linux cheatsheet",
|
||||
"proxmox learning resources",
|
||||
"linux security hardening",
|
||||
"zfs documentation",
|
||||
"tldr pages",
|
||||
"explainshell",
|
||||
"linux command reference",
|
||||
"linux monitoring tools",
|
||||
"proxmox community resources",
|
||||
],
|
||||
alternates: { canonical: "https://proxmenux.com/guides/linux-resources" },
|
||||
openGraph: {
|
||||
title: "Linux & Proxmox Learning Resources",
|
||||
description:
|
||||
"Curated external Linux and Proxmox VE learning resources — cheatsheets, security, monitoring, ZFS — that complement the ProxMenux command catalog.",
|
||||
type: "article",
|
||||
url: "https://proxmenux.com/guides/linux-resources",
|
||||
siteName: "ProxMenux",
|
||||
images: [
|
||||
{
|
||||
url: "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/web/public/main.png",
|
||||
width: 1363,
|
||||
height: 735,
|
||||
alt: "Linux & Proxmox Learning Resources — ProxMenux",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
title: "Linux & Proxmox Learning Resources | ProxMenux",
|
||||
description:
|
||||
"Curated external resources — cheatsheets, security, monitoring, ZFS — that complement the ProxMenux command catalog.",
|
||||
},
|
||||
}
|
||||
|
||||
export default function LinuxResourcesPage() {
|
||||
const resourceCategories = [
|
||||
{
|
||||
title: "Linux General",
|
||||
icon: <BookOpen className="h-6 w-6 text-blue-500" />,
|
||||
resources: [
|
||||
{
|
||||
title: "TLDR Pages",
|
||||
url: "https://tldr.sh/",
|
||||
description:
|
||||
"Terminal commands explained briefly with practical examples. Ideal for quickly remembering how to use tar, find, rsync, etc.",
|
||||
},
|
||||
{
|
||||
title: "Explainshell",
|
||||
url: "https://explainshell.com/",
|
||||
description:
|
||||
"Enter a command and this site breaks it down explaining each part. Very useful for learning complex commands.",
|
||||
},
|
||||
{
|
||||
title: "Cheat.sh",
|
||||
url: "https://cheat.sh/",
|
||||
description:
|
||||
"Online service to quickly search commands from browser or terminal (curl cheat.sh/tar). Very powerful and practical.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Security and Administration",
|
||||
icon: <Shield className="h-6 w-6 text-blue-500" />,
|
||||
resources: [
|
||||
{
|
||||
title: "SSH Hardening Guide",
|
||||
url: "https://www.ssh.com/academy/ssh/security",
|
||||
description:
|
||||
"Advanced guide to secure SSH access. Covers ciphers, versions, authentication, and other recommended practices.",
|
||||
},
|
||||
{
|
||||
title: "Fail2ban Wiki (GitHub)",
|
||||
url: "https://github.com/fail2ban/fail2ban/wiki",
|
||||
description: "Official documentation and usage examples for Fail2ban, an essential tool for servers.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Monitoring and Diagnostics",
|
||||
icon: <Activity className="h-6 w-6 text-blue-500" />,
|
||||
resources: [
|
||||
{
|
||||
title: "nmon Performance Monitor",
|
||||
url: "http://nmon.sourceforge.net/pmwiki.php",
|
||||
description: "Advanced system monitoring tool, with documentation on its usage.",
|
||||
},
|
||||
{
|
||||
title: "htop Official",
|
||||
url: "https://htop.dev/",
|
||||
description: "Official page of htop, one of the most used tools for viewing processes and resource usage.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "ZFS and Storage",
|
||||
icon: <Database className="h-6 w-6 text-blue-500" />,
|
||||
resources: [
|
||||
{
|
||||
title: "OpenZFS Documentation",
|
||||
url: "https://openzfs.github.io/openzfs-docs/",
|
||||
description:
|
||||
"Official and modern guide on ZFS, ideal for administrators using Proxmox with this file system.",
|
||||
},
|
||||
{
|
||||
title: "ZFS Cheatsheet (DigitalOcean)",
|
||||
url: "https://www.digitalocean.com/community/tutorials/how-to-use-zfs-on-ubuntu-20-04",
|
||||
description: "Clear and simple explanation of basic ZFS usage in Linux.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Extra: General Cheatsheets",
|
||||
icon: <FileCode className="h-6 w-6 text-blue-500" />,
|
||||
resources: [
|
||||
{
|
||||
title: "OverAPI.com",
|
||||
url: "https://overapi.com/linux",
|
||||
description: "Collection of interactive cheatsheets on multiple technologies, including Linux commands.",
|
||||
},
|
||||
{
|
||||
title: "DevHints.io Linux",
|
||||
url: "https://devhints.io/bash",
|
||||
description:
|
||||
"Bash shortcuts and basic scripting, useful for automating tasks in Proxmox and other environments.",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-gray-900">
|
||||
<div className="container mx-auto px-4 py-16" style={{ maxWidth: "980px" }}>
|
||||
<div className="mb-8">
|
||||
<Link href="/guides" className="flex items-center text-blue-500 hover:text-blue-700 transition-colors mb-6">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Guides
|
||||
</Link>
|
||||
|
||||
<h1 className="text-4xl font-bold mb-4 text-black">Linux Resources</h1>
|
||||
|
||||
<p className="text-lg mb-8 text-gray-700">
|
||||
A collection of useful resources for learning Linux commands, security practices, monitoring tools, and
|
||||
more. These resources complement the commands available in ProxMenux and will help you deepen your knowledge
|
||||
of Linux system administration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-10 mb-16">
|
||||
{resourceCategories.map((category, index) => (
|
||||
<div key={index} className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
{category.icon}
|
||||
<h2 className="text-2xl font-bold text-black">{category.title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{category.resources.map((resource, resourceIndex) => (
|
||||
<ResourceCard key={resourceIndex} resource={resource} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ResourceProps {
|
||||
resource: {
|
||||
title: string
|
||||
url: string
|
||||
description: string
|
||||
}
|
||||
}
|
||||
|
||||
function ResourceCard({ resource }: ResourceProps) {
|
||||
return (
|
||||
<Card className="transition-all duration-300 hover:shadow-md hover:border-blue-300 bg-white text-black border-2 border-gray-200">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-xl text-black flex items-center justify-between">
|
||||
{resource.title}
|
||||
<a
|
||||
href={resource.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
aria-label={`Visit ${resource.title} (opens in a new window)`}
|
||||
>
|
||||
<ExternalLink className="h-5 w-5" />
|
||||
</a>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-base text-gray-600">{resource.description}</CardDescription>
|
||||
<div className="mt-4">
|
||||
<a
|
||||
href={resource.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-500 hover:text-blue-700 flex items-center"
|
||||
>
|
||||
Visit resource <ExternalLink className="ml-1 h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
217
web/app/[locale]/guides/lxc-samba/page.tsx
Normal file
217
web/app/[locale]/guides/lxc-samba/page.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import type { Metadata } from "next"
|
||||
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
|
||||
import { DocHeader } from "@/components/ui/doc-header"
|
||||
import { Callout } from "@/components/ui/callout"
|
||||
import Image from "next/image"
|
||||
import CopyableCode from "@/components/CopyableCode"
|
||||
import Footer from "@/components/footer"
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params
|
||||
const t = await getTranslations({ locale, namespace: "guides.lxcSamba.meta" })
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
alternates: { canonical: "https://proxmenux.com/docs/guides/lxc-samba" },
|
||||
openGraph: {
|
||||
title: t("ogTitle"),
|
||||
description: t("ogDescription"),
|
||||
type: "article",
|
||||
url: "https://proxmenux.com/docs/guides/lxc-samba",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function LxcSambaGuide({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
const t = await getTranslations({ locale, namespace: "guides.lxcSamba" })
|
||||
|
||||
const messages = (await getMessages({ locale })) as unknown as {
|
||||
guides: { lxcSamba: {
|
||||
recommended: { items: string[] }
|
||||
intro: { steps: string[]; useCases: string[] }
|
||||
troubleshoot: { items: string[] }
|
||||
} }
|
||||
}
|
||||
const recommendedItems = messages.guides.lxcSamba.recommended.items
|
||||
const introSteps = messages.guides.lxcSamba.intro.steps
|
||||
const useCases = messages.guides.lxcSamba.intro.useCases
|
||||
const troubleItems = messages.guides.lxcSamba.troubleshoot.items
|
||||
|
||||
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
|
||||
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-gray-900 pt-16 flex flex-col">
|
||||
<div className="container mx-auto px-4 pt-6 pb-16 flex-grow" style={{ maxWidth: "980px" }}>
|
||||
<DocHeader
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
section={t("header.section")}
|
||||
estimatedMinutes={20}
|
||||
/>
|
||||
|
||||
<Callout variant="warning" title={t("recommended.calloutTitle")}>
|
||||
<p className="mb-2">{t("recommended.calloutIntro")}</p>
|
||||
<ul className="list-disc pl-6 mb-3 space-y-1">
|
||||
{recommendedItems.map((_, idx) => (
|
||||
<li key={idx}>{t.rich(`recommended.items.${idx}`, { strong, code })}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>{t("recommended.calloutOutro")}</p>
|
||||
</Callout>
|
||||
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("intro.body")}</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("intro.stepsTitle")}</h2>
|
||||
<ol className="list-decimal pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
|
||||
{introSteps.map((_, idx) => (
|
||||
<li key={idx}>{t(`intro.steps.${idx}`)}</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("intro.useCasesTitle")}</h2>
|
||||
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
|
||||
{useCases.map((_, idx) => (
|
||||
<li key={idx}>{t(`intro.useCases.${idx}`)}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Callout variant="warning" title={t("intro.privilegedCalloutTitle")}>
|
||||
{t.rich("intro.privilegedCalloutBody", { strong, code })}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("attach.heading")}</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("attach.identifyHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("attach.identifyBody", { strong })}</p>
|
||||
<p className="mb-2 text-gray-800 leading-relaxed">{t("attach.beforeLabel")}</p>
|
||||
<Image
|
||||
src="/guides/lxc_samba/lxc_3.png"
|
||||
alt={t("attach.imageBeforeAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-2 text-gray-800 leading-relaxed">{t("attach.afterLabel")}</p>
|
||||
<Image
|
||||
src="/guides/lxc_samba/lxc_4.png"
|
||||
alt={t("attach.imageAfterAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("attach.lsblkBody", { code })}</p>
|
||||
|
||||
<Callout variant="warning" title={t("attach.stableCalloutTitle")}>
|
||||
<p className="mb-2">{t.rich("attach.stableCalloutBody", { code })}</p>
|
||||
<CopyableCode code={t.raw("attach.stableCalloutCode") as string} className="my-3" />
|
||||
<p>{t.rich("attach.stableCalloutAfter", { code })}</p>
|
||||
</Callout>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("attach.formatHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("attach.formatBody", { strong })}</p>
|
||||
<CopyableCode code={t.raw("attach.formatCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("attach.formatAfter", { code })}</p>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("attach.mkdirHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("attach.mkdirBody")}</p>
|
||||
<CopyableCode code={t.raw("attach.mkdirCode") as string} className="my-4" />
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("attach.wireHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("attach.wireBody", { strong, code })}</p>
|
||||
<CopyableCode code={t.raw("attach.wireEditCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("attach.wireAddLine")}</p>
|
||||
<CopyableCode code={t.raw("attach.wireConfigCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("attach.wireShortForm", { code })}</p>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("attach.wireBackupNote", { code })}</p>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("attach.restartHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("attach.restartBody")}</p>
|
||||
<CopyableCode code={t.raw("attach.restartCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("attach.permsBody")}</p>
|
||||
<CopyableCode code={t.raw("attach.permsCode") as string} className="my-4" />
|
||||
<Callout variant="info" title={t("attach.permsNoteTitle")}>
|
||||
{t.rich("attach.permsNote", { code })}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("samba.heading")}</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("samba.installHeading")}</h3>
|
||||
<CopyableCode code={t.raw("samba.installCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("samba.confirmBody")}</p>
|
||||
<CopyableCode code={t.raw("samba.confirmCode") as string} className="my-4" />
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("samba.userHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("samba.userBody", { code })}</p>
|
||||
<CopyableCode code={t.raw("samba.userCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("samba.passwordBody")}</p>
|
||||
<CopyableCode code={t.raw("samba.passwordCode") as string} className="my-4" />
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("samba.aclHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("samba.aclBody", { code })}</p>
|
||||
<CopyableCode code={t.raw("samba.aclCode") as string} className="my-4" />
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("configure.heading")}</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("configure.editHeading")}</h3>
|
||||
<CopyableCode code={t.raw("configure.editCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("configure.appendBody")}</p>
|
||||
<CopyableCode code={t.raw("configure.shareCode") as string} className="my-4" />
|
||||
<Callout variant="info" title={t("configure.validUsersNoteTitle")}>
|
||||
{t.rich("configure.validUsersNote", { code })}
|
||||
</Callout>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("configure.reloadHeading")}</h3>
|
||||
<CopyableCode code={t.raw("configure.reloadCode") as string} className="my-4" />
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("verify.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("verify.body", { code })}</p>
|
||||
<Image
|
||||
src="/guides/lxc_samba/lxc_1.png"
|
||||
alt={t("verify.image1Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<Image
|
||||
src="/guides/lxc_samba/lxc_2.png"
|
||||
alt={t("verify.image2Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("verify.usageBody")}</p>
|
||||
<Image
|
||||
src="/guides/lxc_samba/lxc_5.png"
|
||||
alt={t("verify.image3Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("troubleshoot.heading")}</h2>
|
||||
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-2">
|
||||
{troubleItems.map((_, idx) => (
|
||||
<li key={idx}>{t.rich(`troubleshoot.items.${idx}`, { code, strong })}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
328
web/app/[locale]/guides/nvidia-manual/page.tsx
Normal file
328
web/app/[locale]/guides/nvidia-manual/page.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
import type { Metadata } from "next"
|
||||
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
|
||||
import { ExternalLink } from "lucide-react"
|
||||
import { DocHeader } from "@/components/ui/doc-header"
|
||||
import { Callout } from "@/components/ui/callout"
|
||||
import Image from "next/image"
|
||||
import CopyableCode from "@/components/CopyableCode"
|
||||
import Footer from "@/components/footer"
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params
|
||||
const t = await getTranslations({ locale, namespace: "guides.nvidiaManual.meta" })
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
alternates: { canonical: "https://proxmenux.com/docs/guides/nvidia-manual" },
|
||||
openGraph: {
|
||||
title: t("ogTitle"),
|
||||
description: t("ogDescription"),
|
||||
type: "article",
|
||||
url: "https://proxmenux.com/docs/guides/nvidia-manual",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function NvidiaManualGuide({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
const t = await getTranslations({ locale, namespace: "guides.nvidiaManual" })
|
||||
|
||||
const messages = (await getMessages({ locale })) as unknown as {
|
||||
guides: { nvidiaManual: {
|
||||
intro: { steps: string[] }
|
||||
lxcSetup: { tableRows: { device: string; major: string }[] }
|
||||
troubleshoot: { items: string[] }
|
||||
} }
|
||||
}
|
||||
const introSteps = messages.guides.nvidiaManual.intro.steps
|
||||
const tableRows = messages.guides.nvidiaManual.lxcSetup.tableRows
|
||||
const troubleItems = messages.guides.nvidiaManual.troubleshoot.items
|
||||
|
||||
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
|
||||
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
|
||||
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
|
||||
const patchLink = (chunks: React.ReactNode) => (
|
||||
<a
|
||||
href="https://github.com/keylase/nvidia-patch"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-700 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-gray-900 pt-16 flex flex-col">
|
||||
<div className="container mx-auto px-4 pt-6 pb-16 flex-grow" style={{ maxWidth: "980px" }}>
|
||||
<DocHeader
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
section={t("header.section")}
|
||||
estimatedMinutes={30}
|
||||
/>
|
||||
|
||||
<Callout variant="info" title={t("intro.calloutTitle")}>
|
||||
{t.rich("intro.calloutBody", { strong, em })}
|
||||
</Callout>
|
||||
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">
|
||||
{t.rich("intro.targetNote", { strong })}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("intro.stepsTitle")}</h2>
|
||||
<ol className="list-decimal pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
|
||||
{introSteps.map((_, idx) => (
|
||||
<li key={idx}>{t(`intro.steps.${idx}`)}</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{/* Section 1 - Prepare host */}
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("prepareHost.heading")}</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("prepareHost.blacklistHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("prepareHost.blacklistBody", { code })}</p>
|
||||
<CopyableCode code={t.raw("prepareHost.blacklistCheckCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("prepareHost.blacklistAdd", { code })}</p>
|
||||
<CopyableCode code={t.raw("prepareHost.blacklistAddCode") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-2.png"
|
||||
alt={t("prepareHost.blacklistImageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("prepareHost.reposHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("prepareHost.reposBody")}</p>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("prepareHost.reposOtherwise")}</p>
|
||||
<CopyableCode code={t.raw("prepareHost.reposEditCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("prepareHost.reposPveBody")}</p>
|
||||
<CopyableCode code={t.raw("prepareHost.reposPveCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("prepareHost.reposDebianBody", { code })}</p>
|
||||
<CopyableCode code={t.raw("prepareHost.reposDebianCode") as string} className="my-4" />
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("prepareHost.updateHeading")}</h3>
|
||||
<CopyableCode code={t.raw("prepareHost.updateCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("prepareHost.buildToolsBody")}</p>
|
||||
<CopyableCode code={t.raw("prepareHost.buildToolsCode") as string} className="my-4" />
|
||||
|
||||
{/* Section 2 - Install driver */}
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("installDriver.heading")}</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("installDriver.pickHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("installDriver.pickBody")}</p>
|
||||
<CopyableCode code={t.raw("installDriver.pickUrlCode") as string} className="my-4" />
|
||||
<Callout variant="info" title={t("installDriver.nvencCalloutTitle")}>
|
||||
{t.rich("installDriver.nvencCallout", { patchLink })}
|
||||
</Callout>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installDriver.pickReplace", { code })}</p>
|
||||
<CopyableCode code={t.raw("installDriver.pickListCode") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-1.png"
|
||||
alt={t("installDriver.pickImageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installDriver.pickVersionNote", { code })}</p>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("installDriver.downloadHeading")}</h3>
|
||||
<CopyableCode code={t.raw("installDriver.downloadCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installDriver.firstPassBody", { code })}</p>
|
||||
<CopyableCode code={t.raw("installDriver.firstPassCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("installDriver.secondPassBody")}</p>
|
||||
<CopyableCode code={t.raw("installDriver.secondPassCode") as string} className="my-4" />
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("installDriver.modulesHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("installDriver.modulesBody")}</p>
|
||||
<CopyableCode code={t.raw("installDriver.modulesEditCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("installDriver.modulesAddBody")}</p>
|
||||
<CopyableCode code={t.raw("installDriver.modulesAddCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installDriver.modulesSaveBody", { code })}</p>
|
||||
<CopyableCode code={t.raw("installDriver.modulesSaveCode") as string} className="my-4" />
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("installDriver.udevHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installDriver.udevBody", { code })}</p>
|
||||
<CopyableCode code={t.raw("installDriver.udevEditCode") as string} className="my-4" />
|
||||
<CopyableCode code={t.raw("installDriver.udevRulesCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("installDriver.udevSaveBody", { code })}</p>
|
||||
|
||||
{/* Section 3 - Persistence */}
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("persistence.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("persistence.body")}</p>
|
||||
<CopyableCode code={t.raw("persistence.installCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("persistence.verifyBody")}</p>
|
||||
<CopyableCode code={t.raw("persistence.verifySmiCode") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-3.png"
|
||||
alt={t("persistence.smiImageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<CopyableCode code={t.raw("persistence.verifyServiceCode") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-4.png"
|
||||
alt={t("persistence.serviceImageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
{/* Section 4 - NVENC */}
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("nvenc.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("nvenc.body")}</p>
|
||||
<CopyableCode code={t.raw("nvenc.code") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-5.png"
|
||||
alt={t("nvenc.imageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("nvenc.after", { code })}</p>
|
||||
|
||||
{/* Section 5 - LXC setup */}
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("lxcSetup.heading")}</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("lxcSetup.identifyHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("lxcSetup.identifyBody")}</p>
|
||||
<CopyableCode code={t.raw("lxcSetup.identifyCode") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-6.png"
|
||||
alt={t("lxcSetup.identifyImageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("lxcSetup.identifyNote")}</p>
|
||||
<div className="overflow-x-auto my-4">
|
||||
<table className="min-w-full border-collapse border border-gray-300">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th className="border border-gray-300 px-4 py-2 text-left text-gray-900">{t("lxcSetup.tableHeaders.device")}</th>
|
||||
<th className="border border-gray-300 px-4 py-2 text-left text-gray-900">{t("lxcSetup.tableHeaders.major")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableRows.map((_, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="border border-gray-300 px-4 py-2 text-gray-800">
|
||||
{t.rich(`lxcSetup.tableRows.${idx}.device`, { code })}
|
||||
</td>
|
||||
<td className="border border-gray-300 px-4 py-2 text-gray-800">
|
||||
{t.rich(`lxcSetup.tableRows.${idx}.major`, { code })}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("lxcSetup.editHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("lxcSetup.editBody", { code })}</p>
|
||||
<CopyableCode code={t.raw("lxcSetup.editCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("lxcSetup.editConfigBody", { code, strong })}</p>
|
||||
<CopyableCode code={t.raw("lxcSetup.editConfigCode") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-7.png"
|
||||
alt={t("lxcSetup.editConfigImageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("lxcSetup.editSave", { code })}</p>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("lxcSetup.installCtHeading")}</h3>
|
||||
<Callout variant="warning" title={t("lxcSetup.installCtCalloutTitle")}>
|
||||
{t.rich("lxcSetup.installCtCalloutBody", { strong })}
|
||||
</Callout>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("lxcSetup.installCtBody")}</p>
|
||||
<CopyableCode code={t.raw("lxcSetup.installCtCode") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("lxcSetup.installCtAfter")}</p>
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-8.png"
|
||||
alt={t("lxcSetup.installCtImageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("lxcSetup.verifyCtHeading")}</h3>
|
||||
<CopyableCode code={t.raw("lxcSetup.verifyCtSmiCode") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-9.png"
|
||||
alt={t("lxcSetup.verifyCtSmiImageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<CopyableCode code={t.raw("lxcSetup.verifyCtLsCode") as string} className="my-4" />
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-10.png"
|
||||
alt={t("lxcSetup.verifyCtLsImageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("lxcSetup.verifyCtAfter")}</p>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3 text-gray-900">{t("lxcSetup.workloadHeading")}</h3>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("lxcSetup.workloadBody")}</p>
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-11.png"
|
||||
alt={t("lxcSetup.workloadImage1Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<Image
|
||||
src="/guides/nvidia/nvidia-12.png"
|
||||
alt={t("lxcSetup.workloadImage2Alt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
unoptimized
|
||||
/>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("lxcSetup.repeatNote", { strong })}</p>
|
||||
|
||||
{/* Section 6 - Docker */}
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("docker.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("docker.body", { code })}</p>
|
||||
<CopyableCode code={t.raw("docker.code") as string} className="my-4" />
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("docker.after")}</p>
|
||||
|
||||
{/* Troubleshooting */}
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("troubleshoot.heading")}</h2>
|
||||
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-2">
|
||||
{troubleItems.map((_, idx) => (
|
||||
<li key={idx}>{t.rich(`troubleshoot.items.${idx}`, { code, strong })}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
309
web/app/[locale]/guides/page.tsx
Normal file
309
web/app/[locale]/guides/page.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import type { Metadata } from "next"
|
||||
import { Link } from "@/i18n/navigation"
|
||||
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
|
||||
import {
|
||||
Play,
|
||||
MessageCircle,
|
||||
Users,
|
||||
Book,
|
||||
Database,
|
||||
Code,
|
||||
BookOpen,
|
||||
Library,
|
||||
Star,
|
||||
Sparkles,
|
||||
ExternalLink,
|
||||
} from "lucide-react"
|
||||
import Footer from "@/components/footer"
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params
|
||||
const t = await getTranslations({ locale, namespace: "guides.meta" })
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
keywords: [
|
||||
"proxmox guides",
|
||||
"proxmox tutorials",
|
||||
"proxmox kodi lxc",
|
||||
"proxmox nvidia driver",
|
||||
"proxmox samba lxc",
|
||||
"proxmox cloud backup",
|
||||
"proxmox vzdump rclone",
|
||||
"proxmox coral tpu",
|
||||
"proxmox gpu lxc",
|
||||
"proxmox ve 9 guides",
|
||||
],
|
||||
alternates: { canonical: "https://proxmenux.com/guides" },
|
||||
openGraph: {
|
||||
title: t("ogTitle"),
|
||||
description: t("ogDescription"),
|
||||
type: "website",
|
||||
url: "https://proxmenux.com/guides",
|
||||
siteName: "ProxMenux",
|
||||
images: [
|
||||
{
|
||||
url: "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/web/public/main.png",
|
||||
width: 1363,
|
||||
height: 735,
|
||||
alt: t("ogImageAlt"),
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: t("twitterTitle"),
|
||||
description: t("twitterDescription"),
|
||||
images: [
|
||||
"https://raw.githubusercontent.com/MacRimi/ProxMenux/main/web/public/main.png",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface Guide {
|
||||
title: string
|
||||
description: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
interface ExternalCardProps {
|
||||
href: string
|
||||
title: string
|
||||
description: string
|
||||
Icon: React.ComponentType<{ className?: string }>
|
||||
color: string // tailwind bg + hover classes
|
||||
external?: boolean
|
||||
}
|
||||
|
||||
function CardLink({ href, title, description, Icon, color, external = true }: ExternalCardProps) {
|
||||
const Inner = (
|
||||
<div className={`block p-6 rounded-lg shadow-md transition-colors h-full ${color}`}>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Icon className="h-6 w-6 text-white flex-shrink-0" />
|
||||
<h3 className="text-xl font-semibold text-white m-0">{title}</h3>
|
||||
</div>
|
||||
<p className="text-gray-200 text-sm m-0">{description}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (external) {
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer" className="block h-full">
|
||||
{Inner}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Link href={href} className="block h-full">
|
||||
{Inner}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function GuidesPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
const t = await getTranslations({ locale, namespace: "guides" })
|
||||
|
||||
const messages = (await getMessages({ locale })) as unknown as {
|
||||
guides: { inDepth: { items: Guide[] } }
|
||||
}
|
||||
const guides = messages.guides.inDepth.items
|
||||
|
||||
const link = (chunks: React.ReactNode) => (
|
||||
<a
|
||||
href="https://github.com/MacRimi/ProxMenux/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-400 hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-gray-900 to-gray-800 text-white pt-16 flex flex-col">
|
||||
<div className="flex-grow container mx-auto px-4 pt-6 pb-16">
|
||||
<div className="mb-10">
|
||||
<h1 className="text-4xl font-bold mb-3">{t("header.title")}</h1>
|
||||
<p className="text-xl text-gray-200 max-w-3xl">{t("header.tagline")}</p>
|
||||
</div>
|
||||
|
||||
{/* ─────────────────────────── In-depth guides ─────────────────────────── */}
|
||||
<section className="mb-16">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<BookOpen className="h-7 w-7 text-blue-400" />
|
||||
<h2 className="text-3xl font-bold m-0">{t("inDepth.heading")}</h2>
|
||||
</div>
|
||||
<p className="text-gray-300 mb-6 max-w-3xl">{t("inDepth.intro")}</p>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{guides.map((guide) => (
|
||||
<Link
|
||||
key={guide.slug}
|
||||
href={`/guides/${guide.slug}`}
|
||||
className="block p-6 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<h3 className="text-xl font-semibold mb-2 text-gray-900 m-0">{guide.title}</h3>
|
||||
<p className="text-sm text-gray-600 mt-2 mb-0">{guide.description}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────────────── ProxMenux references ─────────────────────────── */}
|
||||
<section className="mb-16">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Library className="h-7 w-7 text-emerald-400" />
|
||||
<h2 className="text-3xl font-bold m-0">{t("references.heading")}</h2>
|
||||
</div>
|
||||
<p className="text-gray-300 mb-6 max-w-3xl">{t("references.intro")}</p>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<CardLink
|
||||
href="/docs/glossary"
|
||||
title={t("references.cards.glossary.title")}
|
||||
description={t("references.cards.glossary.description")}
|
||||
Icon={Sparkles}
|
||||
color="bg-emerald-600 hover:bg-emerald-700"
|
||||
external={false}
|
||||
/>
|
||||
<CardLink
|
||||
href="/docs/help-info"
|
||||
title={t("references.cards.helpInfo.title")}
|
||||
description={t("references.cards.helpInfo.description")}
|
||||
Icon={Code}
|
||||
color="bg-teal-600 hover:bg-teal-700"
|
||||
external={false}
|
||||
/>
|
||||
<CardLink
|
||||
href="/guides/linux-resources"
|
||||
title={t("references.cards.linuxResources.title")}
|
||||
description={t("references.cards.linuxResources.description")}
|
||||
Icon={BookOpen}
|
||||
color="bg-cyan-600 hover:bg-cyan-700"
|
||||
external={false}
|
||||
/>
|
||||
<CardLink
|
||||
href="/docs/external-repositories"
|
||||
title={t("references.cards.externalRepos.title")}
|
||||
description={t("references.cards.externalRepos.description")}
|
||||
Icon={Code}
|
||||
color="bg-slate-600 hover:bg-slate-700"
|
||||
external={false}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────────────── Official Proxmox resources ─────────────────────────── */}
|
||||
<section className="mb-16">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Book className="h-7 w-7 text-amber-400" />
|
||||
<h2 className="text-3xl font-bold m-0">{t("official.heading")}</h2>
|
||||
</div>
|
||||
<p className="text-gray-300 mb-6 max-w-3xl">{t("official.intro")}</p>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<CardLink
|
||||
href="https://pve.proxmox.com/pve-docs/index.html"
|
||||
title={t("official.cards.pveDocs.title")}
|
||||
description={t("official.cards.pveDocs.description")}
|
||||
Icon={Book}
|
||||
color="bg-green-600 hover:bg-green-700"
|
||||
/>
|
||||
<CardLink
|
||||
href="https://pbs.proxmox.com/docs/index.html"
|
||||
title={t("official.cards.pbsDocs.title")}
|
||||
description={t("official.cards.pbsDocs.description")}
|
||||
Icon={Database}
|
||||
color="bg-yellow-600 hover:bg-yellow-700"
|
||||
/>
|
||||
<CardLink
|
||||
href="https://www.proxmox.com/en/services/training-courses/videos"
|
||||
title={t("official.cards.videoTraining.title")}
|
||||
description={t("official.cards.videoTraining.description")}
|
||||
Icon={Play}
|
||||
color="bg-red-600 hover:bg-red-700"
|
||||
/>
|
||||
<CardLink
|
||||
href="https://forum.proxmox.com/"
|
||||
title={t("official.cards.forum.title")}
|
||||
description={t("official.cards.forum.description")}
|
||||
Icon={MessageCircle}
|
||||
color="bg-purple-600 hover:bg-purple-700"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────────────── Community projects & resources ─────────────────────────── */}
|
||||
<section className="mb-16">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Star className="h-7 w-7 text-pink-400" />
|
||||
<h2 className="text-3xl font-bold m-0">{t("community.heading")}</h2>
|
||||
</div>
|
||||
<p className="text-gray-300 mb-6 max-w-3xl">{t("community.intro")}</p>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<CardLink
|
||||
href="https://community-scripts.github.io/ProxmoxVE/"
|
||||
title={t("community.cards.helperScripts.title")}
|
||||
description={t("community.cards.helperScripts.description")}
|
||||
Icon={Code}
|
||||
color="bg-indigo-600 hover:bg-indigo-700"
|
||||
/>
|
||||
<CardLink
|
||||
href="https://github.com/Corsinvest/awesome-proxmox-ve"
|
||||
title={t("community.cards.awesome.title")}
|
||||
description={t("community.cards.awesome.description")}
|
||||
Icon={Star}
|
||||
color="bg-pink-600 hover:bg-pink-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-400 mt-6 italic">
|
||||
{t.rich("community.suggestRich", { link })}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* ─────────────────────────── Discussion ─────────────────────────── */}
|
||||
<section className="mb-8">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Users className="h-7 w-7 text-orange-400" />
|
||||
<h2 className="text-3xl font-bold m-0">{t("discussion.heading")}</h2>
|
||||
</div>
|
||||
<p className="text-gray-300 mb-6 max-w-3xl">{t("discussion.intro")}</p>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<CardLink
|
||||
href="https://github.com/MacRimi/ProxMenux/discussions"
|
||||
title={t("discussion.cards.proxmenuxDiscussions.title")}
|
||||
description={t("discussion.cards.proxmenuxDiscussions.description")}
|
||||
Icon={MessageCircle}
|
||||
color="bg-blue-600 hover:bg-blue-700"
|
||||
/>
|
||||
<CardLink
|
||||
href="https://forum.proxmox.com/"
|
||||
title={t("discussion.cards.proxmoxForum.title")}
|
||||
description={t("discussion.cards.proxmoxForum.description")}
|
||||
Icon={MessageCircle}
|
||||
color="bg-purple-600 hover:bg-purple-700"
|
||||
/>
|
||||
<CardLink
|
||||
href="https://www.reddit.com/r/Proxmox/"
|
||||
title={t("discussion.cards.reddit.title")}
|
||||
description={t("discussion.cards.reddit.description")}
|
||||
Icon={Users}
|
||||
color="bg-orange-600 hover:bg-orange-700"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user