fix(web): correct relative imports in opengraph-image routes
Some checks failed
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Failing after 8m32s
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Failing after 1m33s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
Some checks failed
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Failing after 8m32s
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Failing after 1m33s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
This commit is contained in:
70
apps/web/app/(site)/blog/[slug]/opengraph-image.tsx
Normal file
70
apps/web/app/(site)/blog/[slug]/opengraph-image.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { getAllPosts } from "../../../../src/lib/posts";
|
||||
import { blogThumbnails } from "../../../../src/components/blog/blogThumbnails";
|
||||
import { BlogOGImageTemplate } from "../../../../src/components/BlogOGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../../src/lib/og-helper";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
let backgroundImageSrc: string | undefined = undefined;
|
||||
|
||||
// If we have a custom generated thumbnail, convert it to a data URI for Satori
|
||||
if (post?.thumbnail) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), "public", post.thumbnail);
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
|
||||
const ext = path.extname(post.thumbnail).substring(1).toLowerCase();
|
||||
const mimeType =
|
||||
ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
||||
|
||||
backgroundImageSrc = `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[OG Image Generator] Could not read thumbnail file for ${slug} to use as background:`,
|
||||
err,
|
||||
);
|
||||
// Fall through to standard plain background
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnail = blogThumbnails[slug];
|
||||
|
||||
const title = post?.title || "Marc Mintel";
|
||||
const description =
|
||||
post?.description ||
|
||||
"Technical problem solver's blog - practical insights and learning notes";
|
||||
const label = post ? "Blog Post" : "Engineering";
|
||||
const accentColor = thumbnail?.accent;
|
||||
const keyword = thumbnail?.keyword;
|
||||
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<BlogOGImageTemplate
|
||||
title={title}
|
||||
description={description}
|
||||
label={label}
|
||||
accentColor={accentColor}
|
||||
keyword={keyword}
|
||||
backgroundImageSrc={backgroundImageSrc}
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
115
apps/web/app/(site)/blog/[slug]/page.tsx
Normal file
115
apps/web/app/(site)/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { BlogPostClient } from "@/src/components/BlogPostClient";
|
||||
import { TextSelectionShare } from "@/src/components/TextSelectionShare";
|
||||
import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar";
|
||||
import { MDXContent } from "@/src/components/MDXContent";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allPosts = await getAllPosts();
|
||||
return allPosts.map((post) => ({
|
||||
slug: post.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) return {};
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
type: "article",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const formattedDate = new Date(post.date).toLocaleDateString("de-DE", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const wordCount = post.description.split(/\s+/).length + 300;
|
||||
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 md:gap-12 py-8 md:py-24 overflow-hidden">
|
||||
<BlogPostClient readingTime={readingTime} title={post.title} />
|
||||
|
||||
<BlogPostHeader
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
date={formattedDate}
|
||||
readingTime={readingTime}
|
||||
slug={slug}
|
||||
thumbnail={post.thumbnail}
|
||||
/>
|
||||
|
||||
<main id="post-content">
|
||||
<BlogPostStickyBar
|
||||
title={post.title}
|
||||
url={`https://mintel.me/blog/${slug}`}
|
||||
/>
|
||||
|
||||
<Section containerVariant="wide" className="pt-0 md:pt-0">
|
||||
<div className="max-w-4xl mx-auto px-5 md:px-0">
|
||||
<Reveal delay={0.4} width="100%">
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
|
||||
{post.tags?.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded text-[9px] md:text-[10px] font-mono text-slate-500 uppercase tracking-widest"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="article-content max-w-none">
|
||||
<MDXContent code={post.body.code} />
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
|
||||
<TextSelectionShare />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
apps/web/app/(site)/blog/page.tsx
Normal file
14
apps/web/app/(site)/blog/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { BlogClient } from "@/src/components/blog/BlogClient";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog | Mintel.me",
|
||||
description:
|
||||
"Gedanken über Engineering, Design und die Architektur der Zukunft.",
|
||||
};
|
||||
|
||||
export default async function BlogPage() {
|
||||
const posts = await getAllPosts();
|
||||
return <BlogClient allPosts={posts as any} />;
|
||||
}
|
||||
Reference in New Issue
Block a user