feat: Dynamic SEO metadata, OpenGraph images, and social sharing for articles#5
Merged
halilibrahimcelik merged 2 commits intoMar 17, 2026
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… sharing Co-authored-by: halilibrahimcelik <92088301+halilibrahimcelik@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Add dynamic SEO improvements and social media sharing features
feat: Dynamic SEO metadata, OpenGraph images, and social sharing for articles
Mar 17, 2026
There was a problem hiding this comment.
Pull request overview
Adds SEO and social-sharing support to wiki article pages by generating per-article metadata (OpenGraph/Twitter/canonical), introducing a dynamic OpenGraph image route, adding in-page share actions, and sharing markdown-to-text logic via a utility.
Changes:
- Implemented dynamic
generateMetadatafor/wiki/[id]with OpenGraph/Twitter/canonical fields and not-found fallbacks. - Added a
/wiki/[id]/opengraph-imageimage generator and a sharedstripMarkdown()helper for excerpts. - Added social sharing buttons (Twitter/X, Facebook, LinkedIn) plus “Copy Link” to the article viewer.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
lib/utils.ts |
Adds stripMarkdown() helper used for meta descriptions/excerpts. |
components/features/wikicards/wiki-article-viewer.tsx |
Adds social share and copy-link UI + handlers. |
app/wiki/[id]/page.tsx |
Adds generateMetadata() for article-specific SEO/OpenGraph/Twitter fields. |
app/wiki/[id]/opengraph-image.tsx |
Adds an OpenGraph image generator route for article previews. |
Comments suppressed due to low confidence (1)
app/wiki/[id]/page.tsx:71
getArticleByIdFromDB(id)is executed both ingenerateMetadataand again in the page component, which will result in two DB queries per request (and two cache lookups) for the same article. Consider deduping by wrapping the fetch in a shared memoized function (e.g.,react'scache()), or restructuring to reuse the same promise where possible to reduce latency and load.
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) {
const { id } = await params;
const user = await stackServerApp.getUser({ or: "redirect" });
const userId = user.id;
const [article, canEdit] = await Promise.all([
getArticleByIdFromDB(id),
authorizeUserToEditArticle(userId, id),
]);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+22
to
+27
| const raw = content | ||
| .replace(/[#*_`[\]()>~]/g, "") | ||
| .replace(/\n+/g, " ") | ||
| .trim() | ||
| .slice(0, maxLength); | ||
| return raw.length === maxLength ? `${raw}…` : raw; |
Comment on lines
+177
to
+187
| 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
+2
to
+17
| 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); | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Article pages lacked SEO metadata entirely and had no mechanism for social sharing or link previews.
Changes
Dynamic
generateMetadata—app/wiki/[id]/page.tsx<title>,<meta description>, full OpenGraph (articletype withpublishedTime,authors,images), Twitter Card (summary_large_image), and canonical URLPlaceholder OpenGraph image —
app/wiki/[id]/opengraph-image.tsxImageResponse(1200×630) auto-served at/wiki/[id]/opengraph-imageimageUrlSocial sharing buttons —
wiki-article-viewer.tsxwindow.open), plus Copy Link (clipboard with toast feedback)encodeURIComponentShared utility —
lib/utils.tsDeduplicated markdown-stripping logic used by both
generateMetadataand the OG image generator.💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.