Skip to content
Merged
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
26 changes: 19 additions & 7 deletions app/(app)/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,31 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
}

const { bio, name } = profile;
const title = `@${username} ${name ? `(${name}) -` : " -"} Codú`;

const description = `Read writing from ${name}. ${bio}`;
const title = `${name || username} - Codú Profile | Codú - The Web Developer Community`;
const description = `${name || username}'s profile on Codú. ${bio ? `Bio: ${bio}` : "View their posts and contributions."}`;

return {
title,
description,
openGraph: {
title,
description,
type: "article",
images: [`/api/og?title=${encodeURIComponent(`${name} on Codú`)}`],
type: "profile",
images: [
{
url: "/images/og/home-og.png",
width: 1200,
height: 630,
alt: `${name || username}'s profile on Codú`,
},
],
siteName: "Codú",
},
twitter: {
card: "summary_large_image",
title,
description,
images: [`/api/og?title=${encodeURIComponent(`${name} on Codú`)}`],
images: ["/images/og/home-og.png"],
},
};
}
Expand Down Expand Up @@ -108,6 +117,9 @@ export default async function Page({
};

return (
<Content profile={shapedProfile} isOwner={isOwner} session={session} />
<>
<h1 className="sr-only">{`${shapedProfile.name || shapedProfile.username}'s Coding Profile`}</h1>
<Content profile={shapedProfile} isOwner={isOwner} session={session} />
</>
);
}
8 changes: 7 additions & 1 deletion app/(app)/articles/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
openGraph: {
description: post.excerpt,
type: "article",
images: [`/og?title=${encodeURIComponent(post.title)}`],
images: [
`/og?title=${encodeURIComponent(
post.title,
)}&readTime=${post.readTimeMins}&author=${encodeURIComponent(
post.user.name,
)}&date=${post.updatedAt}`,
],
Comment on lines +41 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

⚠️ Potential issue

readTimeMins is properly defined and usage is safe.

The readTimeMins property is consistently defined in the codebase, ensuring that its usage does not require additional null checks. This addresses and resolves the concern about potential undefined errors.

However, there is still an inconsistency between the openGraph.images and twitter.images properties that needs to be addressed to maintain metadata consistency across platforms.

🔗 Analysis chain

Improved Open Graph metadata with article details.

The addition of readTime, author, and date to the Open Graph image URL enhances the metadata, aligning with the PR objective. Good use of encodeURIComponent for proper URL encoding.

However, there are two points to consider:

  1. The twitter.images property hasn't been updated to match the new format used in openGraph.images. This could lead to inconsistency in metadata across platforms.

Consider updating the twitter.images property to match:

twitter: {
  description: post.excerpt,
  images: [`/og?title=${encodeURIComponent(post.title)}&readTime=${post.readTimeMins}&author=${encodeURIComponent(post.user.name)}&date=${post.updatedAt}`],
},
  1. The readTimeMins property is used without checking if it exists, which could potentially lead to errors if the property is undefined.

Consider adding a null check or providing a default value:

readTime=${post.readTimeMins ?? 0}

Let's verify the existence and type of the readTimeMins property:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for the existence and usage of readTimeMins property in the codebase

# Test 1: Check if readTimeMins is defined in any type definitions
echo "Checking for readTimeMins in type definitions:"
rg --type typescript "readTimeMins.*:" -g "!*.test.ts" -g "!*.spec.ts"

# Test 2: Check for usage of readTimeMins in the codebase
echo "\nChecking for usage of readTimeMins:"
rg --type typescript "\.readTimeMins" -g "!*.test.ts" -g "!*.spec.ts"

Length of output: 402


Script:

#!/bin/bash
# Description: Check for the existence and usage of readTimeMins property in the codebase

# Test 1: Check if readTimeMins is defined in any type definitions
echo "Checking for readTimeMins in type definitions:"
rg "readTimeMins.*:" -g "!*.test.ts" -g "!*.spec.ts"

# Test 2: Check for usage of readTimeMins in the codebase
echo "\nChecking for usage of readTimeMins:"
rg "\.readTimeMins" -g "!*.test.ts" -g "!*.spec.ts"

Length of output: 1935

siteName: "Codú",
},
twitter: {
Expand Down
243 changes: 146 additions & 97 deletions app/og/route.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { ImageResponse } from "next/og";
import * as Sentry from "@sentry/nextjs";
import { Stars, Waves } from "@/components/background/background";

export const runtime = "edge";

const height = 630;
Expand All @@ -8,153 +11,199 @@ export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const origin = `${request.headers.get("x-forwarded-proto") || "http"}://${request.headers.get("host")}`;
// ?title=<title>
const hasTitle = searchParams.has("title");
const title = hasTitle
? searchParams.get("title")?.slice(0, 100)
: "My default title";

const fontData = await fetch(
new URL(
"https://og-playground.vercel.app/inter-latin-ext-700-normal.woff",
import.meta.url,
),
const title = searchParams.get("title");
const author = searchParams.get("author");
const readTime = searchParams.get("readTime");
const date = searchParams.get("date");

if (!title || !author || !readTime || !date) {
throw new Error("Missing required parameters");
}

const regularFontData = await fetch(
new URL("@/assets/Lato-Regular.ttf", import.meta.url),
).then((res) => res.arrayBuffer());

const boldFontData = await fetch(
new URL("@/assets/Lato-Bold.ttf", import.meta.url),
).then((res) => res.arrayBuffer());

Comment on lines +24 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Suggestion: Fetch font data in parallel to improve performance

Currently, the font data for regular and bold fonts is fetched sequentially, which can lead to increased response times. Fetching both fonts in parallel will optimize performance by reducing the total fetch time.

Apply this diff to fetch font data in parallel:

-    const regularFontData = await fetch(
-      new URL("@/assets/Lato-Regular.ttf", import.meta.url),
-    ).then((res) => res.arrayBuffer());
-
-    const boldFontData = await fetch(
-      new URL("@/assets/Lato-Bold.ttf", import.meta.url),
-    ).then((res) => res.arrayBuffer());
+    const [regularFontData, boldFontData] = await Promise.all([
+      fetch(new URL("@/assets/Lato-Regular.ttf", import.meta.url)).then((res) => res.arrayBuffer()),
+      fetch(new URL("@/assets/Lato-Bold.ttf", import.meta.url)).then((res) => res.arrayBuffer()),
+    ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const regularFontData = await fetch(
new URL("@/assets/Lato-Regular.ttf", import.meta.url),
).then((res) => res.arrayBuffer());
const boldFontData = await fetch(
new URL("@/assets/Lato-Bold.ttf", import.meta.url),
).then((res) => res.arrayBuffer());
const [regularFontData, boldFontData] = await Promise.all([
fetch(new URL("@/assets/Lato-Regular.ttf", import.meta.url)).then((res) => res.arrayBuffer()),
fetch(new URL("@/assets/Lato-Bold.ttf", import.meta.url)).then((res) => res.arrayBuffer()),
]);

return new ImageResponse(
(
<div
tw="flex flex-col h-full w-full justify-center"
tw="flex flex-col h-full w-full"
style={{
padding: "0 114px",
fontFamily: "'Lato'",
backgroundColor: "#1d1b36",
backgroundRepeat: "repeat",
background: `
url('${origin}/images/og/noise.svg'),
radial-gradient(circle at bottom left, rgba(255, 255, 255, 0.1), transparent 40%),
radial-gradient(circle at top right, rgba(255, 255, 255, 0.1), transparent 40%)
`,
backgroundImage: `
url('${origin}/images/og/noise.png'),
radial-gradient(circle at top left, rgba(255, 255, 255, 0.15), transparent 40%),
radial-gradient(circle at top right, rgba(255, 255, 255, 0.15), transparent 40%)
`,
backgroundRepeat: "repeat, no-repeat, no-repeat",
backgroundSize: "100px 100px, 100% 100%, 100% 100%",
}}
>
<div
className="line1"
<Waves
style={{
width: "1200px",
height: "30px",
borderTop: "1px solid #39374E",
borderBottom: "1px solid #39374E",
position: "absolute",
top: "50px",
top: 0,
left: 0,
width,
height,
}}
></div>
<div
className="line2"
/>
<Stars
style={{
width: "1200px",
height: "30px",
borderTop: "1px solid #39374E",
borderBottom: "1px solid #39374E",
position: "absolute",
bottom: "50px",
top: 0,
left: 0,
width,
height,
}}
></div>
/>
<div
className="line3"
style={{
width: "30px",
height: "100%",
borderRight: "1px solid #39374E",
position: "absolute",
left: "50px",
top: "50px",
left: 0,
right: 0,
borderTop: "1px solid rgba(255, 255, 255, 0.1)",
}}
></div>
/>
<div
className="line4"
style={{
width: "30px",
height: "100%",
borderLeft: "1px solid #39374E",
position: "absolute",
right: "50px",
}}
></div>
<img
alt="waves"
src={`${origin}/images/og/waves.svg`}
style={{
position: "absolute",
top: "0",
left: "0",
width: "1200",
height: "630",
bottom: "50px",
left: 0,
right: 0,
borderBottom: "1px solid rgba(255, 255, 255, 0.1)",
}}
/>
<img
alt="stars"
src={`${origin}/images/og/stars.svg`}
<div
style={{
position: "absolute",
top: "0",
left: "0",
width: "1200",
height: "630",
left: "50px",
top: 0,
bottom: 0,
width: "40px",
borderLeft: "1px solid rgba(255, 255, 255, 0.1)",
}}
/>
<img
alt="planet"
src={`${origin}/images/og/planet.svg`}
<div
style={{
position: "absolute",
height: "188px",
width: "188px",
right: "0",
top: "10px",
right: "50px",
top: 0,
bottom: 0,
width: "40px",
borderRight: "1px solid rgba(255, 255, 255, 0.1)",
}}
/>

<img
alt="Codu Logo"
style={{
position: "absolute",
height: "53px",
width: "163px",
top: "114px",
left: "114px",
}}
src="https://www.codu.co/_next/image?url=%2Fimages%2Fcodu.png&w=1920&q=75"
alt="planet"
tw="h-[528px] w-[528px] absolute right-[-170px] top-[-170px]"
src={`${origin}/images/og/planet.png`}
/>
<div tw="flex relative flex-col" style={{ marginTop: "200px" }}>
<div
style={{
color: "white",
fontSize: "52px",
lineHeight: 1,
fontWeight: "800",
letterSpacing: "-.025em",
fontFamily: "Lato",
lineClamp: 3,
textWrap: "balance",
}}
>
{title}
{/* Main content */}
<div tw="flex flex-col h-full w-full px-28 py-28">
<div tw="flex flex-grow">
<img
alt="Codu Logo"
tw="h-10"
src={`${origin}/images/codu.png`}
/>
</div>
<div tw="flex flex-col">
<div
tw="mb-8 font-bold"
style={{
color: "white",
fontSize: "46px",
lineHeight: "1.2",
letterSpacing: "-0.025em",
fontFamily: "Lato-Bold",
display: "-webkit-box",
WebkitLineClamp: "3",
WebkitBoxOrient: "vertical",
overflow: "hidden",
textOverflow: "ellipsis",
paddingBottom: "0.1em",
}}
>
{title}
</div>
<div tw="flex items-center justify-between">
<div tw="flex flex-col">
<div
tw="flex text-2xl text-neutral-100"
style={{ paddingBottom: "0.1em" }}
>
{author}
</div>
<div
tw="text-xl text-neutral-400"
style={{ paddingBottom: "0.1em" }}
>
{`${formatDate(date)} · ${readTime} min read`}
</div>
</div>
</div>
</div>
</div>
</div>
),
{
fonts: [
{
name: "Inter Latin",
data: fontData,
name: "Lato",
data: regularFontData,
style: "normal",
weight: 400,
},
{
name: "Lato-Bold",
data: boldFontData,
style: "normal",
weight: 700,
},
],
height,
width,
},
);
} catch {
} catch (err) {
Sentry.captureException(err);
return new Response(`Failed to generate the image`, {
status: 500,
});
}
}

function formatDate(dateString: string): string {
try {
let date: Date;
if (dateString.includes(" ")) {
// Handle the specific format from the URL
const [datePart, timePart] = dateString.split(" ");
const [year, month, day] = datePart.split("-");
const [time] = timePart.split("."); // Remove milliseconds
const isoString = `${year}-${month}-${day}T${time}Z`;
date = new Date(isoString);
} else {
date = new Date(dateString);
}

if (isNaN(date.getTime())) {
throw new Error("Invalid date");
}
Comment on lines +198 to +200
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace unsafe isNaN with Number.isNaN

Using the global isNaN function is unsafe because it performs type coercion, which may lead to unexpected results. The Number.isNaN method provides a more reliable check by ensuring the value is of the Number type and is actually NaN.

Apply this diff to fix the issue:

-    if (isNaN(date.getTime())) {
+    if (Number.isNaN(date.getTime())) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isNaN(date.getTime())) {
throw new Error("Invalid date");
}
if (Number.isNaN(date.getTime())) {
throw new Error("Invalid date");
}
🧰 Tools
🪛 Biome

[error] 198-198: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

return date.toLocaleString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
} catch (error) {
return "";
}
}
Binary file added assets/Lato-Bold.ttf
Binary file not shown.
Binary file added assets/Lato-Regular.ttf
Binary file not shown.
5 changes: 4 additions & 1 deletion cdk/lambdas/uploadResize/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ exports.handler = async (event) => {
return resizedImage.webp({ quality: 80 }).toBuffer();
}
} catch (error) {
console.error(`Error resizing image to ${size.maxWidth}x${size.maxHeight}:`, error);
console.error(
`Error resizing image to ${size.maxWidth}x${size.maxHeight}:`,
error,
);
throw error;
}
};
Expand Down
Loading