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:
MacRimi
2026-05-31 12:41:10 +02:00
parent 875910b4d7
commit 5ca3463bf6
649 changed files with 83958 additions and 11096 deletions

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}