feat: add dynamic open graph metadata for enterprise whitelabeling

This commit is contained in:
Yash Kumar 2026-09-04 20:56:23 +05:30
parent 7fb02abb3e
commit b5c3d8660e
8 changed files with 9223 additions and 36 deletions

View File

@ -47,8 +47,8 @@ const formSchema = z.object({
docsUrl: safeUrlField,
errorPageTitle: z.string(),
errorPageDescription: z.string(),
metaTitle: z.string(),
footerText: z.string(),
ogImageUrl: safeUrlField,
});
type FormSchema = z.infer<typeof formSchema>;
@ -193,8 +193,8 @@ export function WhitelabelingSettings() {
docsUrl: "",
errorPageTitle: "",
errorPageDescription: "",
metaTitle: "",
footerText: "",
ogImageUrl: "",
},
resolver: zodResolver(formSchema),
});
@ -212,8 +212,8 @@ export function WhitelabelingSettings() {
docsUrl: data.docsUrl ?? "",
errorPageTitle: data.errorPageTitle ?? "",
errorPageDescription: data.errorPageDescription ?? "",
metaTitle: data.metaTitle ?? "",
footerText: data.footerText ?? "",
ogImageUrl: data.ogImageUrl ?? "",
});
}
}, [data, form]);
@ -242,8 +242,8 @@ export function WhitelabelingSettings() {
docsUrl: values.docsUrl || null,
errorPageTitle: values.errorPageTitle || null,
errorPageDescription: values.errorPageDescription || null,
metaTitle: values.metaTitle || null,
footerText: values.footerText || null,
ogImageUrl: values.ogImageUrl || null,
},
})
.then(async () => {
@ -388,6 +388,26 @@ export function WhitelabelingSettings() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="ogImageUrl"
render={({ field }) => (
<FormItem>
<FormLabel>OG Image URL</FormLabel>
<FormControl>
<Input
placeholder="https://example.com/og.png"
{...field}
/>
</FormControl>
<FormDescription>
Open Graph image used for link previews on social media and messaging platforms. Recommended size: 1200x630px.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
@ -441,31 +461,16 @@ export function WhitelabelingSettings() {
</CardContent>
</Card>
{/* Metadata & Links Section */}
{/* Links Section */}
<Card className="bg-transparent">
<CardHeader>
<CardTitle>Metadata & Links</CardTitle>
<CardTitle>Links</CardTitle>
<CardDescription>
Customize the page title, footer text, and sidebar links.
Customize the footer text and sidebar links.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<FormField
control={form.control}
name="metaTitle"
render={({ field }) => (
<FormItem>
<FormLabel>Page Title</FormLabel>
<FormControl>
<Input placeholder="Dokploy" {...field} />
</FormControl>
<FormDescription>
Browser tab title. Defaults to "Dokploy" if empty.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}

View File

@ -0,0 +1 @@
ALTER TABLE "webServerSettings" ALTER COLUMN "whitelabelingConfig" SET DEFAULT '{"appName":null,"appDescription":null,"logoUrl":null,"faviconUrl":null,"customCss":null,"loginLogoUrl":null,"supportUrl":null,"docsUrl":null,"errorPageTitle":null,"errorPageDescription":null,"footerText":null,"ogImageUrl":null}'::jsonb;

File diff suppressed because it is too large Load Diff

View File

@ -1338,6 +1338,13 @@
"when": 1788300390648,
"tag": "0190_nappy_anita_blake",
"breakpoints": true
},
{
"idx": 191,
"version": "7",
"when": 1788534770506,
"tag": "0191_aberrant_energizer",
"breakpoints": true
}
]
}

View File

@ -9,17 +9,25 @@ import NextDocument, {
} from "next/document";
interface WhitelabelingDocumentProps {
metaTitle: string | null;
appName: string | null;
appDescription: string | null;
ogImageUrl: string | null;
faviconHref: string | null;
customCss: string | null;
}
export default function Document({
metaTitle,
appName,
appDescription,
ogImageUrl,
faviconHref,
customCss,
}: WhitelabelingDocumentProps) {
const title = metaTitle || "Dokploy";
const title = appName || "Dokploy";
const description =
appDescription || "The Open Source alternative to Netlify, Vercel, Heroku.";
const ogImage = ogImageUrl || "/og.png";
return (
<Html lang="en" className="font-sans">
<Head>
@ -27,6 +35,9 @@ export default function Document({
paint (and for social scrapers), avoiding a flash of / fallback to
the default Dokploy branding. */}
<title>{title}</title>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<link rel="icon" href={faviconHref || "/icon.svg"} />
{customCss && (
<style
@ -48,7 +59,9 @@ const SETTINGS_CACHE_TTL = 60 * 1000; // 1 minute
declare global {
var __SETTINGS_CACHE: {
data: {
metaTitle: string | null;
appName: string | null;
appDescription: string | null;
ogImageUrl: string | null;
faviconHref: string | null;
customCss: string | null;
};
@ -61,7 +74,9 @@ Document.getInitialProps = async (
): Promise<DocumentInitialProps & WhitelabelingDocumentProps> => {
const initialProps = await NextDocument.getInitialProps(ctx);
let metaTitle: string | null = null;
let appName: string | null = null;
let appDescription: string | null = null;
let ogImageUrl: string | null = null;
let faviconHref: string | null = null;
let customCss: string | null = null;
@ -79,7 +94,9 @@ Document.getInitialProps = async (
try {
const config = await getPublicWhitelabelingConfig();
if (config) {
metaTitle = config.metaTitle;
appName = config.appName;
appDescription = config.appDescription;
ogImageUrl = config.ogImageUrl;
// Remove any </style> tags to prevent XSS breakout
customCss = config.customCss
? config.customCss.replace(/<\/\s*style[^>]*>/gi, "")
@ -92,7 +109,9 @@ Document.getInitialProps = async (
globalThis.__SETTINGS_CACHE = {
data: {
metaTitle,
appName,
appDescription,
ogImageUrl,
faviconHref,
customCss,
},
@ -101,7 +120,9 @@ Document.getInitialProps = async (
return {
...initialProps,
metaTitle,
appName,
appDescription,
ogImageUrl,
faviconHref,
customCss,
};

BIN
apps/dokploy/public/og.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

View File

@ -86,8 +86,8 @@ export const webServerSettings = pgTable("webServerSettings", {
docsUrl: string | null;
errorPageTitle: string | null;
errorPageDescription: string | null;
metaTitle: string | null;
footerText: string | null;
ogImageUrl: string | null;
}>()
.default({
appName: null,
@ -100,8 +100,8 @@ export const webServerSettings = pgTable("webServerSettings", {
docsUrl: null,
errorPageTitle: null,
errorPageDescription: null,
metaTitle: null,
footerText: null,
ogImageUrl: null,
}),
// Deployment Configuration (self-hosted only)
remoteServersOnly: boolean("remoteServersOnly").notNull().default(false),
@ -223,8 +223,8 @@ export const whitelabelingConfigSchema = z.object({
docsUrl: safeUrl,
errorPageTitle: z.string().nullable(),
errorPageDescription: z.string().nullable(),
metaTitle: z.string().nullable(),
footerText: z.string().nullable(),
ogImageUrl: safeUrl,
});
export const apiUpdateWhitelabeling = z.object({

View File

@ -10,7 +10,7 @@ export interface PublicWhitelabelingConfig {
loginLogoUrl: string | null;
faviconUrl: string | null;
customCss: string | null;
metaTitle: string | null;
ogImageUrl: string | null;
errorPageTitle: string | null;
errorPageDescription: string | null;
footerText: string | null;
@ -50,7 +50,7 @@ export const getPublicWhitelabelingConfig =
loginLogoUrl: config.loginLogoUrl,
faviconUrl: config.faviconUrl,
customCss: config.customCss,
metaTitle: config.metaTitle,
ogImageUrl: config.ogImageUrl,
errorPageTitle: config.errorPageTitle,
errorPageDescription: config.errorPageDescription,
footerText: config.footerText,