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
177 changes: 177 additions & 0 deletions app/wiki/[id]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { ImageResponse } from "next/og";
import { getArticleByIdFromDB } from "@/lib/data/articles";
import { stripMarkdown } from "@/lib/utils";

export const runtime = "edge";
export const alt = "WikiMasters Article";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

interface OgImageProps {
params: Promise<{ id: string }>;
}

export default async function ArticleOgImage({ params }: OgImageProps) {
const { id } = await params;
const article = await getArticleByIdFromDB(id);

Comment on lines +2 to +17
const title = article?.title ?? "WikiMasters";
const author = article?.authorName ?? "WikiMasters";

const description = article
? stripMarkdown(article.content, 120)
: "A knowledge sharing platform.";

return new ImageResponse(
<div
style={{
background:
"linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%)",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: "60px",
fontFamily: "sans-serif",
position: "relative",
}}
>
{/* Top accent line */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "6px",
background:
"linear-gradient(90deg, #0ea5e9 0%, #6366f1 50%, #0ea5a4 100%)",
}}
/>

{/* Logo / Brand */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "12px",
}}
>
<div
style={{
width: "40px",
height: "40px",
background: "linear-gradient(135deg, #0ea5e9, #6366f1)",
borderRadius: "8px",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
}}
>
W
</div>
<span
style={{
color: "#94a3b8",
fontSize: "22px",
fontWeight: "600",
letterSpacing: "0.05em",
}}
>
WikiMasters
</span>
</div>

{/* Article Title */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "20px",
flex: 1,
justifyContent: "center",
}}
>
<div
style={{
color: "#f8fafc",
fontSize: title.length > 50 ? "44px" : "56px",
fontWeight: "800",
lineHeight: "1.15",
letterSpacing: "-0.02em",
maxWidth: "900px",
}}
>
{title}
</div>
{description && (
<div
style={{
color: "#94a3b8",
fontSize: "22px",
lineHeight: "1.5",
maxWidth: "820px",
}}
>
{description}
</div>
)}
</div>

{/* Footer */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<div
style={{
width: "32px",
height: "32px",
background: "linear-gradient(135deg, #6366f1, #0ea5e9)",
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "white",
fontSize: "14px",
fontWeight: "700",
}}
>
{author.charAt(0).toUpperCase()}
</div>
<span style={{ color: "#64748b", fontSize: "18px" }}>
By {author}
</span>
</div>
<div
style={{
background: "rgba(14,165,164,0.15)",
border: "1px solid rgba(14,165,164,0.3)",
borderRadius: "6px",
padding: "6px 16px",
color: "#0ea5a4",
fontSize: "16px",
fontWeight: "600",
}}
>
Read Article
</div>
</div>
</div>,
{
...size,
},
);
}
50 changes: 50 additions & 0 deletions app/wiki/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import WikiArticleViewer from "@/components/features/wikicards/wiki-article-viewer";
import { authorizeUserToEditArticle } from "@/db/authz";
import { getArticleByIdFromDB } from "@/lib/data/articles";
import { stripMarkdown } from "@/lib/utils";
import { stackServerApp } from "@/stack/server";

interface ViewArticlePageProps {
Expand All @@ -9,6 +11,54 @@ interface ViewArticlePageProps {
}>;
}

export async function generateMetadata({
params,
}: ViewArticlePageProps): Promise<Metadata> {
const { id } = await params;
const article = await getArticleByIdFromDB(id);

if (!article) {
return {
title: "Article Not Found | WikiMasters",
description: "The article you are looking for does not exist.",
};
}

const description = stripMarkdown(article.content);

const BASE_URL =
process.env.NEXT_PUBLIC_BASE_URL ?? "https://wikimasters.com";
const articleUrl = `${BASE_URL}/wiki/${id}`;

const images = article.imageUrl
? [{ url: article.imageUrl, width: 1200, height: 630, alt: article.title }]
: undefined;

return {
title: `${article.title} | WikiMasters`,
description,
openGraph: {
title: article.title,
description,
url: articleUrl,
siteName: "WikiMasters",
type: "article",
publishedTime: article.createdAt ?? undefined,
authors: article.authorName ? [article.authorName] : undefined,
images,
},
twitter: {
card: "summary_large_image",
title: article.title,
description,
images: article.imageUrl ? [article.imageUrl] : undefined,
},
alternates: {
canonical: articleUrl,
},
};
}

export default async function ViewArticlePage({
params,
}: ViewArticlePageProps) {
Expand Down
106 changes: 98 additions & 8 deletions components/features/wikicards/wiki-article-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import {
CopyIcon,
Edit,
Eye,
Facebook,
Home,
Link2,
Linkedin,
Share2,
Trash,
Twitter,
User,
} from "lucide-react";
import Image from "next/image";
Expand Down Expand Up @@ -154,6 +159,43 @@ const WikiArticleViewer: React.FC<WikiArticleViewerProps> = ({
}
};

const handleShareSocial = (platform: "twitter" | "facebook" | "linkedin") => {
const url = encodeURIComponent(window.location.href);
const text = encodeURIComponent(article.title);
const shareUrls = {
twitter: `https://twitter.com/intent/tweet?text=${text}&url=${url}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${url}`,
linkedin: `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${text}`,
};
window.open(
shareUrls[platform],
"_blank",
"noopener,noreferrer,width=600,height=450",
);
};

const handleCopyLink = async () => {
if (!navigator.clipboard) {
toast.error("Copy to clipboard is not supported in this browser.", {
position: "bottom-left",
duration: 2500,
});
return;
}
try {
await navigator.clipboard.writeText(window.location.href);
toast.success("Link copied to clipboard!", {
Comment on lines +177 to +187
position: "bottom-left",
duration: 2500,
});
} catch {
toast.error("Failed to copy link.", {
position: "bottom-left",
duration: 2500,
});
}
};

return (
<div className="container mx-auto px-4 py-8 max-w-4xl">
{/* Breadcrumb Navigation */}
Expand Down Expand Up @@ -404,14 +446,62 @@ const WikiArticleViewer: React.FC<WikiArticleViewerProps> = ({
)}

{/* Footer Actions */}
<div className="mt-8 flex justify-between items-center">
<Button
variant={"secondary"}
nativeButton={false}
render={(props) => <Link {...props} href={Routes.HOME} />}
>
Back to Articles
</Button>
<div className="mt-8 flex flex-col gap-6">
{/* Social Share Section */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Share2 className="h-4 w-4" />
<span>Share this article:</span>
</div>
<div className="flex items-center gap-2 flex-wrap">
<Button
variant="outline"
size="sm"
className="cursor-pointer gap-2 hover:bg-[#1da1f2] hover:text-white hover:border-[#1da1f2] transition-colors"
onClick={() => handleShareSocial("twitter")}
>
<Twitter className="h-4 w-4" />
Twitter / X
</Button>
<Button
variant="outline"
size="sm"
className="cursor-pointer gap-2 hover:bg-[#1877f2] hover:text-white hover:border-[#1877f2] transition-colors"
onClick={() => handleShareSocial("facebook")}
>
<Facebook className="h-4 w-4" />
Facebook
</Button>
<Button
variant="outline"
size="sm"
className="cursor-pointer gap-2 hover:bg-[#0a66c2] hover:text-white hover:border-[#0a66c2] transition-colors"
onClick={() => handleShareSocial("linkedin")}
>
<Linkedin className="h-4 w-4" />
LinkedIn
</Button>
<Button
variant="outline"
size="sm"
className="cursor-pointer gap-2 hover:bg-muted transition-colors"
onClick={handleCopyLink}
>
<Link2 className="h-4 w-4" />
Copy Link
</Button>
</div>
Comment thread
halilibrahimcelik marked this conversation as resolved.
</div>

<div className="flex justify-between items-center">
<Button
variant={"secondary"}
nativeButton={false}
render={(props) => <Link {...props} href={Routes.HOME} />}
>
Back to Articles
</Button>
</div>
</div>
</div>
);
Expand Down
13 changes: 13 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,16 @@ export const formatDate = (dateString: string) => {
day: "numeric",
});
};

/**
* Strips common markdown syntax from a string and returns plain text.
* Useful for generating SEO descriptions and OpenGraph excerpts.
*/
export function stripMarkdown(content: string, maxLength = 160): string {
const raw = content
.replace(/[#*_`[\]()>~]/g, "")
.replace(/\n+/g, " ")
.trim()
.slice(0, maxLength);
return raw.length === maxLength ? `${raw}…` : raw;
Comment on lines +22 to +27
}
Loading
Loading