diff --git a/apps/docs/app/layout.config.tsx b/apps/docs/app/layout.config.tsx
index e9413af..d04993b 100644
--- a/apps/docs/app/layout.config.tsx
+++ b/apps/docs/app/layout.config.tsx
@@ -1,5 +1,12 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
-import { Github, GlobeIcon, HeartIcon, Rss } from "lucide-react";
+import {
+ Github,
+ GlobeIcon,
+ HeartIcon,
+ Rss,
+ LogIn,
+ UserPlus,
+} from "lucide-react";
import Link from "next/link";
/**
* Shared layout configurations
@@ -45,6 +52,18 @@ export const baseOptions: BaseLayoutProps = {
),
},
links: [
+ {
+ text: "Login",
+ url: "https://app.dokploy.com/",
+ active: "nested-url",
+ icon: ,
+ },
+ {
+ text: "Sign Up",
+ url: "https://app.dokploy.com/register",
+ active: "nested-url",
+ icon: ,
+ },
{
text: "Website",
url: "https://dokploy.com",
diff --git a/apps/docs/content/docs/core/cluster.mdx b/apps/docs/content/docs/core/cluster.mdx
index 134e801..72d971b 100644
--- a/apps/docs/content/docs/core/cluster.mdx
+++ b/apps/docs/content/docs/core/cluster.mdx
@@ -38,24 +38,20 @@ If you choose the second option, we will proceed to configure the different serv
To start, we need to configure a Docker registry, as when deploying an application, you need a registry to deploy and download the application image on the other servers.
-We offer two ways to configure a registry:
-
-1. **External Registry**: Use any registry you want.
-2. **Self-Hosted Registry**: We create and configure a self-hosted registry for you.
-
### External Registry
-You can use any registry, such as Docker Hub, DigitalOcean Spaces, ECR, or your choice. Make sure to enter the correct credentials and test the connection before adding the registry.
+You can use any external registry of your choice. Here are some popular options:
-### Self-Hosted Registry
+1. **Docker Hub** - Free tier available, easy to set up
+2. **GitHub Container Registry (ghcr.io)** - Free for public repositories
+3. **DigitalOcean Container Registry** - Simple setup with good integration
+4. **Amazon ECR** - AWS's managed container registry
+5. **Google Container Registry** - Google Cloud's managed registry
+6. **Azure Container Registry** - Microsoft's managed registry
-We will ask you for three things:
+Make sure to enter the correct credentials and test the connection before adding the registry to your cluster configuration.
-1. A user.
-2. A password.
-3. A domain. Ensure this domain is pointing to the dokploy VPS.
-
-Once set up, the Cluster section will be unlocked.
+Once configured, the Cluster section will be unlocked.
## Understanding Docker Swarm
diff --git a/apps/docs/content/docs/core/docker-compose/index.mdx b/apps/docs/content/docs/core/docker-compose/index.mdx
index 5d24dd6..9c9ea50 100644
--- a/apps/docs/content/docs/core/docker-compose/index.mdx
+++ b/apps/docs/content/docs/core/docker-compose/index.mdx
@@ -73,6 +73,8 @@ volumes:
- "../files/my-configs:/etc/my-app/config" ✅
```
+**Important:** If you need to use files from your repository (configuration files, scripts, etc.), you must move them to Dokploy's File Mounts (via Advanced → Mounts) instead of mounting them directly from the repository. When using AutoDeploy, Dokploy performs a `git clone` on each deployment, which clears the repository directory. Mounting files directly from your repository using relative paths (e.g., `./` or `./config/file.conf`) will cause them to be lost or empty in subsequent deployments. See the [Troubleshooting guide](/docs/core/troubleshooting#using-files-from-your-repository) for more details.
+
## Keyboard Shortcuts
diff --git a/apps/docs/content/docs/core/manual-installation.mdx b/apps/docs/content/docs/core/manual-installation.mdx
index 9fa3088..a4cd347 100644
--- a/apps/docs/content/docs/core/manual-installation.mdx
+++ b/apps/docs/content/docs/core/manual-installation.mdx
@@ -143,7 +143,7 @@ install_dokploy() {
--mount type=volume,source=redis-data-volume,target=/data \
redis:7
- docker pull traefik:v3.5.0
+ docker pull traefik:v3.6.1
docker pull dokploy/dokploy:latest
# Installation
@@ -167,11 +167,11 @@ install_dokploy() {
--restart always \
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
-v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
- -v /var/run/docker.sock:/var/run/docker.sock \
+ -v /var/run/docker.sock:/var/run/docker.sock:ro \
-p 80:80/tcp \
-p 443:443/tcp \
-p 443:443/udp \
- traefik:v3.5.0
+ traefik:v3.6.1
docker network connect dokploy-network dokploy-traefik
@@ -183,11 +183,11 @@ install_dokploy() {
# --network dokploy-network \
# --mount type=bind,source=/etc/dokploy/traefik/traefik.yml,target=/etc/traefik/traefik.yml \
# --mount type=bind,source=/etc/dokploy/traefik/dynamic,target=/etc/dokploy/traefik/dynamic \
- # --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
+ # --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock,readonly \
# --publish mode=host,published=443,target=443 \
# --publish mode=host,published=80,target=80 \
# --publish mode=host,published=443,target=443,protocol=udp \
- # traefik:v3.5.0
+ # traefik:v3.6.1
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
diff --git a/apps/docs/content/docs/core/multi-server/deployments.mdx b/apps/docs/content/docs/core/multi-server/deployments.mdx
index 059fbac..14d3fac 100644
--- a/apps/docs/content/docs/core/multi-server/deployments.mdx
+++ b/apps/docs/content/docs/core/multi-server/deployments.mdx
@@ -16,6 +16,10 @@ The server setup process prepares the necessary environment for securely and eff
Root access to the server is required. We currently do not support non-root deployments.
+
+ If your remote server is configured with a different shell (other than bash), you must configure bash as the default shell, as Dokploy has been developed and tested with bash.
+
+
+ If your remote server is configured with a different shell (other than bash), you must configure bash as the default shell, as Dokploy has been developed and tested with bash.
+
+
2. Create an SSH key by going to `/dashboard/settings/ssh-keys` and add a new key. Be sure to copy the public key.
+ If you need to use files from your repository (e.g., configuration files, scripts, or directories), you **must** move them to Dokploy's file mounts and reference them manually using the Dokploy interface. This is because when using AutoDeploy, Dokploy performs a `git clone` operation on each deployment, which clears the repository directory. If you mount files directly from your repository using relative paths like `./` or `./docker/config/odoo.conf`, these files will be lost or empty in subsequent deployments, even though the first deployment may work correctly.
+
+
+**Why this happens:**
+- On the first deployment, the files exist and are mounted correctly
+- On subsequent deployments, Dokploy cleans the directory and performs a fresh `git clone`
+- Docker loses the reference to the files that were in the filesystem, and the new files have a new reference
+- This causes mounted directories and files to be empty or missing inside the container
+
+**Solution:**
+1. Go to **Advanced** → **Mounts** in your Docker Compose application
+2. Create a new **File Mount** for each file or directory you need from your repository
+3. Copy the content from your repository files into the File Mount content field
+4. Specify the file path for your configuration
+5. Reference the file mount in your `docker-compose.yml` using the `../files/` path:
+
+```yaml
+volumes:
+ - "../files/my-config.json:/etc/my-app/config" ✅
+ - "../files/my-directory:/path/in/container" ✅
+```
+
+**Example:**
+Instead of mounting directly from your repository:
+```yaml
+volumes:
+ - ./:/mnt/extra-addons/va_subscription_18 ❌
+ - ./docker/config/odoo.conf:/etc/odoo/odoo.conf ❌
+```
+
+Use Dokploy's file mounts:
+```yaml
+volumes:
+ - ../files/va_subscription_18:/mnt/extra-addons/va_subscription_18 ✅
+ - ../files/odoo.conf:/etc/odoo/odoo.conf ✅
+```
+
## Logs Not Loading When Deploying to a Remote Server?
There are a few potential reasons for this:
@@ -204,6 +244,10 @@ volumes:
- ../files/my-config.json:/etc/my-app/config
```
+
+ **Important for AutoDeploy users:** If you have configuration files or directories in your repository that you need to mount into your containers, you must copy their content to Dokploy's File Mounts (via Advanced → Mounts) instead of mounting them directly from the repository. This ensures the files persist across deployments, as the repository directory is cleaned and re-cloned on each AutoDeploy.
+
+
## Failed to initialize Docker Swarm
@@ -428,11 +472,11 @@ docker run -d \
--restart always \
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
-v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
- -v /var/run/docker.sock:/var/run/docker.sock \
+ -v /var/run/docker.sock:/var/run/docker.sock:ro \
-p 80:80/tcp \
-p 443:443/tcp \
-p 443:443/udp \
- traefik:v3.5.0
+ traefik:v3.6.1
docker network connect dokploy-network dokploy-traefik
@@ -445,11 +489,11 @@ docker run -d \
--restart always \
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
-v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
- -v /var/run/docker.sock:/var/run/docker.sock \
+ -v /var/run/docker.sock:/var/run/docker.sock:ro \
-p 80:80/tcp \
-p 443:443/tcp \
-p 443:443/udp \
- traefik:v3.5.0
+ traefik:v3.6.1
```
Remove the dokploy service:
diff --git a/apps/website/README.md b/apps/website/README.md
index 693a4a8..5a7f037 100644
--- a/apps/website/README.md
+++ b/apps/website/README.md
@@ -2,6 +2,8 @@
Main Landing Page of Dokploy
+## Development
+
Run development server:
```bash
@@ -14,9 +16,20 @@ yarn dev
Open http://localhost:3000 with your browser to see the result.
+## Environment Variables
-For Blog Page, you can use the following command to generate the static pages:
+### Required for Contact Form
+```
+RESEND_API_KEY=your_resend_api_key_here
+```
+### Required for HubSpot Integration (Sales Forms)
+```
+HUBSPOT_PORTAL_ID=147033433
+HUBSPOT_FORM_GUID=0d788925-ef54-4fda-9b76-741fb5877056
+```
+
+### Required for Blog Page
```
GHOST_URL=""
GHOST_KEY=""
diff --git a/apps/website/app/[locale]/[...rest]/page.tsx b/apps/website/app/[locale]/[...rest]/page.tsx
deleted file mode 100644
index 4583936..0000000
--- a/apps/website/app/[locale]/[...rest]/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { notFound } from "next/navigation";
-
-export default function CatchAll() {
- notFound();
-}
diff --git a/apps/website/app/[locale]/api/og/route.ts b/apps/website/app/[locale]/api/og/route.ts
deleted file mode 100644
index 7a2dc79..0000000
--- a/apps/website/app/[locale]/api/og/route.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { getPost } from "@/lib/ghost";
-import { generateOGImage } from "@/lib/og-image";
-import type { NextRequest } from "next/server";
-
-export async function GET(
- request: NextRequest,
- { params }: { params: { locale: string } },
-) {
- try {
- const { searchParams } = new URL(request.url);
- const slug = searchParams.get("slug");
-
- console.log(
- "Generating OG image for slug:",
- slug,
- "locale:",
- params.locale,
- );
-
- if (!slug) {
- console.error("Missing slug parameter");
- return new Response("Missing slug parameter", { status: 400 });
- }
-
- const post = await getPost(slug);
-
- if (!post) {
- console.error("Post not found for slug:", slug);
- return new Response("Post not found", { status: 404 });
- }
-
- console.log("Found post:", post.title);
-
- const formattedDate = new Date(post.published_at).toLocaleDateString(
- params.locale,
- {
- year: "numeric",
- month: "long",
- day: "numeric",
- },
- );
-
- const ogImage = await generateOGImage({
- title: post.title,
- author: post.primary_author
- ? {
- name: post.primary_author.name,
- image: post.primary_author.profile_image || undefined,
- }
- : undefined,
- date: formattedDate,
- readingTime: post.reading_time,
- });
-
- console.log("Successfully generated OG image");
-
- return new Response(ogImage, {
- headers: {
- "Content-Type": "image/png",
- "Cache-Control": "public, max-age=31536000, immutable",
- },
- });
- } catch (error) {
- console.error("Error generating OG image:", error);
- return new Response(`Error generating image: ${error}`, { status: 500 });
- }
-}
diff --git a/apps/website/app/[locale]/layout.tsx b/apps/website/app/[locale]/layout.tsx
deleted file mode 100644
index 8a7729e..0000000
--- a/apps/website/app/[locale]/layout.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { Inter, Lexend } from "next/font/google";
-import "@/styles/tailwind.css";
-import "react-photo-view/dist/react-photo-view.css";
-import { Footer } from "@/components/Footer";
-import { Header } from "@/components/Header";
-import type { Metadata } from "next";
-import { setRequestLocale } from "next-intl/server";
-
-export const metadata: Metadata = {
- metadataBase: new URL("https://dokploy.com"),
- title: {
- default: "Dokploy - Effortless Deployment Solutions",
- template: "%s | Simplify Your DevOps",
- },
- icons: {
- icon: "icon.svg",
- apple: "apple-touch-icon.png",
- },
- alternates: {
- canonical: "https://dokploy.com",
- languages: {
- en: "https://dokploy.com",
- },
- },
- description:
- "Streamline your deployment process with Dokploy. Effortlessly manage applications and databases on any VPS using Docker and Traefik for improved performance and security.",
- applicationName: "Dokploy",
- keywords: [
- "Dokploy",
- "Docker",
- "Traefik",
- "deployment",
- "VPS",
- "application management",
- "database management",
- "DevOps",
- "cloud infrastructure",
- "UI Self hosted",
- ],
- referrer: "origin",
- robots: "index, follow",
- openGraph: {
- type: "website",
- url: "https://dokploy.com",
- title: "Dokploy - Effortless Deployment Solutions",
- description:
- "Simplify your DevOps with Dokploy. Deploy applications and manage databases efficiently on any VPS.",
- siteName: "Dokploy",
- images: [
- {
- url: "https://dokploy.com/og.png",
- },
- {
- url: "https://dokploy.com/icon.svg",
- width: 24,
- height: 24,
- alt: "Dokploy Logo",
- },
- ],
- },
- twitter: {
- card: "summary_large_image",
- site: "@Dokploy",
- creator: "@Dokploy",
- title: "Dokploy - Simplify Your DevOps",
- description:
- "Deploy applications and manage databases with ease using Dokploy. Learn how our platform can elevate your infrastructure management.",
- images: "https://dokploy.com/og.png",
- },
-};
-
-export default async function RootLayout({
- children,
- params,
-}: {
- children: React.ReactNode;
- params: { locale: string };
-}) {
- const { locale } = await params;
- setRequestLocale(locale);
- return (
-
-
- {children}
-
-
- );
-}
diff --git a/apps/website/app/api/contact/route.ts b/apps/website/app/api/contact/route.ts
new file mode 100644
index 0000000..26469e1
--- /dev/null
+++ b/apps/website/app/api/contact/route.ts
@@ -0,0 +1,145 @@
+import type { NextRequest } from "next/server";
+import { NextResponse } from "next/server";
+import { Resend } from "resend";
+import { submitToHubSpot, getHubSpotUTK } from "@/lib/hubspot";
+
+interface ContactFormData {
+ inquiryType: "support" | "sales" | "other";
+ firstName: string;
+ lastName: string;
+ email: string;
+ company: string;
+ message: string;
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ // Initialize Resend with API key check
+ const apiKey = process.env.RESEND_API_KEY;
+ if (!apiKey) {
+ console.error("RESEND_API_KEY is not configured");
+ return NextResponse.json(
+ { error: "Email service not configured" },
+ { status: 500 },
+ );
+ }
+
+ const resend = new Resend(apiKey);
+ const body: ContactFormData = await request.json();
+
+ // Validate required fields
+ if (
+ !body.inquiryType ||
+ !body.firstName ||
+ !body.lastName ||
+ !body.email ||
+ !body.company ||
+ !body.message
+ ) {
+ return NextResponse.json(
+ { error: "All fields are required" },
+ { status: 400 },
+ );
+ }
+
+ // Validate email format
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(body.email)) {
+ return NextResponse.json(
+ { error: "Invalid email format" },
+ { status: 400 },
+ );
+ }
+
+ // Submit to HubSpot if it's a sales inquiry
+ if (body.inquiryType === "sales") {
+ try {
+ const hutk = getHubSpotUTK(request.headers.get("cookie") || undefined);
+ const hubspotSuccess = await submitToHubSpot(body, hutk);
+
+ if (hubspotSuccess) {
+ console.log("Successfully submitted sales inquiry to HubSpot");
+ } else {
+ console.warn(
+ "Failed to submit sales inquiry to HubSpot, but continuing with email",
+ );
+ }
+ } catch (error) {
+ console.error("Error submitting to HubSpot:", error);
+ // Continue with email even if HubSpot fails
+ }
+ }
+
+ // Format email content
+ const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.firstName} ${body.lastName}`;
+ const emailBody = `
+New contact form submission:
+
+Type: ${body.inquiryType}
+First Name: ${body.firstName}
+Last Name: ${body.lastName}
+Email: ${body.email}
+Company: ${body.company}
+
+Message:
+${body.message}
+
+---
+Sent from Dokploy website contact form
+ `.trim();
+
+ // Send email to Dokploy team
+ await resend.emails.send({
+ from: "Dokploy Contact Form ",
+ to:
+ body.inquiryType === "sales"
+ ? ["sales@dokploy.com", "contact@dokploy.com"]
+ : ["contact@dokploy.com"],
+ subject: emailSubject,
+ text: emailBody,
+ replyTo: body.email,
+ });
+
+ // Send confirmation email to the user
+ const confirmationSubject =
+ "Thank you for contacting Dokploy - We received your message";
+ const confirmationBody = `
+Hello ${body.firstName} ${body.lastName},
+
+Thank you for reaching out to us! We have successfully received your message and our team will get back to you as soon as possible.
+
+Here's a summary of what you sent us:
+
+Subject: ${body.inquiryType.charAt(0).toUpperCase() + body.inquiryType.slice(1)} inquiry
+Company: ${body.company}
+Message: ${body.message}
+
+We typically respond within 24-48 hours during business days. If your inquiry is urgent, please don't hesitate to reach out to us directly.
+
+Best regards,
+The Dokploy Team
+
+---
+This is an automated confirmation email. Please do not reply to this email.
+If you need immediate assistance, contact us at contact@dokploy.com
+ `.trim();
+
+ await resend.emails.send({
+ from: "Dokploy Team ",
+ to: [body.email],
+ subject: confirmationSubject,
+ text: confirmationBody,
+ });
+
+ return NextResponse.json(
+ { message: "Contact form submitted successfully" },
+ { status: 200 },
+ );
+ } catch (error) {
+ console.error("Error processing contact form:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 },
+ );
+ }
+}
diff --git a/apps/website/app/api/github-stars/route.ts b/apps/website/app/api/github-stars/route.ts
new file mode 100644
index 0000000..57c7a4d
--- /dev/null
+++ b/apps/website/app/api/github-stars/route.ts
@@ -0,0 +1,77 @@
+import { NextResponse } from "next/server";
+
+// Cache the result for 5 minutes to avoid rate limiting
+let cachedStars: { count: number; timestamp: number } | null = null;
+const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const owner = searchParams.get("owner");
+ const repo = searchParams.get("repo");
+
+ if (!owner || !repo) {
+ return NextResponse.json(
+ { error: "Owner and repo parameters are required" },
+ { status: 400 },
+ );
+ }
+
+ // Check if we have a valid cached result
+ if (
+ cachedStars &&
+ Date.now() - cachedStars.timestamp < CACHE_DURATION
+ ) {
+ return NextResponse.json(
+ { stargazers_count: cachedStars.count },
+ {
+ headers: {
+ "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
+ },
+ },
+ );
+ }
+
+ try {
+ const response = await fetch(
+ `https://api.github.com/repos/${owner}/${repo}`,
+ {
+ headers: {
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "Dokploy-Website",
+ },
+ },
+ );
+
+ if (!response.ok) {
+ return NextResponse.json(
+ { error: "Failed to fetch repository data" },
+ { status: response.status },
+ );
+ }
+
+ const data = await response.json();
+ const starCount = data.stargazers_count;
+
+ // Cache the result
+ cachedStars = {
+ count: starCount,
+ timestamp: Date.now(),
+ };
+
+ return NextResponse.json(
+ { stargazers_count: starCount },
+ {
+ headers: {
+ "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
+ },
+ },
+ );
+ } catch (error) {
+ console.error("Error fetching GitHub stars:", error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 },
+ );
+ }
+}
+
diff --git a/apps/website/app/api/og/route.ts b/apps/website/app/api/og/route.ts
index 9883471..53cac07 100644
--- a/apps/website/app/api/og/route.ts
+++ b/apps/website/app/api/og/route.ts
@@ -21,7 +21,6 @@ export async function GET(request: NextRequest) {
return new Response("Post not found", { status: 404 });
}
- console.log("Found post:", post.title);
const formattedDate = new Date(post.published_at).toLocaleDateString(
"en-US",
@@ -44,8 +43,6 @@ export async function GET(request: NextRequest) {
readingTime: post.reading_time,
});
- console.log("Successfully generated OG image");
-
return new Response(ogImage, {
headers: {
"Content-Type": "image/png",
diff --git a/apps/website/app/[locale]/blog/[slug]/components/CodeBlock.tsx b/apps/website/app/blog/[slug]/components/CodeBlock.tsx
similarity index 80%
rename from apps/website/app/[locale]/blog/[slug]/components/CodeBlock.tsx
rename to apps/website/app/blog/[slug]/components/CodeBlock.tsx
index 0cee0a2..da8be4e 100644
--- a/apps/website/app/[locale]/blog/[slug]/components/CodeBlock.tsx
+++ b/apps/website/app/blog/[slug]/components/CodeBlock.tsx
@@ -18,9 +18,7 @@ interface CodeBlockProps {
async function formatCode(code: string, lang: string) {
try {
let parser: string;
- let plugins = [];
-
- // Select parser and plugins based on language
+ let plugins = [] as any[];
switch (lang.toLowerCase()) {
case "yaml":
case "yml":
@@ -35,12 +33,8 @@ async function formatCode(code: string, lang: string) {
plugins = [babel, estree];
break;
default:
- // For unsupported languages, return the original code
return code;
}
-
- console.log(`Formatting ${lang} with parser:`, parser);
-
const formatted = await prettier.format(code, {
parser,
plugins,
@@ -50,12 +44,10 @@ async function formatCode(code: string, lang: string) {
useTabs: false,
printWidth: 120,
});
-
- console.log("Formatted code:", formatted);
return formatted;
} catch (error) {
console.error("Error formatting code:", error);
- return code; // Return original code if there's an error
+ return code;
}
}
@@ -66,22 +58,15 @@ export function CodeBlock({ code, lang, initial }: CodeBlockProps) {
useLayoutEffect(() => {
async function formatAndHighlight() {
try {
- console.log("Original code:", code);
- console.log("Language:", lang);
const formatted = await formatCode(code, lang);
setFormattedCode(formatted);
-
- // Then highlight the formatted code
const highlighted = await highlight(formatted, lang);
setNodes(highlighted);
} catch (error) {
- console.error("Error in formatAndHighlight:", error);
- // If formatting fails, try to highlight the original code
const highlighted = await highlight(code, lang);
setNodes(highlighted);
}
}
-
void formatAndHighlight();
}, [code, lang]);
diff --git a/apps/website/app/[locale]/blog/[slug]/components/Headings.tsx b/apps/website/app/blog/[slug]/components/Headings.tsx
similarity index 87%
rename from apps/website/app/[locale]/blog/[slug]/components/Headings.tsx
rename to apps/website/app/blog/[slug]/components/Headings.tsx
index 540c247..cc6ac6d 100644
--- a/apps/website/app/[locale]/blog/[slug]/components/Headings.tsx
+++ b/apps/website/app/blog/[slug]/components/Headings.tsx
@@ -29,15 +29,10 @@ function LinkIcon() {
export function H1({ children, ...props }: HeadingProps) {
const router = useRouter();
- const id = slugify(children?.toString() || "", {
- lower: true,
- strict: true,
- });
-
+ const id = slugify(children?.toString() || "", { lower: true, strict: true });
const handleClick = () => {
router.push(`#${id}`);
};
-
return (
{
router.push(`#${id}`);
};
-
return (
{
router.push(`#${id}`);
};
-
return (
observer.disconnect();
@@ -48,31 +46,25 @@ export function TableOfContents() {
Table of Contents
{headings.length > 0 ? (
- <>
- {headings.map((heading) => (
- - (
+
-
+ {
+ e.preventDefault();
+ document
+ .getElementById(heading.id)
+ ?.scrollIntoView({ behavior: "smooth" });
+ }}
+ className={`hover:text-primary transition-colors block ${activeId === heading.id ? "text-primary font-medium" : "text-muted-foreground"}`}
>
- {
- e.preventDefault();
- document.getElementById(heading.id)?.scrollIntoView({
- behavior: "smooth",
- });
- }}
- className={`hover:text-primary transition-colors block ${
- activeId === heading.id
- ? "text-primary font-medium"
- : "text-muted-foreground"
- }`}
- >
- {heading.text}
-
-
- ))}
- >
+ {heading.text}
+
+
+ ))
) : (
-
No headings found
diff --git a/apps/website/app/[locale]/blog/[slug]/components/ZoomableImage.tsx b/apps/website/app/blog/[slug]/components/ZoomableImage.tsx
similarity index 99%
rename from apps/website/app/[locale]/blog/[slug]/components/ZoomableImage.tsx
rename to apps/website/app/blog/[slug]/components/ZoomableImage.tsx
index 702c342..7b97960 100644
--- a/apps/website/app/[locale]/blog/[slug]/components/ZoomableImage.tsx
+++ b/apps/website/app/blog/[slug]/components/ZoomableImage.tsx
@@ -3,6 +3,7 @@
import { cn } from "@/lib/utils";
import { PhotoProvider, PhotoView } from "react-photo-view";
import "react-photo-view/dist/react-photo-view.css";
+
interface ZoomableImageProps {
src: string;
alt: string;
diff --git a/apps/website/app/[locale]/blog/[slug]/components/shared.ts b/apps/website/app/blog/[slug]/components/shared.ts
similarity index 84%
rename from apps/website/app/[locale]/blog/[slug]/components/shared.ts
rename to apps/website/app/blog/[slug]/components/shared.ts
index 377f2ca..4e6096b 100644
--- a/apps/website/app/[locale]/blog/[slug]/components/shared.ts
+++ b/apps/website/app/blog/[slug]/components/shared.ts
@@ -10,10 +10,5 @@ export async function highlight(code: string, lang: BundledLanguage) {
lang,
theme: "houston",
});
-
- return toJsxRuntime(out, {
- Fragment,
- jsx,
- jsxs,
- }) as JSX.Element;
+ return toJsxRuntime(out, { Fragment, jsx, jsxs }) as JSX.Element;
}
diff --git a/apps/website/app/[locale]/blog/[slug]/page.tsx b/apps/website/app/blog/[slug]/page.tsx
similarity index 90%
rename from apps/website/app/[locale]/blog/[slug]/page.tsx
rename to apps/website/app/blog/[slug]/page.tsx
index 95a95db..03e63cf 100644
--- a/apps/website/app/[locale]/blog/[slug]/page.tsx
+++ b/apps/website/app/blog/[slug]/page.tsx
@@ -1,6 +1,5 @@
import { getPost, getPosts } from "@/lib/ghost";
import type { Metadata, ResolvingMetadata } from "next";
-import { getTranslations } from "next-intl/server";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
@@ -18,15 +17,16 @@ import { CodeBlock } from "./components/CodeBlock";
import { H1, H2, H3 } from "./components/Headings";
import { TableOfContents } from "./components/TableOfContents";
import { ZoomableImage } from "./components/ZoomableImage";
+
type Props = {
- params: { locale: string; slug: string };
+ params: { slug: string };
};
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata,
): Promise {
- const { locale, slug } = await params;
+ const { slug } = await params;
const post = await getPost(slug);
if (!post) {
@@ -36,7 +36,7 @@ export async function generateMetadata(
}
const ogUrl = new URL(
- `/${locale}/api/og`,
+ `/api/og`,
process.env.NODE_ENV === "production"
? "https://dokploy.com"
: "http://localhost:3000",
@@ -69,51 +69,29 @@ export async function generateMetadata(
};
}
-// export async function generateStaticParams() {
-// const posts = await getPosts();
-// const locales = ["en", "fr", "es", "zh-Hans"];
-
-// return posts.flatMap((post) =>
-// locales.map((locale) => ({
-// locale,
-// slug: post.slug,
-// })),
-// );
-// }
-
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params;
- const t = await getTranslations("blog");
const post = await getPost(slug);
const allPosts = await getPosts();
- // Get related posts (excluding current post)
- const relatedPosts = allPosts.filter((p) => p.id !== post?.id).slice(0, 3); // Show only 3 related posts
+ const relatedPosts = allPosts.filter((p) => p.id !== post?.id).slice(0, 3);
if (!post) {
notFound();
}
- // Limpiar HTML antes de convertir a Markdown
const cleanHtml = (html: string) => {
- // Crear un DOM temporal para limpiar el HTML
if (typeof window !== "undefined") {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
-
- // Remover scripts JSON-LD y otros scripts
const scripts = doc.querySelectorAll(
'script[type="application/ld+json"], script',
);
scripts.forEach((script) => script.remove());
-
- // Remover otros elementos no deseados
const unwantedElements = doc.querySelectorAll("style, meta, link");
unwantedElements.forEach((el) => el.remove());
-
return doc.body.innerHTML;
} else {
- // Fallback para servidor - usar regex para limpiar
return html
.replace(
/