feat: Use team logos for various meta icons (#8955)
* Use team logos for various meta icons * Automatically resize team icons for different uses * Fix api/logo error when unable to find a team * Avoid loading image-optimizer in api/logo when not needed --------- Co-authored-by: Omar López <zomars@me.com> Co-authored-by: alannnc <alannnc@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
This commit is contained in:
parent
7c46d1b348
commit
ee9b3eccaf
|
@ -40,9 +40,9 @@ class MyDocument extends Document<Props> {
|
|||
return (
|
||||
<Html lang={locale}>
|
||||
<Head nonce={nonce}>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/api/logo?type=apple-touch-icon" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/api/logo?type=favicon-32" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/api/logo?type=favicon-16" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
||||
<meta name="msapplication-TileColor" content="#ff0000" />
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { IS_SELF_HOSTED, LOGO, LOGO_ICON, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import {
|
||||
ANDROID_CHROME_ICON_192,
|
||||
ANDROID_CHROME_ICON_256,
|
||||
APPLE_TOUCH_ICON,
|
||||
FAVICON_16,
|
||||
FAVICON_32,
|
||||
IS_SELF_HOSTED,
|
||||
LOGO,
|
||||
LOGO_ICON,
|
||||
MSTILE_ICON,
|
||||
WEBAPP_URL,
|
||||
} from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["[api/logo]"] });
|
||||
|
@ -20,11 +31,79 @@ function extractSubdomainAndDomain(hostname: string) {
|
|||
}
|
||||
|
||||
const logoApiSchema = z.object({
|
||||
icon: z.coerce.boolean().optional(),
|
||||
type: z.coerce.string().optional(),
|
||||
});
|
||||
|
||||
const SYSTEM_SUBDOMAINS = ["console", "app", "www"];
|
||||
|
||||
type LogoType =
|
||||
| "logo"
|
||||
| "icon"
|
||||
| "favicon-16"
|
||||
| "favicon-32"
|
||||
| "apple-touch-icon"
|
||||
| "mstile"
|
||||
| "android-chrome-192"
|
||||
| "android-chrome-256";
|
||||
|
||||
type LogoTypeDefinition = {
|
||||
fallback: string;
|
||||
w?: number;
|
||||
h?: number;
|
||||
source: "appLogo" | "appIconLogo";
|
||||
};
|
||||
|
||||
const logoDefinitions: Record<LogoType, LogoTypeDefinition> = {
|
||||
logo: {
|
||||
fallback: `${WEBAPP_URL}${LOGO}`,
|
||||
source: "appLogo",
|
||||
},
|
||||
icon: {
|
||||
fallback: `${WEBAPP_URL}${LOGO_ICON}`,
|
||||
source: "appIconLogo",
|
||||
},
|
||||
"favicon-16": {
|
||||
fallback: `${WEBAPP_URL}${FAVICON_16}`,
|
||||
w: 16,
|
||||
h: 16,
|
||||
source: "appIconLogo",
|
||||
},
|
||||
"favicon-32": {
|
||||
fallback: `${WEBAPP_URL}${FAVICON_32}`,
|
||||
w: 32,
|
||||
h: 32,
|
||||
source: "appIconLogo",
|
||||
},
|
||||
"apple-touch-icon": {
|
||||
fallback: `${WEBAPP_URL}${APPLE_TOUCH_ICON}`,
|
||||
w: 180,
|
||||
h: 180,
|
||||
source: "appLogo",
|
||||
},
|
||||
mstile: {
|
||||
fallback: `${WEBAPP_URL}${MSTILE_ICON}`,
|
||||
w: 150,
|
||||
h: 150,
|
||||
source: "appLogo",
|
||||
},
|
||||
"android-chrome-192": {
|
||||
fallback: `${WEBAPP_URL}${ANDROID_CHROME_ICON_192}`,
|
||||
w: 192,
|
||||
h: 192,
|
||||
source: "appLogo",
|
||||
},
|
||||
"android-chrome-256": {
|
||||
fallback: `${WEBAPP_URL}${ANDROID_CHROME_ICON_256}`,
|
||||
w: 256,
|
||||
h: 256,
|
||||
source: "appLogo",
|
||||
},
|
||||
};
|
||||
|
||||
function isValidLogoType(type: string): type is LogoType {
|
||||
return type in logoDefinitions;
|
||||
}
|
||||
|
||||
async function getTeamLogos(subdomain: string) {
|
||||
try {
|
||||
if (
|
||||
|
@ -39,7 +118,7 @@ async function getTeamLogos(subdomain: string) {
|
|||
}
|
||||
// load from DB
|
||||
const { default: prisma } = await import("@calcom/prisma");
|
||||
const team = await prisma.team.findUniqueOrThrow({
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
slug: subdomain,
|
||||
},
|
||||
|
@ -48,16 +127,16 @@ async function getTeamLogos(subdomain: string) {
|
|||
appIconLogo: true,
|
||||
},
|
||||
});
|
||||
// try to use team logos, otherwise default to LOGO/LOGO_ICON regardless
|
||||
|
||||
return {
|
||||
appLogo: team.appLogo || `${WEBAPP_URL}${LOGO}`,
|
||||
appIconLogo: team.appIconLogo || `${WEBAPP_URL}${LOGO_ICON}`,
|
||||
appLogo: team?.appLogo,
|
||||
appIconLogo: team?.appIconLogo,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) log.debug(error.message);
|
||||
return {
|
||||
appLogo: `${WEBAPP_URL}${LOGO}`,
|
||||
appIconLogo: `${WEBAPP_URL}${LOGO_ICON}`,
|
||||
appLogo: undefined,
|
||||
appIconLogo: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -75,14 +154,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
if (!domains) throw new Error("No domains");
|
||||
|
||||
const [subdomain] = domains;
|
||||
const { appLogo, appIconLogo } = await getTeamLogos(subdomain);
|
||||
const teamLogos = await getTeamLogos(subdomain);
|
||||
|
||||
const filteredLogo = parsedQuery?.icon ? appIconLogo : appLogo;
|
||||
// Resolve all icon types to team logos, falling back to Cal.com defaults.
|
||||
const type: LogoType = parsedQuery?.type && isValidLogoType(parsedQuery.type) ? parsedQuery.type : "logo";
|
||||
const logoDefinition = logoDefinitions[type];
|
||||
const filteredLogo = teamLogos[logoDefinition.source] ?? logoDefinition.fallback;
|
||||
|
||||
try {
|
||||
const response = await fetch(filteredLogo);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
let buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// If we need to resize the team logos (via Next.js' built-in image processing)
|
||||
if (teamLogos[logoDefinition.source] && logoDefinition.w) {
|
||||
const { detectContentType, optimizeImage } = await import("next/dist/server/image-optimizer");
|
||||
buffer = await optimizeImage({
|
||||
buffer,
|
||||
contentType: detectContentType(buffer) ?? "image/jpeg",
|
||||
quality: 100,
|
||||
width: logoDefinition.w,
|
||||
height: logoDefinition.h, // optional
|
||||
});
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", response.headers.get("content-type") as string);
|
||||
res.setHeader("Cache-Control", "s-maxage=86400");
|
||||
res.send(buffer);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<square150x150logo src="/api/logo?type=mstile"/>
|
||||
<TileColor>#ff0000</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
"short_name": "Cal.com",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"src": "/api/logo?type=android-chrome-192",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-256x256.png",
|
||||
"src": "/api/logo?type=android-chrome-256",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
|
|
@ -40,6 +40,12 @@ export const HOSTED_CAL_FEATURES = process.env.NEXT_PUBLIC_HOSTED_CAL_FEATURES |
|
|||
export const NEXT_PUBLIC_BASE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL || `https://${process.env.VERCEL_URL}`;
|
||||
export const LOGO = "/calcom-logo-white-word.svg";
|
||||
export const LOGO_ICON = "/cal-com-icon-white.svg";
|
||||
export const FAVICON_16 = "/favicon-16x16.png";
|
||||
export const FAVICON_32 = "/favicon-32x32.png";
|
||||
export const APPLE_TOUCH_ICON = "/apple-touch-icon.png";
|
||||
export const MSTILE_ICON = "/mstile-150x150.png";
|
||||
export const ANDROID_CHROME_ICON_192 = "/android-chrome-192x192.png";
|
||||
export const ANDROID_CHROME_ICON_256 = "/android-chrome-256x256.png";
|
||||
export const ROADMAP = "https://cal.com/roadmap";
|
||||
export const DESKTOP_APP_LINK = "https://cal.com/download";
|
||||
export const JOIN_SLACK = "https://cal.com/slack";
|
||||
|
|
|
@ -15,7 +15,7 @@ export default function Logo({
|
|||
<h3 className={classNames("logo", inline && "inline", className)}>
|
||||
<strong>
|
||||
{icon ? (
|
||||
<img className="mx-auto w-9 dark:invert" alt="Cal" title="Cal" src="/api/logo?icon=1" />
|
||||
<img className="mx-auto w-9 dark:invert" alt="Cal" title="Cal" src="/api/logo?type=icon" />
|
||||
) : (
|
||||
<img
|
||||
className={classNames(small ? "h-4 w-auto" : "h-5 w-auto", "dark:invert")}
|
||||
|
|
Loading…
Reference in New Issue
Block a user