mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-01 12:23:50 +00:00
blog: honor canonical_url so guest posts point at their source (#151)
* blog: honor canonical_url so guest posts point at their source A post syndicated from another blog carries the original URL in blog_posts.canonical_url (the autoblog webhook already stores it). The post page ignored it and always self-canonicalized, so a guest post would compete with its source for the same words. Now the page selects canonical_url and author, sets rel=canonical to the original when present (self otherwise), points JSON-LD mainEntityOfPage at it, and shows readers an "Originally published on <host>" line with the author byline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MxNif5tsYq4LczgG7aE8Jp * blog: author is jsonb, render a name not [object Object] The blog_posts.author column is jsonb (e.g. { name, url }), not text. Extract a display name for the byline and JSON-LD instead of rendering the object directly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MxNif5tsYq4LczgG7aE8Jp --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9f42ce222a
commit
00622f5882
1 changed files with 40 additions and 3 deletions
|
|
@ -17,8 +17,22 @@ type PostRow = {
|
|||
featured_image: { url?: string } | null;
|
||||
published_at: string;
|
||||
updated_at: string;
|
||||
// A guest post syndicated from another blog carries the original URL here.
|
||||
// When set, it is the canonical for search engines and is shown to readers,
|
||||
// so a cross-post never competes with its source for the same words.
|
||||
canonical_url: string | null;
|
||||
// Stored as jsonb: an object like { name, url }, or occasionally a bare
|
||||
// string. Never rendered directly, or an object prints as [object Object].
|
||||
author: { name?: string; url?: string } | string | null;
|
||||
};
|
||||
|
||||
/** A display name from the jsonb author, whatever shape it took. */
|
||||
function authorName(author: PostRow["author"]): string {
|
||||
if (!author) return "";
|
||||
if (typeof author === "string") return author;
|
||||
return typeof author.name === "string" ? author.name : "";
|
||||
}
|
||||
|
||||
const SITE_URL = (process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, "");
|
||||
|
||||
async function loadPost(slug: string): Promise<PostRow | null> {
|
||||
|
|
@ -26,7 +40,7 @@ async function loadPost(slug: string): Promise<PostRow | null> {
|
|||
const supabase = publicClient();
|
||||
const { data } = await supabase
|
||||
.from("blog_posts")
|
||||
.select("slug, title, excerpt, html, featured_image, published_at, updated_at")
|
||||
.select("slug, title, excerpt, html, featured_image, published_at, updated_at, canonical_url, author")
|
||||
.eq("slug", slug)
|
||||
.eq("status", "published")
|
||||
.maybeSingle();
|
||||
|
|
@ -47,7 +61,9 @@ export async function generateMetadata({
|
|||
return {
|
||||
title: `${post.title} · LogicSRC`,
|
||||
description: post.excerpt ?? undefined,
|
||||
alternates: { canonical: `/blog/${post.slug}` },
|
||||
// A guest post points its canonical at the original; an original post is
|
||||
// canonical to itself.
|
||||
alternates: { canonical: post.canonical_url ?? `/blog/${post.slug}` },
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.excerpt ?? undefined,
|
||||
|
|
@ -74,6 +90,7 @@ export default async function BlogPostPage({
|
|||
const post = await loadPost(slug);
|
||||
if (!post) notFound();
|
||||
|
||||
const canonical = post.canonical_url ?? `${SITE_URL}/blog/${post.slug}`;
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
|
|
@ -83,9 +100,20 @@ export default async function BlogPostPage({
|
|||
datePublished: post.published_at,
|
||||
dateModified: post.updated_at,
|
||||
url: `${SITE_URL}/blog/${post.slug}`,
|
||||
mainEntityOfPage: `${SITE_URL}/blog/${post.slug}`,
|
||||
// For a guest post this is the original; search engines follow it.
|
||||
mainEntityOfPage: canonical,
|
||||
...(authorName(post.author) ? { author: { "@type": "Person", name: authorName(post.author) } } : {}),
|
||||
publisher: { "@id": `${SITE_URL}/#organization` },
|
||||
};
|
||||
const byline = authorName(post.author);
|
||||
let originHost = "";
|
||||
if (post.canonical_url) {
|
||||
try {
|
||||
originHost = new URL(post.canonical_url).host;
|
||||
} catch {
|
||||
originHost = "";
|
||||
}
|
||||
}
|
||||
const html = sanitizeRenderedHtml(post.html);
|
||||
|
||||
return (
|
||||
|
|
@ -105,6 +133,15 @@ export default async function BlogPostPage({
|
|||
</h1>
|
||||
<div style={{ color: "#5b6b7a", fontSize: "0.85rem", marginBottom: "2rem" }}>
|
||||
{formatDate(post.published_at)}
|
||||
{byline ? ` · ${byline}` : ""}
|
||||
{post.canonical_url ? (
|
||||
<>
|
||||
{" · "}
|
||||
<a href={post.canonical_url} rel="noreferrer" style={{ color: "#5b6b7a" }}>
|
||||
Originally published{originHost ? ` on ${originHost}` : ""}
|
||||
</a>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
{post.featured_image?.url ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue