Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app): add dynamic open-graph image #509

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@next/font": "13.1.1",
"@tanstack/react-query": "4.20.4",
"@tanstack/react-query-devtools": "4.20.4",
"@vercel/og": "0.0.27",
"client-only": "0.0.1",
"easymde": "2.18.0",
"flagsmith": "3.15.1",
Expand Down
25 changes: 25 additions & 0 deletions apps/app/public/icons/og/angularjs-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions apps/app/public/icons/og/css3-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions apps/app/public/icons/og/git-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions apps/app/public/icons/og/html5-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions apps/app/public/icons/og/javascript-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions apps/app/public/icons/og/reactjs-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Params } from "../../../../../types";

export default function Head({ params: { technology } }: { params: Params<"technology"> }) {
const isValid = validateTechnology(technology);

if (!isValid) {
return <HeadTags title="" />;
}
Expand All @@ -17,5 +18,5 @@ export default function Head({ params: { technology } }: { params: Params<"techn

const suffix = longLabel ? `${longLabel} (${label})` : label;

return <HeadTags title={`Pytania ${suffix}`} />;
return <HeadTags title={`Pytania ${suffix}`} og={{ technology }} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,11 @@ export default async function Head({ params }: { params: Params<"questionId"> })
const textForTitle = await stripMarkdown(data.question, { stripCode: true });
const textForDescription = await stripMarkdown(data.question, { stripCode: false });

return <HeadTags title={textForTitle} description={textForDescription} />;
return (
<HeadTags
title={textForTitle}
description={textForDescription}
og={{ questionId: questionId.toString() }}
/>
);
}
14 changes: 13 additions & 1 deletion apps/app/src/components/HeadTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { hellip } from "../utils/utils";
type HeadTagsProps = Readonly<{
title?: string;
description?: string;
og?: {
technology?: string;
questionId?: string;
levels?: string;
};
}>;

const titleSuffix = ` • DevFAQ.pl`;
Expand All @@ -12,11 +17,18 @@ const maxDescriptionLength = 160;
export const HeadTags = ({
title = "",
description = "DevFAQ.pl — największa baza pytań z programowania tworzona przez społeczność. DevFAQ.pl jest serwisem internetowym służącym do udostępniania i wymiany pytań rekrutacyjnych na stanowiska developerów.",
og,
}: HeadTagsProps) => {
const shortTitle = hellip(title, maxTitleLength);
const shortDescription = hellip(description, maxDescriptionLength);

const formattedShortTitle = shortTitle.trim() ? `${shortTitle}${titleSuffix}` : `DevFAQ.pl`;
const ogParams = new URLSearchParams(og);
const APP_URL = process.env.NEXT_PUBLIC_APP_URL;

if (!APP_URL) {
throw new Error(`Missing NEXT_PUBLIC_APP_URL!`);
}

return (
<>
Expand All @@ -27,7 +39,7 @@ export const HeadTags = ({
<meta
property="og:image"
itemProp="logo image"
content="https://app.devfaq.pl/img/devfaq-cover-facebook.png"
content={`${APP_URL}/api/og?${ogParams.toString()}`}
/>
<meta property="og:site_name" content="DevFAQ.pl" />
<meta property="fb:app_id" content="2005583769700691" />
Expand Down
60 changes: 49 additions & 11 deletions apps/app/src/components/TechnologyIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,62 @@ import AngularLogo from "../../public/icons/angularjs-logo.svg";
import ReactLogo from "../../public/icons/reactjs-logo.svg";
import GitLogo from "../../public/icons/git-logo.svg";
import OtherLogo from "../../public/icons/other-logo.svg";
import OGHTMLLogo from "../../public/icons/og/html5-logo.svg";
import OGCSSLogo from "../../public/icons/og/css3-logo.svg";
import OGJavaScriptLogo from "../../public/icons/og/javascript-logo.svg";
import OGAngularLogo from "../../public/icons/og/angularjs-logo.svg";
import OGReactLogo from "../../public/icons/og/reactjs-logo.svg";
import OGGitLogo from "../../public/icons/og/git-logo.svg";

const icons: Record<Technology, ComponentType<HTMLAttributes<HTMLElement>>> = {
html: HTMLLogo,
css: CSSLogo,
js: JavaScriptLogo,
angular: AngularLogo,
react: ReactLogo,
git: GitLogo,
other: OtherLogo,
const icons: Record<
Technology,
{
normal: ComponentType<HTMLAttributes<HTMLElement>>;
og?: ComponentType<HTMLAttributes<HTMLElement>>;
}
> = {
html: {
normal: HTMLLogo,
og: OGHTMLLogo,
},
css: {
normal: CSSLogo,
og: OGCSSLogo,
},
js: {
normal: JavaScriptLogo,
og: OGJavaScriptLogo,
},
angular: {
normal: AngularLogo,
og: OGAngularLogo,
},
react: {
normal: ReactLogo,
og: OGReactLogo,
},
git: {
normal: GitLogo,
og: OGGitLogo,
},
other: {
normal: OtherLogo,
},
};

type TechnologyIconProps = Readonly<{
technology: Technology;
isOG?: boolean;
className?: string;
tw?: string;
}>;

export const TechnologyIcon = ({ technology, className }: TechnologyIconProps) => {
const Icon = icons[technology];
export const TechnologyIcon = ({ technology, isOG, className, tw }: TechnologyIconProps) => {
const Icon = icons[technology][isOG ? "og" : "normal"];

return <Icon className={className} />;
if (!Icon) {
return null;
}

return <Icon className={className} tw={tw} />;
};
107 changes: 107 additions & 0 deletions apps/app/src/pages/api/og.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { NextRequest } from "next/server";
import { ImageResponse } from "@vercel/og";
import { ReactNode } from "react";
import type { ImageResponseOptions } from "@vercel/og";
import { twMerge } from "tailwind-merge";
import Logo from "../../../public/devfaq-logo.svg";
import { TechnologyIcon } from "../../components/TechnologyIcon";
import { validateTechnology } from "../../lib/technologies";
import type { Technology } from "../../lib/technologies";
import { getQuestionById } from "../../services/questions.service";
import { Level, parseQueryLevels } from "../../lib/level";

export const config = {
runtime: "experimental-edge",
};

const technologies: Technology[] = ["js", "html", "css", "react", "angular", "git"];

const Wrapper = ({ children }: { readonly children: ReactNode }) => (
<div tw="h-full w-full bg-[#6737b8] flex items-center justify-center">
<Logo tw="w-[1280px] h-[265px]" />
{children}
</div>
);

const QuestionLevel = ({ level, tw }: { level: Level; tw?: string }) => (
<span
tw={twMerge(
"text-6xl w-72 h-24 rounded-full capitalize flex items-center justify-center text-white",
level === "junior" && "bg-[#439fff]",
level === "mid" && "bg-[#26be2a]",
level === "senior" && "bg-[#fbba00]",
tw,
)}
>
{level}
</span>
);

export default async function handler(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const options: ImageResponseOptions = { width: 2400, height: 1350 };

const technology = searchParams.get("technology");
const questionId = Number.parseInt(searchParams.get("questionId") || "");

if (technology && validateTechnology(technology)) {
const levels = parseQueryLevels(searchParams.get("levels"));

return new ImageResponse(
(
<Wrapper>
<div tw="flex flex-col items-center ml-32">
<TechnologyIcon technology={technology} tw="w-56 h-56 mb-12" />
{levels &&
levels.map((level) => <QuestionLevel key={level} level={level} tw="mb-4" />)}
</div>
</Wrapper>
),
options,
);
}

if (!Number.isNaN(questionId)) {
try {
const {
data: {
data: { question, _categoryId, _levelId },
},
} = await getQuestionById({ id: questionId });

return new ImageResponse(
(
<Wrapper>
<div tw="flex flex-col items-center ml-14">
<h1 tw="text-6xl text-white text-center font-bold max-w-[850px] mb-14">
{question}
</h1>
<TechnologyIcon technology={_categoryId} tw="w-48 h-48 mb-14" />
<QuestionLevel level={_levelId} />
</div>
</Wrapper>
),
options,
);
} catch (_err) {}
}

return new ImageResponse(
(
<Wrapper>
<div tw="flex flex-wrap max-w-[960px] ml-12">
{technologies.map((technology) => (
<TechnologyIcon key={technology} technology={technology} isOG tw="w-56 h-56 m-12" />
))}
</div>
</Wrapper>
),
options,
);
} catch (err) {
return new Response("Failed to generate the image", {
status: 500,
});
}
}
Loading