Skip to content
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
6 changes: 1 addition & 5 deletions app/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@ export async function generateMetadata({
}

const { metadata } = page;
const ogParams = new URLSearchParams({ title: metadata.title });
if (metadata.description) {
ogParams.set("description", metadata.description);
}
const ogImage = metadata.image || `${baseUrl}/og?${ogParams.toString()}`;
const ogImage = metadata.image || `${baseUrl}/og`;

return {
alternates: {
Expand Down
6 changes: 1 addition & 5 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,7 @@ export async function generateMetadata({
imageAlt,
} = post.metadata;
const publishedTime = publishedAt.toISOString();
const ogParams = new URLSearchParams({ title });
if (description) {
ogParams.set("description", description);
}
const ogImage = image || `${baseUrl}/og?${ogParams.toString()}`;
const ogImage = image || `${baseUrl}/og`;
const ogImageAlt = imageAlt || title;
Comment thread
anshroboto marked this conversation as resolved.

return {
Expand Down
Binary file modified app/favicon.ico
Binary file not shown.
23 changes: 6 additions & 17 deletions app/og/preview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ interface OgEntry {
route: string;
}

function buildOgUrl(title: string, description?: string): string {
const params = new URLSearchParams({ title });
if (description) {
params.set("description", description);
}
return `${baseUrl}/og?${params.toString()}`;
function buildOgUrl(): string {
return `${baseUrl}/og`;
}

function getOgEntries(): OgEntry[] {
Expand All @@ -23,18 +19,12 @@ function getOgEntries(): OgEntry[] {
const entries: OgEntry[] = [
{
label: "Home",
ogUrl: buildOgUrl(
"The AI-Powered Code Editor for Productive Teams",
"Build faster with intelligent code completion, real-time collaboration, and seamless AI integration."
),
ogUrl: buildOgUrl(),
route: "/",
},
{
label: "Blog Index",
ogUrl: buildOgUrl(
"Blog",
"Insights on AI-powered development, productivity, and the future of coding."
),
ogUrl: buildOgUrl(),
route: "/blog",
},
];
Expand All @@ -43,8 +33,7 @@ function getOgEntries(): OgEntry[] {
const title = page.metadata.title ?? "Untitled";
entries.push({
label: title,
ogUrl:
page.metadata.image || buildOgUrl(title, page.metadata.description),
ogUrl: page.metadata.image || buildOgUrl(),
route: page.path,
});
}
Expand All @@ -53,7 +42,7 @@ function getOgEntries(): OgEntry[] {
const { title } = post.metadata;
entries.push({
label: title,
ogUrl: post.metadata.image || buildOgUrl(title, post.metadata.summary),
ogUrl: post.metadata.image || buildOgUrl(),
route: `/blog/${post.slug}`,
});
}
Expand Down
205 changes: 32 additions & 173 deletions app/og/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,198 +2,57 @@ import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { ImageResponse } from "next/og";

// Not the schema's 220-char ceiling — this is what fits the OG canvas.
const OG_TITLE_LIMIT = 100;
const OG_DESCRIPTION_LIMIT = 140;
const ELLIPSIS = "...";

const truncate = (value: string, limit: number) =>
value.length > limit
? `${value.slice(0, limit - ELLIPSIS.length)}${ELLIPSIS}`
: value;

export async function GET(request: Request) {
const url = new URL(request.url);
// Both params are attacker-controlled on a public unauthenticated route.
const title = truncate(
url.searchParams.get("title") ||
"The AI-Powered Code Editor for Productive Teams",
OG_TITLE_LIMIT
);
const description = url.searchParams.get("description") || "";
// Geometry from the Figma "pointer-og" frame. The icon PNG has transparent
// side padding, so the 16px flex gap renders as the spec's 36px optical gap.
const OG_WIDTH = 1200;
const OG_HEIGHT = 630;
const OG_BACKGROUND = "#13120D";
const MARK_SIZE = 164;
const WORDMARK_SIZE = 125.91;
const WORDMARK_TRACKING = "-0.03em";
const WORDMARK_INNER_TRACKING = "-0.06em";
const LOCKUP_GAP = 16;

export async function GET() {
const [fontData, logoData] = await Promise.all([
readFile(join(process.cwd(), "fonts/CursorGothic-Regular.ttf")),
readFile(join(process.cwd(), "public/icon-192.png")),
readFile(join(process.cwd(), "public/icon-512.png")),
]);

const logoBase64 = `data:image/png;base64,${logoData.toString("base64")}`;

const hasDescription = description.length > 0;
const titleSize = hasDescription ? 56 : 64;
const truncatedDescription = truncate(description, OG_DESCRIPTION_LIMIT);

return new ImageResponse(
<div
style={{
backgroundColor: "#09090b",
alignItems: "center",
backgroundColor: OG_BACKGROUND,
display: "flex",
flexDirection: "column",
fontFamily: "CursorGothic",
gap: `${LOCKUP_GAP}px`,
height: "100%",
overflow: "hidden",
position: "relative",
justifyContent: "center",
width: "100%",
}}
>
<div
style={{
background:
"radial-gradient(circle, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.015) 40%, transparent 70%)",
borderRadius: "50%",
display: "flex",
height: "900px",
position: "absolute",
right: "-100px",
top: "-300px",
width: "900px",
}}
/>

<div
style={{
background:
"radial-gradient(circle, rgba(180,200,255,0.025) 0%, transparent 60%)",
borderRadius: "50%",
bottom: "-400px",
display: "flex",
height: "800px",
left: "-200px",
position: "absolute",
width: "800px",
}}
{/* biome-ignore lint/performance/noImgElement: Satori ImageResponse only supports img */}
<img
alt="Pointer Logo"
height={MARK_SIZE}
src={logoBase64}
width={MARK_SIZE}
/>

<div
style={{
background:
"linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.08) 30%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.08) 70%, transparent 100%)",
color: "white",
display: "flex",
height: "1px",
left: 0,
position: "absolute",
right: 0,
top: 0,
}}
/>

<div
style={{
background:
"linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.12) 20%, rgba(255,255,255,0.25) 50%, rgba(255,255,255,0.12) 80%, transparent 100%)",
bottom: 0,
display: "flex",
height: "2px",
left: 0,
position: "absolute",
right: 0,
}}
/>

<div
style={{
background:
"linear-gradient(135deg, rgba(255,255,255,0.01) 0%, transparent 50%, rgba(255,255,255,0.008) 100%)",
display: "flex",
inset: 0,
position: "absolute",
}}
/>

<div
style={{
display: "flex",
flexDirection: "column",
height: "100%",
justifyContent: "space-between",
padding: "56px 80px",
fontFamily: "CursorGothic",
fontSize: WORDMARK_SIZE,
fontWeight: 400,
letterSpacing: WORDMARK_TRACKING,
}}
>
<div
style={{
alignItems: "center",
display: "flex",
gap: "14px",
}}
>
{/* biome-ignore lint/performance/noImgElement: Satori ImageResponse only supports img */}
<img alt="Pointer Logo" height={36} src={logoBase64} width={36} />
<div
style={{
color: "rgba(255,255,255,0.9)",
fontSize: 20,
fontWeight: 400,
letterSpacing: "0.14em",
textTransform: "uppercase" as const,
}}
>
Pointer
</div>
</div>

<div
style={{
display: "flex",
flexDirection: "column",
gap: hasDescription ? "20px" : "0",
maxWidth: "950px",
}}
>
<div
style={{
color: "white",
fontSize: titleSize,
fontWeight: 400,
letterSpacing: "-0.03em",
lineHeight: 1.1,
}}
>
{title}
</div>
{hasDescription ? (
<div
style={{
color: "rgba(255,255,255,0.45)",
display: "flex",
fontSize: 22,
fontWeight: 400,
letterSpacing: "0.01em",
lineHeight: 1.5,
maxWidth: "780px",
}}
>
{truncatedDescription}
</div>
) : null}
</div>

<div
style={{
alignItems: "center",
display: "flex",
justifyContent: "space-between",
}}
>
<div
style={{
color: "rgba(255,255,255,0.35)",
fontSize: 17,
letterSpacing: "0.03em",
}}
>
pointer.dev
</div>
</div>
<span>P</span>
<span style={{ letterSpacing: WORDMARK_INNER_TRACKING }}>OIN</span>
<span>TER</span>
</div>
</div>,
{
Expand All @@ -205,8 +64,8 @@ export async function GET(request: Request) {
weight: 400,
},
],
height: 630,
width: 1200,
height: OG_HEIGHT,
width: OG_WIDTH,
}
);
}
3 changes: 1 addition & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export async function generateMetadata() {
page?.metadata.description ??
"Pointer: A clean, simple portfolio and content platform. Build beautiful portfolios without complex CMS or databases. Everything is code, in git.";

const ogParams = new URLSearchParams({ description, title });
const ogImage = `${baseUrl}/og?${ogParams.toString()}`;
const ogImage = `${baseUrl}/og`;

return {
alternates: {
Expand Down
6 changes: 1 addition & 5 deletions components/json-ld.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ function buildCollectionPageSchema(props: CollectionPageProps) {
}

function buildArticleSchema(props: ArticleProps, baseUrl: string) {
const ogParams = new URLSearchParams({ title: props.headline });
if (props.description) {
ogParams.set("description", props.description);
}
const image = props.image ?? `${baseUrl}/og?${ogParams.toString()}`;
const image = props.image ?? `${baseUrl}/og`;

return {
"@context": "https://schema.org",
Expand Down
Binary file modified public/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading