diff --git a/README.md b/README.md
index f40e0e7..cdae681 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,6 @@ Example:
feat: add new feature
```
-
## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release.
diff --git a/apps/docs/app/docs/[[...slug]]/page.tsx b/apps/docs/app/docs/[[...slug]]/page.tsx
index f235418..1527077 100644
--- a/apps/docs/app/docs/[[...slug]]/page.tsx
+++ b/apps/docs/app/docs/[[...slug]]/page.tsx
@@ -4,114 +4,114 @@ import { baseUrl } from "@/utils/metadata";
import { ImageZoom } from "fumadocs-ui/components/image-zoom";
import defaultMdxComponents from "fumadocs-ui/mdx";
import {
- DocsBody,
- DocsDescription,
- DocsPage,
- DocsTitle,
+ DocsBody,
+ DocsDescription,
+ DocsPage,
+ DocsTitle,
} from "fumadocs-ui/page";
import { notFound, permanentRedirect } from "next/navigation";
export default async function Page(props: {
- params: Promise<{ slug?: string[] }>;
+ params: Promise<{ slug?: string[] }>;
}) {
- const params = await props.params;
- const page = source.getPage(params.slug);
- if (!page) {
- permanentRedirect("/docs/core");
- }
+ const params = await props.params;
+ const page = source.getPage(params.slug);
+ if (!page) {
+ permanentRedirect("/docs/core");
+ }
- const MDX = page.data.body;
+ const MDX = page.data.body;
- return (
-
- {page.data.title}
- {page.data.description}
-
- ,
- p: ({ children }) => (
-
- {children}
-
- ),
- li: ({ children, id }) => (
-
- {children}
-
- ),
- APIPage: openapi.APIPage,
- }}
- />
-
-
- );
+ return (
+
+ {page.data.title}
+ {page.data.description}
+
+ ,
+ p: ({ children }) => (
+
+ {children}
+
+ ),
+ li: ({ children, id }) => (
+
+ {children}
+
+ ),
+ APIPage: openapi.APIPage,
+ }}
+ />
+
+
+ );
}
export async function generateStaticParams() {
- return source.generateParams();
+ return source.generateParams();
}
export async function generateMetadata(props: {
- params: Promise<{ slug?: string[] }>;
+ params: Promise<{ slug?: string[] }>;
}) {
- const params = await props.params;
- const page = source.getPage(params.slug);
- if (!page) notFound();
+ const params = await props.params;
+ const page = source.getPage(params.slug);
+ if (!page) notFound();
- return {
- title: page.data.title,
+ return {
+ title: page.data.title,
- description: page.data.description,
- robots: "index,follow",
- alternates: {
- canonical: new URL(`${baseUrl}${page.url}`).toString(),
- languages: {
- en: `${baseUrl}/${page.url}`,
- },
- },
- openGraph: {
- title: page.data.title,
- description: page.data.description,
- url: new URL(`${baseUrl}`).toString(),
- images: [
- {
- url: new URL(`${baseUrl}/logo.png`).toString(),
- width: 1200,
- height: 630,
- alt: page.data.title,
- },
- ],
- },
- twitter: {
- card: "summary_large_image",
- creator: "@getdokploy",
- title: page.data.title,
- description: page.data.description,
- images: [
- {
- url: new URL(`${baseUrl}/logo.png`).toString(),
- width: 1200,
- height: 630,
- alt: page.data.title,
- },
- ],
- },
- applicationName: "Dokploy Docs",
- keywords: [
- "dokploy",
- "vps",
- "open source",
- "cloud",
- "self hosting",
- "free",
- ],
- icons: {
- icon: "/icon.svg",
- },
- };
+ description: page.data.description,
+ robots: "index,follow",
+ alternates: {
+ canonical: new URL(`${baseUrl}${page.url}`).toString(),
+ languages: {
+ en: `${baseUrl}/${page.url}`,
+ },
+ },
+ openGraph: {
+ title: page.data.title,
+ description: page.data.description,
+ url: new URL(`${baseUrl}`).toString(),
+ images: [
+ {
+ url: new URL(`${baseUrl}/logo.png`).toString(),
+ width: 1200,
+ height: 630,
+ alt: page.data.title,
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ creator: "@getdokploy",
+ title: page.data.title,
+ description: page.data.description,
+ images: [
+ {
+ url: new URL(`${baseUrl}/logo.png`).toString(),
+ width: 1200,
+ height: 630,
+ alt: page.data.title,
+ },
+ ],
+ },
+ applicationName: "Dokploy Docs",
+ keywords: [
+ "dokploy",
+ "vps",
+ "open source",
+ "cloud",
+ "self hosting",
+ "free",
+ ],
+ icons: {
+ icon: "/icon.svg",
+ },
+ };
}
diff --git a/apps/docs/app/docs/layout.tsx b/apps/docs/app/docs/layout.tsx
index fb8ff11..322a090 100644
--- a/apps/docs/app/docs/layout.tsx
+++ b/apps/docs/app/docs/layout.tsx
@@ -5,18 +5,18 @@ import { DocsLayout } from "fumadocs-ui/layouts/docs";
import type { ReactNode } from "react";
export const metadata = createMetadata({
- title: {
- template: "%s | Dokploy",
- default: "Dokploy",
- },
- description: "The Open Source Alternative to Vercel, Heroku, and Netlify",
- metadataBase: new URL(baseUrl),
+ title: {
+ template: "%s | Dokploy",
+ default: "Dokploy",
+ },
+ description: "The Open Source Alternative to Vercel, Heroku, and Netlify",
+ metadataBase: new URL(baseUrl),
});
export default function Layout({ children }: { children: ReactNode }) {
- return (
-
- {children}
-
- );
+ return (
+
+ {children}
+
+ );
}
diff --git a/apps/docs/app/layout.config.tsx b/apps/docs/app/layout.config.tsx
index d04993b..b643745 100644
--- a/apps/docs/app/layout.config.tsx
+++ b/apps/docs/app/layout.config.tsx
@@ -1,11 +1,11 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import {
- Github,
- GlobeIcon,
- HeartIcon,
- Rss,
- LogIn,
- UserPlus,
+ Github,
+ GlobeIcon,
+ HeartIcon,
+ Rss,
+ LogIn,
+ UserPlus,
} from "lucide-react";
import Link from "next/link";
/**
@@ -17,106 +17,106 @@ import Link from "next/link";
*/
export const Logo = () => {
- return (
-
+ );
};
export const baseOptions: BaseLayoutProps = {
- nav: {
- // title: "Dokploy",
- children: (
-
-
- Dokploy
-
- ),
- },
- 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",
- active: "nested-url",
- icon: ,
- },
- {
- text: "Discord",
- url: "https://discord.com/invite/2tBnJ3jDJc",
- active: "nested-url",
- icon: (
- <>
-
-
-
- >
- ),
- },
- {
- text: "Support",
- url: "https://opencollective.com/dokploy",
- active: "nested-url",
- icon: (
- <>
-
- >
- ),
- },
- {
- text: "Github",
- url: "https://github.com/dokploy/dokploy",
- active: "nested-url",
- icon: (
- <>
-
- >
- ),
- },
- {
- text: "Blog",
- url: "https://dokploy.com/blog",
- active: "nested-url",
- icon: (
- <>
-
- >
- ),
- },
- ],
+ nav: {
+ // title: "Dokploy",
+ children: (
+
+
+ Dokploy
+
+ ),
+ },
+ 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",
+ active: "nested-url",
+ icon: ,
+ },
+ {
+ text: "Discord",
+ url: "https://discord.com/invite/2tBnJ3jDJc",
+ active: "nested-url",
+ icon: (
+ <>
+
+
+
+ >
+ ),
+ },
+ {
+ text: "Support",
+ url: "https://opencollective.com/dokploy",
+ active: "nested-url",
+ icon: (
+ <>
+
+ >
+ ),
+ },
+ {
+ text: "Github",
+ url: "https://github.com/dokploy/dokploy",
+ active: "nested-url",
+ icon: (
+ <>
+
+ >
+ ),
+ },
+ {
+ text: "Blog",
+ url: "https://dokploy.com/blog",
+ active: "nested-url",
+ icon: (
+ <>
+
+ >
+ ),
+ },
+ ],
};
diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx
index 1c6adc0..d2bd648 100644
--- a/apps/docs/app/layout.tsx
+++ b/apps/docs/app/layout.tsx
@@ -4,19 +4,21 @@ import { Inter } from "next/font/google";
import type { ReactNode } from "react";
import { GoogleAnalytics } from "@next/third-parties/google";
const inter = Inter({
- subsets: ["latin"],
+ subsets: ["latin"],
});
export default async function Layout({
- children,
- ...rest
-}: { children: ReactNode }) {
- return (
-
-
-
- {children}
-
-
- );
+ children,
+ ...rest
+}: {
+ children: ReactNode;
+}) {
+ return (
+
+
+
+ {children}
+
+
+ );
}
diff --git a/apps/docs/app/robots.ts b/apps/docs/app/robots.ts
index 3b00704..1c2ea94 100644
--- a/apps/docs/app/robots.ts
+++ b/apps/docs/app/robots.ts
@@ -1,11 +1,11 @@
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
- return {
- rules: {
- userAgent: "*",
- allow: "/",
- },
- sitemap: "https://docs.dokploy.com/sitemap.xml",
- };
+ return {
+ rules: {
+ userAgent: "*",
+ allow: "/",
+ },
+ sitemap: "https://docs.dokploy.com/sitemap.xml",
+ };
}
diff --git a/apps/docs/app/sitemap.ts b/apps/docs/app/sitemap.ts
index 39ca0aa..49a40f1 100644
--- a/apps/docs/app/sitemap.ts
+++ b/apps/docs/app/sitemap.ts
@@ -3,17 +3,17 @@ import { url } from "@/utils/metadata";
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise {
- return [
- ...(await Promise.all(
- source.getPages().map(async (page) => {
- const { lastModified } = page.data;
- return {
- url: url(page.url),
- lastModified: lastModified ? new Date(lastModified) : undefined,
- changeFrequency: "weekly",
- priority: 0.5,
- } as MetadataRoute.Sitemap[number];
- }),
- )),
- ];
+ return [
+ ...(await Promise.all(
+ source.getPages().map(async (page) => {
+ const { lastModified } = page.data;
+ return {
+ url: url(page.url),
+ lastModified: lastModified ? new Date(lastModified) : undefined,
+ changeFrequency: "weekly",
+ priority: 0.5,
+ } as MetadataRoute.Sitemap[number];
+ }),
+ )),
+ ];
}
diff --git a/apps/docs/lib/source.ts b/apps/docs/lib/source.ts
index 5593e67..d588396 100644
--- a/apps/docs/lib/source.ts
+++ b/apps/docs/lib/source.ts
@@ -5,13 +5,13 @@ import { createOpenAPI } from "fumadocs-openapi/server";
import { attachFile } from "fumadocs-openapi/server";
export const source = loader({
- baseUrl: "/docs",
- source: createMDXSource(docs, meta),
- // pageTree: {
- // attachFile,
- // },
+ baseUrl: "/docs",
+ source: createMDXSource(docs, meta),
+ // pageTree: {
+ // attachFile,
+ // },
});
export const openapi = createOpenAPI({
- // options
+ // options
});
diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts
index 62ab25d..443d9d3 100644
--- a/apps/docs/source.config.ts
+++ b/apps/docs/source.config.ts
@@ -1,7 +1,7 @@
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
export const { docs, meta } = defineDocs({
- dir: "content/docs",
+ dir: "content/docs",
});
export default defineConfig();
diff --git a/apps/docs/utils/metadata.ts b/apps/docs/utils/metadata.ts
index c68397d..8bc700d 100644
--- a/apps/docs/utils/metadata.ts
+++ b/apps/docs/utils/metadata.ts
@@ -1,30 +1,30 @@
import type { Metadata } from "next";
export const baseUrl =
- process.env.NODE_ENV === "development"
- ? "http://localhost:3000"
- : "https://docs.dokploy.com";
+ process.env.NODE_ENV === "development"
+ ? "http://localhost:3000"
+ : "https://docs.dokploy.com";
export const url = (path: string): string => new URL(path, baseUrl).toString();
export function createMetadata(override: Metadata): Metadata {
- return {
- ...override,
- openGraph: {
- title: override.title ?? undefined,
- description: override.description ?? undefined,
- url: "https://fumadocs.vercel.app",
- images: "/og.png",
- siteName: "Fumadocs",
- ...override.openGraph,
- },
- twitter: {
- card: "summary_large_image",
- creator: "@money_is_shark",
- title: override.title ?? undefined,
- description: override.description ?? undefined,
- images: "/banner.png",
- ...override.twitter,
- },
- };
+ return {
+ ...override,
+ openGraph: {
+ title: override.title ?? undefined,
+ description: override.description ?? undefined,
+ url: "https://fumadocs.vercel.app",
+ images: "/og.png",
+ siteName: "Fumadocs",
+ ...override.openGraph,
+ },
+ twitter: {
+ card: "summary_large_image",
+ creator: "@money_is_shark",
+ title: override.title ?? undefined,
+ description: override.description ?? undefined,
+ images: "/banner.png",
+ ...override.twitter,
+ },
+ };
}
diff --git a/apps/website/README.md b/apps/website/README.md
index 5a7f037..4d88448 100644
--- a/apps/website/README.md
+++ b/apps/website/README.md
@@ -19,18 +19,21 @@ Open http://localhost:3000 with your browser to see the result.
## Environment Variables
### 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=""
-```
\ No newline at end of file
+```
diff --git a/apps/website/app/api/contact/route.ts b/apps/website/app/api/contact/route.ts
index 26469e1..1322171 100644
--- a/apps/website/app/api/contact/route.ts
+++ b/apps/website/app/api/contact/route.ts
@@ -1,31 +1,31 @@
-import type { NextRequest } from "next/server";
-import { NextResponse } from "next/server";
-import { Resend } from "resend";
-import { submitToHubSpot, getHubSpotUTK } from "@/lib/hubspot";
+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;
+ 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;
+ const apiKey = process.env.RESEND_API_KEY
if (!apiKey) {
- console.error("RESEND_API_KEY is not configured");
+ console.error('RESEND_API_KEY is not configured')
return NextResponse.json(
- { error: "Email service not configured" },
+ { error: 'Email service not configured' },
{ status: 500 },
- );
+ )
}
- const resend = new Resend(apiKey);
- const body: ContactFormData = await request.json();
+ const resend = new Resend(apiKey)
+ const body: ContactFormData = await request.json()
// Validate required fields
if (
@@ -37,41 +37,45 @@ export async function POST(request: NextRequest) {
!body.message
) {
return NextResponse.json(
- { error: "All fields are required" },
+ { error: 'All fields are required' },
{ status: 400 },
- );
+ )
}
// Validate email format
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(body.email)) {
return NextResponse.json(
- { error: "Invalid email format" },
+ { error: 'Invalid email format' },
{ status: 400 },
- );
+ )
}
// Submit to HubSpot if it's a sales inquiry
- if (body.inquiryType === "sales") {
+ if (body.inquiryType === 'sales') {
try {
- const hutk = getHubSpotUTK(request.headers.get("cookie") || undefined);
- const hubspotSuccess = await submitToHubSpot(body, hutk);
+ const hutk = getHubSpotUTK(
+ request.headers.get('cookie') || undefined,
+ )
+ const hubspotSuccess = await submitToHubSpot(body, hutk)
if (hubspotSuccess) {
- console.log("Successfully submitted sales inquiry to HubSpot");
+ console.log(
+ 'Successfully submitted sales inquiry to HubSpot',
+ )
} else {
console.warn(
- "Failed to submit sales inquiry to HubSpot, but continuing with email",
- );
+ 'Failed to submit sales inquiry to HubSpot, but continuing with email',
+ )
}
} catch (error) {
- console.error("Error submitting to HubSpot:", 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 emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.firstName} ${body.lastName}`
const emailBody = `
New contact form submission:
@@ -86,23 +90,23 @@ ${body.message}
---
Sent from Dokploy website contact form
- `.trim();
+ `.trim()
// Send email to Dokploy team
await resend.emails.send({
- from: "Dokploy Contact Form ",
+ from: 'Dokploy Contact Form ',
to:
- body.inquiryType === "sales"
- ? ["sales@dokploy.com", "contact@dokploy.com"]
- : ["contact@dokploy.com"],
+ 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";
+ 'Thank you for contacting Dokploy - We received your message'
const confirmationBody = `
Hello ${body.firstName} ${body.lastName},
@@ -122,24 +126,24 @@ 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();
+ `.trim()
await resend.emails.send({
- from: "Dokploy Team ",
+ from: 'Dokploy Team ',
to: [body.email],
subject: confirmationSubject,
text: confirmationBody,
- });
+ })
return NextResponse.json(
- { message: "Contact form submitted successfully" },
+ { message: 'Contact form submitted successfully' },
{ status: 200 },
- );
+ )
} catch (error) {
- console.error("Error processing contact form:", error);
+ console.error('Error processing contact form:', error)
return NextResponse.json(
- { error: "Internal server error" },
+ { 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
index 57c7a4d..9426ff5 100644
--- a/apps/website/app/api/github-stars/route.ts
+++ b/apps/website/app/api/github-stars/route.ts
@@ -1,34 +1,32 @@
-import { NextResponse } from "next/server";
+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
+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");
+ 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" },
+ { 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
- ) {
+ 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",
+ 'Cache-Control':
+ 'public, s-maxage=300, stale-while-revalidate=600',
},
},
- );
+ )
}
try {
@@ -36,42 +34,42 @@ export async function GET(request: Request) {
`https://api.github.com/repos/${owner}/${repo}`,
{
headers: {
- Accept: "application/vnd.github.v3+json",
- "User-Agent": "Dokploy-Website",
+ Accept: 'application/vnd.github.v3+json',
+ 'User-Agent': 'Dokploy-Website',
},
},
- );
+ )
if (!response.ok) {
return NextResponse.json(
- { error: "Failed to fetch repository data" },
+ { error: 'Failed to fetch repository data' },
{ status: response.status },
- );
+ )
}
- const data = await response.json();
- const starCount = data.stargazers_count;
+ 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",
+ 'Cache-Control':
+ 'public, s-maxage=300, stale-while-revalidate=600',
},
},
- );
+ )
} catch (error) {
- console.error("Error fetching GitHub stars:", error);
+ console.error('Error fetching GitHub stars:', error)
return NextResponse.json(
- { error: "Internal server error" },
+ { 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 53cac07..2bbbddd 100644
--- a/apps/website/app/api/og/route.ts
+++ b/apps/website/app/api/og/route.ts
@@ -1,35 +1,34 @@
-import { getPost } from "@/lib/ghost";
-import { generateOGImage } from "@/lib/og-image";
-import type { NextRequest } from "next/server";
+import { getPost } from '@/lib/ghost'
+import { generateOGImage } from '@/lib/og-image'
+import type { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
try {
- const { searchParams } = new URL(request.url);
- const slug = searchParams.get("slug");
+ const { searchParams } = new URL(request.url)
+ const slug = searchParams.get('slug')
- console.log("Generating OG image for slug:", slug);
+ console.log('Generating OG image for slug:', slug)
if (!slug) {
- console.error("Missing slug parameter");
- return new Response("Missing slug parameter", { status: 400 });
+ console.error('Missing slug parameter')
+ return new Response('Missing slug parameter', { status: 400 })
}
- const post = await getPost(slug);
+ const post = await getPost(slug)
if (!post) {
- console.error("Post not found for slug:", slug);
- return new Response("Post not found", { status: 404 });
+ console.error('Post not found for slug:', slug)
+ return new Response('Post not found', { status: 404 })
}
-
const formattedDate = new Date(post.published_at).toLocaleDateString(
- "en-US",
+ 'en-US',
{
- year: "numeric",
- month: "long",
- day: "numeric",
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
},
- );
+ )
const ogImage = await generateOGImage({
title: post.title,
@@ -41,16 +40,16 @@ export async function GET(request: NextRequest) {
: undefined,
date: formattedDate,
readingTime: post.reading_time,
- });
+ })
return new Response(ogImage, {
headers: {
- "Content-Type": "image/png",
- "Cache-Control": "public, max-age=31536000, immutable",
+ '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 });
+ console.error('Error generating OG image:', error)
+ return new Response(`Error generating image: ${error}`, { status: 500 })
}
}
diff --git a/apps/website/app/blog/[slug]/components/CodeBlock.tsx b/apps/website/app/blog/[slug]/components/CodeBlock.tsx
index da8be4e..ac8ea91 100644
--- a/apps/website/app/blog/[slug]/components/CodeBlock.tsx
+++ b/apps/website/app/blog/[slug]/components/CodeBlock.tsx
@@ -1,39 +1,39 @@
-"use client";
+'use client'
-import { CopyButton } from "@/components/ui/copy-button";
-import * as babel from "prettier/plugins/babel";
-import * as estree from "prettier/plugins/estree";
-import * as yaml from "prettier/plugins/yaml";
-import * as prettier from "prettier/standalone";
-import { type JSX, useLayoutEffect, useState } from "react";
-import type { BundledLanguage } from "shiki/bundle/web";
-import { highlight } from "./shared";
+import { CopyButton } from '@/components/ui/copy-button'
+import * as babel from 'prettier/plugins/babel'
+import * as estree from 'prettier/plugins/estree'
+import * as yaml from 'prettier/plugins/yaml'
+import * as prettier from 'prettier/standalone'
+import { type JSX, useLayoutEffect, useState } from 'react'
+import type { BundledLanguage } from 'shiki/bundle/web'
+import { highlight } from './shared'
interface CodeBlockProps {
- code: string;
- lang: BundledLanguage;
- initial?: JSX.Element;
+ code: string
+ lang: BundledLanguage
+ initial?: JSX.Element
}
async function formatCode(code: string, lang: string) {
try {
- let parser: string;
- let plugins = [] as any[];
+ let parser: string
+ let plugins = [] as any[]
switch (lang.toLowerCase()) {
- case "yaml":
- case "yml":
- parser = "yaml";
- plugins = [yaml];
- break;
- case "javascript":
- case "typescript":
- case "jsx":
- case "tsx":
- parser = "babel-ts";
- plugins = [babel, estree];
- break;
+ case 'yaml':
+ case 'yml':
+ parser = 'yaml'
+ plugins = [yaml]
+ break
+ case 'javascript':
+ case 'typescript':
+ case 'jsx':
+ case 'tsx':
+ parser = 'babel-ts'
+ plugins = [babel, estree]
+ break
default:
- return code;
+ return code
}
const formatted = await prettier.format(code, {
parser,
@@ -43,50 +43,50 @@ async function formatCode(code: string, lang: string) {
tabWidth: 2,
useTabs: false,
printWidth: 120,
- });
- return formatted;
+ })
+ return formatted
} catch (error) {
- console.error("Error formatting code:", error);
- return code;
+ console.error('Error formatting code:', error)
+ return code
}
}
export function CodeBlock({ code, lang, initial }: CodeBlockProps) {
- const [nodes, setNodes] = useState(initial);
- const [formattedCode, setFormattedCode] = useState(code);
+ const [nodes, setNodes] = useState(initial)
+ const [formattedCode, setFormattedCode] = useState(code)
useLayoutEffect(() => {
async function formatAndHighlight() {
try {
- const formatted = await formatCode(code, lang);
- setFormattedCode(formatted);
- const highlighted = await highlight(formatted, lang);
- setNodes(highlighted);
+ const formatted = await formatCode(code, lang)
+ setFormattedCode(formatted)
+ const highlighted = await highlight(formatted, lang)
+ setNodes(highlighted)
} catch (error) {
- const highlighted = await highlight(code, lang);
- setNodes(highlighted);
+ const highlighted = await highlight(code, lang)
+ setNodes(highlighted)
}
}
- void formatAndHighlight();
- }, [code, lang]);
+ void formatAndHighlight()
+ }, [code, lang])
if (!nodes) {
return (
-
- );
+ )
}
return (
-
- );
+ )
}
diff --git a/apps/website/app/blog/[slug]/components/Headings.tsx b/apps/website/app/blog/[slug]/components/Headings.tsx
index cc6ac6d..7385214 100644
--- a/apps/website/app/blog/[slug]/components/Headings.tsx
+++ b/apps/website/app/blog/[slug]/components/Headings.tsx
@@ -1,18 +1,18 @@
-"use client";
+'use client'
-import { useRouter } from "next/navigation";
-import type { DetailedHTMLProps, HTMLAttributes } from "react";
-import slugify from "slugify";
+import { useRouter } from 'next/navigation'
+import type { DetailedHTMLProps, HTMLAttributes } from 'react'
+import slugify from 'slugify'
type HeadingProps = DetailedHTMLProps<
HTMLAttributes
,
HTMLHeadingElement
->;
+>
function LinkIcon() {
return (
- );
+ )
}
export function H1({ children, ...props }: HeadingProps) {
- const router = useRouter();
- const id = slugify(children?.toString() || "", { lower: true, strict: true });
+ const router = useRouter()
+ const id = slugify(children?.toString() || '', {
+ lower: true,
+ strict: true,
+ })
const handleClick = () => {
- router.push(`#${id}`);
- };
+ router.push(`#${id}`)
+ }
return (
{children}
- );
+ )
}
export function H2({ children, ...props }: HeadingProps) {
- const router = useRouter();
- const id = slugify(children?.toString() || "", { lower: true, strict: true });
+ const router = useRouter()
+ const id = slugify(children?.toString() || '', {
+ lower: true,
+ strict: true,
+ })
const handleClick = () => {
- router.push(`#${id}`);
- };
+ router.push(`#${id}`)
+ }
return (
{children}
- );
+ )
}
export function H3({ children, ...props }: HeadingProps) {
- const router = useRouter();
- const id = slugify(children?.toString() || "", { lower: true, strict: true });
+ const router = useRouter()
+ const id = slugify(children?.toString() || '', {
+ lower: true,
+ strict: true,
+ })
const handleClick = () => {
- router.push(`#${id}`);
- };
+ router.push(`#${id}`)
+ }
return (
{children}
- );
+ )
}
diff --git a/apps/website/app/blog/[slug]/components/TableOfContents.tsx b/apps/website/app/blog/[slug]/components/TableOfContents.tsx
index 3bbaac3..099d3db 100644
--- a/apps/website/app/blog/[slug]/components/TableOfContents.tsx
+++ b/apps/website/app/blog/[slug]/components/TableOfContents.tsx
@@ -1,65 +1,67 @@
-"use client";
+'use client'
-import { useEffect, useState } from "react";
+import { useEffect, useState } from 'react'
interface Heading {
- id: string;
- text: string;
- level: number;
+ id: string
+ text: string
+ level: number
}
export function TableOfContents() {
- const [headings, setHeadings] = useState([]);
- const [activeId, setActiveId] = useState();
+ const [headings, setHeadings] = useState([])
+ const [activeId, setActiveId] = useState()
useEffect(() => {
- const elements = Array.from(document.querySelectorAll("h1, h2, h3"))
+ const elements = Array.from(document.querySelectorAll('h1, h2, h3'))
.filter((element) => element.id)
.map((element) => ({
id: element.id,
- text: element.textContent || "",
+ text: element.textContent || '',
level: Number(element.tagName.charAt(1)),
- }));
- setHeadings(elements);
+ }))
+ setHeadings(elements)
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
- setActiveId(entry.target.id);
+ setActiveId(entry.target.id)
}
}
},
- { rootMargin: "-100px 0px -66%" },
- );
+ { rootMargin: '-100px 0px -66%' },
+ )
for (const { id } of elements) {
- const element = document.getElementById(id);
- if (element) observer.observe(element);
+ const element = document.getElementById(id)
+ if (element) observer.observe(element)
}
- return () => observer.disconnect();
- }, []);
+ return () => observer.disconnect()
+ }, [])
return (
- );
+ )
}
diff --git a/apps/website/app/blog/[slug]/components/ZoomableImage.tsx b/apps/website/app/blog/[slug]/components/ZoomableImage.tsx
index 7b97960..cbcc66e 100644
--- a/apps/website/app/blog/[slug]/components/ZoomableImage.tsx
+++ b/apps/website/app/blog/[slug]/components/ZoomableImage.tsx
@@ -1,21 +1,25 @@
-"use client";
+'use client'
-import { cn } from "@/lib/utils";
-import { PhotoProvider, PhotoView } from "react-photo-view";
-import "react-photo-view/dist/react-photo-view.css";
+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;
- className?: string;
+ src: string
+ alt: string
+ className?: string
}
export function ZoomableImage({ src, alt, className }: ZoomableImageProps) {
return (
-
+
- );
+ )
}
diff --git a/apps/website/app/blog/[slug]/components/shared.ts b/apps/website/app/blog/[slug]/components/shared.ts
index 4e6096b..a2daa08 100644
--- a/apps/website/app/blog/[slug]/components/shared.ts
+++ b/apps/website/app/blog/[slug]/components/shared.ts
@@ -1,14 +1,14 @@
-import { toJsxRuntime } from "hast-util-to-jsx-runtime";
-import type { JSX } from "react";
-import { Fragment } from "react";
-import { jsx, jsxs } from "react/jsx-runtime";
-import type { BundledLanguage } from "shiki/bundle/web";
-import { codeToHast } from "shiki/bundle/web";
+import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
+import type { JSX } from 'react'
+import { Fragment } from 'react'
+import { jsx, jsxs } from 'react/jsx-runtime'
+import type { BundledLanguage } from 'shiki/bundle/web'
+import { codeToHast } from 'shiki/bundle/web'
export async function highlight(code: string, lang: BundledLanguage) {
const out = await codeToHast(code, {
lang,
- theme: "houston",
- });
- return toJsxRuntime(out, { Fragment, jsx, jsxs }) as JSX.Element;
+ theme: 'houston',
+ })
+ return toJsxRuntime(out, { Fragment, jsx, jsxs }) as JSX.Element
}
diff --git a/apps/website/app/blog/[slug]/page.tsx b/apps/website/app/blog/[slug]/page.tsx
index 03e63cf..4bad59b 100644
--- a/apps/website/app/blog/[slug]/page.tsx
+++ b/apps/website/app/blog/[slug]/page.tsx
@@ -1,47 +1,47 @@
-import { getPost, getPosts } from "@/lib/ghost";
-import type { Metadata, ResolvingMetadata } from "next";
-import Image from "next/image";
-import Link from "next/link";
-import { notFound } from "next/navigation";
-import type React from "react";
-import ReactMarkdown from "react-markdown";
-import type { Components } from "react-markdown";
-import rehypeRaw from "rehype-raw";
-import remarkGfm from "remark-gfm";
-import remarkToc from "remark-toc";
-import type { BundledLanguage } from "shiki/bundle/web";
-import TurndownService from "turndown";
+import { getPost, getPosts } from '@/lib/ghost'
+import type { Metadata, ResolvingMetadata } from 'next'
+import Image from 'next/image'
+import Link from 'next/link'
+import { notFound } from 'next/navigation'
+import type React from 'react'
+import ReactMarkdown from 'react-markdown'
+import type { Components } from 'react-markdown'
+import rehypeRaw from 'rehype-raw'
+import remarkGfm from 'remark-gfm'
+import remarkToc from 'remark-toc'
+import type { BundledLanguage } from 'shiki/bundle/web'
+import TurndownService from 'turndown'
// @ts-ignore
-import * as turndownPluginGfm from "turndown-plugin-gfm";
-import { CodeBlock } from "./components/CodeBlock";
-import { H1, H2, H3 } from "./components/Headings";
-import { TableOfContents } from "./components/TableOfContents";
-import { ZoomableImage } from "./components/ZoomableImage";
+import * as turndownPluginGfm from 'turndown-plugin-gfm'
+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: { slug: string };
-};
+ params: { slug: string }
+}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata,
): Promise {
- const { slug } = await params;
- const post = await getPost(slug);
+ const { slug } = await params
+ const post = await getPost(slug)
if (!post) {
return {
- title: "Post Not Found",
- };
+ title: 'Post Not Found',
+ }
}
const ogUrl = new URL(
`/api/og`,
- process.env.NODE_ENV === "production"
- ? "https://dokploy.com"
- : "http://localhost:3000",
- );
- ogUrl.searchParams.set("slug", slug);
+ process.env.NODE_ENV === 'production'
+ ? 'https://dokploy.com'
+ : 'http://localhost:3000',
+ )
+ ogUrl.searchParams.set('slug', slug)
return {
title: post.title,
@@ -49,7 +49,7 @@ export async function generateMetadata(
openGraph: {
title: post.title,
description: post.custom_excerpt || post.excerpt,
- type: "article",
+ type: 'article',
url: `${process.env.NEXT_PUBLIC_APP_URL}/blog/${post.slug}`,
images: [
{
@@ -61,66 +61,66 @@ export async function generateMetadata(
],
},
twitter: {
- card: "summary_large_image",
+ card: 'summary_large_image',
title: post.title,
description: post.custom_excerpt || post.excerpt,
images: [ogUrl.toString()],
},
- };
+ }
}
export default async function BlogPostPage({ params }: Props) {
- const { slug } = await params;
- const post = await getPost(slug);
- const allPosts = await getPosts();
+ const { slug } = await params
+ const post = await getPost(slug)
+ const allPosts = await getPosts()
- const relatedPosts = allPosts.filter((p) => p.id !== post?.id).slice(0, 3);
+ const relatedPosts = allPosts.filter((p) => p.id !== post?.id).slice(0, 3)
if (!post) {
- notFound();
+ notFound()
}
const cleanHtml = (html: string) => {
- if (typeof window !== "undefined") {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, "text/html");
+ if (typeof window !== 'undefined') {
+ const parser = new DOMParser()
+ const doc = parser.parseFromString(html, 'text/html')
const scripts = doc.querySelectorAll(
'script[type="application/ld+json"], script',
- );
- scripts.forEach((script) => script.remove());
- const unwantedElements = doc.querySelectorAll("style, meta, link");
- unwantedElements.forEach((el) => el.remove());
- return doc.body.innerHTML;
+ )
+ scripts.forEach((script) => script.remove())
+ const unwantedElements = doc.querySelectorAll('style, meta, link')
+ unwantedElements.forEach((el) => el.remove())
+ return doc.body.innerHTML
} else {
return html
.replace(
/