From 08059e2a9d4bc8f45ab5fdd5fefca5d8a79bb8ed Mon Sep 17 00:00:00 2001 From: tangsongdayo Date: Sun, 19 Jul 2026 18:51:16 +0800 Subject: [PATCH] fix: correct Memos links and author identities --- CHANGELOG.md | 4 +++- app/components/post-card.tsx | 2 +- app/page.tsx | 4 ++-- app/posts/[id]/page.tsx | 11 ++++++----- app/sources/[id]/page.tsx | 2 +- app/tags/[tag]/page.tsx | 2 +- lib/db.ts | 5 +++++ lib/display.ts | 9 +++++---- lib/memos.ts | 7 ++++++- tests/display.test.ts | 14 ++++++++++---- tests/sync.test.ts | 2 +- 11 files changed, 41 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d104246..93ddfab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,12 @@ ### Fixed - Constrained remote Memos avatars to a fixed, cropped size so oversized source images cannot break dashboard or source-page layouts. +- Updated Memos article links for UID-based releases while retaining legacy numeric-ID links. +- Standardized article-page and share-metadata author labels with the source account identity. ### Changed -- Changed homepage author labels for Memos and Hub-to-Memos posts from duplicated source names to `@name@source-hostname` identities. +- Changed author labels for Memos and Hub-to-Memos posts from duplicated source names to `@remote-user@source-hostname` identities. ## [0.8.0] - 2026-07-19 diff --git a/app/components/post-card.tsx b/app/components/post-card.tsx index 4e22512..14a4183 100644 --- a/app/components/post-card.tsx +++ b/app/components/post-card.tsx @@ -4,7 +4,7 @@ import { Markdown } from "./markdown"; import { canonicalTags } from "@/lib/tags"; import { postAuthorLabel } from "@/lib/display"; -export type PublicPost = { id: number; source_id: number | null; origin: string; content: string; tags_json: string; attachments_json: string; created_at: string; remote_created_at?: string | null; username: string; name: string | null; remote_display_name?: string | null; source_base_url: string | null; comment_count: number; reaction_count: number }; +export type PublicPost = { id: number; source_id: number | null; origin: string; content: string; tags_json: string; attachments_json: string; created_at: string; remote_created_at?: string | null; username: string; name: string | null; remote_user?: string | null; remote_display_name?: string | null; source_base_url: string | null; comment_count: number; reaction_count: number }; export function PostCard({ post }: { post: PublicPost }) { let tags: string[] = []; try { tags = canonicalTags(JSON.parse(post.tags_json)); } catch { /* Ignore malformed legacy tags. */ } diff --git a/app/page.tsx b/app/page.tsx index 36c8c29..791d2b6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,10 +10,10 @@ export default async function Home({ searchParams }: { searchParams: Promise `"${term.replaceAll('"', '""')}"`).join(" AND "); - if (q) { where.push("posts_fts MATCH ?"); args.push(ftsQuery); } if (tag) { where.push("p.tags_json LIKE ?"); args.push(`%${JSON.stringify(tag).slice(1, -1)}%`); } if (author) { where.push("u.username LIKE ?"); args.push(`%${author}%`); } if (sourceId) { where.push("s.id=?"); args.push(sourceId); } if (from) { where.push("date(COALESCE(p.remote_created_at,p.created_at)) >= date(?)"); args.push(from); } if (to) { where.push("date(COALESCE(p.remote_created_at,p.created_at)) <= date(?)"); args.push(to); } if (attachments) where.push("p.attachments_json <> '[]'"); + if (q) { where.push("posts_fts MATCH ?"); args.push(ftsQuery); } if (tag) { where.push("p.tags_json LIKE ?"); args.push(`%${JSON.stringify(tag).slice(1, -1)}%`); } if (author) { where.push("(u.username LIKE ? OR s.remote_user LIKE ?)"); args.push(`%${author}%`, `%${author}%`); } if (sourceId) { where.push("s.id=?"); args.push(sourceId); } if (from) { where.push("date(COALESCE(p.remote_created_at,p.created_at)) >= date(?)"); args.push(from); } if (to) { where.push("date(COALESCE(p.remote_created_at,p.created_at)) <= date(?)"); args.push(to); } if (attachments) where.push("p.attachments_json <> '[]'"); const joins = ` FROM posts p ${q ? "JOIN posts_fts ON posts_fts.rowid=p.id" : ""} JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id `; const predicate = ` WHERE ${where.join(" AND ")}`; const total = Number((db.prepare(`SELECT count(*) count${joins}${predicate}`).get(...args) as { count: number }).count); const pages = Math.max(1, Math.ceil(total / pageSize)); const safePage = Math.min(page, pages); - const posts = db.prepare(`SELECT p.*,u.username,s.name,s.remote_display_name,s.base_url AS source_base_url,(SELECT count(*) FROM comments c WHERE c.post_id=p.id AND c.hidden=0) comment_count,(SELECT count(*) FROM reactions r WHERE r.post_id=p.id) reaction_count${joins}${predicate} ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT ? OFFSET ?`).all(...args, pageSize, (safePage - 1) * pageSize) as PublicPost[]; + const posts = db.prepare(`SELECT p.*,u.username,s.name,s.remote_user,s.remote_display_name,s.base_url AS source_base_url,(SELECT count(*) FROM comments c WHERE c.post_id=p.id AND c.hidden=0) comment_count,(SELECT count(*) FROM reactions r WHERE r.post_id=p.id) reaction_count${joins}${predicate} ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT ? OFFSET ?`).all(...args, pageSize, (safePage - 1) * pageSize) as PublicPost[]; const sources = db.prepare("SELECT id,name FROM sources WHERE is_enabled=1 ORDER BY name").all() as { id: number; name: string }[]; const params = new URLSearchParams(); for (const [key, value] of Object.entries(query)) if (value && key !== "page") params.set(key, value); const pageHref = (target: number) => { const next = new URLSearchParams(params); next.set("page", String(target)); return `/?${next}`; }; return <>

公開 Memos Hub

聚合朋友們公開分享的筆記。

發佈/連接來源

共 {total} 篇公開貼文

{posts.length ? posts.map((post) => ) :

尚無符合的公開貼文。

}{pages > 1 && }; diff --git a/app/posts/[id]/page.tsx b/app/posts/[id]/page.tsx index 2997075..f5315f8 100644 --- a/app/posts/[id]/page.tsx +++ b/app/posts/[id]/page.tsx @@ -5,25 +5,26 @@ import { getSession } from "@/lib/auth"; import { Attachments } from "@/app/components/attachments"; import { Markdown } from "@/app/components/markdown"; import { canonicalTags } from "@/lib/tags"; +import { postAuthorLabel } from "@/lib/display"; export const dynamic = "force-dynamic"; export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { - const { id: rawId } = await params; const post = db.prepare("SELECT p.content,p.hidden,p.visibility,u.username,s.name FROM posts p JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id WHERE p.id=?").get(Number(rawId)) as { content: string; hidden: number; visibility: string; username: string; name: string | null } | undefined; + const { id: rawId } = await params; const post = db.prepare("SELECT p.content,p.hidden,p.visibility,p.origin,u.username,s.name,s.remote_user,s.base_url AS source_base_url FROM posts p JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id WHERE p.id=?").get(Number(rawId)) as { content: string; hidden: number; visibility: string; origin: string; username: string; name: string | null; remote_user: string | null; source_base_url: string | null } | undefined; if (!post || post.hidden || post.visibility !== "PUBLIC") return { title: "找不到貼文" }; - const description = post.content.replace(/\s+/g, " ").slice(0, 160); - return { title: `@${post.username} 的貼文|Mebbling`, description, openGraph: { title: `@${post.username}${post.name ? ` · ${post.name}` : ""}|Mebbling`, description, type: "article" } }; + const description = post.content.replace(/\s+/g, " ").slice(0, 160); const author = postAuthorLabel(post); + return { title: `${author} 的貼文|Mebbling`, description, openGraph: { title: `${author}|Mebbling`, description, type: "article" } }; } export default async function PostPage({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise<{ reported?: string }> }) { const { id: rawId } = await params; const id = Number(rawId); - const post = db.prepare("SELECT p.*,u.username,s.name,s.base_url AS source_base_url,s.remote_display_name FROM posts p JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id WHERE p.id=?").get(id) as any; const query = await searchParams; + const post = db.prepare("SELECT p.*,u.username,s.name,s.base_url AS source_base_url,s.remote_user,s.remote_display_name FROM posts p JOIN users u ON u.id=p.author_id LEFT JOIN sources s ON s.id=p.source_id WHERE p.id=?").get(id) as any; const query = await searchParams; if (!post || post.hidden) notFound(); const user = await getSession(); if (post.visibility !== "PUBLIC" && post.author_id !== user?.id) redirect("/"); if (user && post.visibility === "PUBLIC") db.prepare("INSERT INTO reading_history(user_id,post_id) VALUES(?,?) ON CONFLICT(user_id,post_id) DO UPDATE SET last_read_at=CURRENT_TIMESTAMP").run(user.id, id); const bookmark = user ? db.prepare("SELECT kind FROM bookmarks WHERE user_id=? AND post_id=?").get(user.id, id) as { kind: string } | undefined : undefined; const comments = db.prepare("SELECT c.*,u.username FROM comments c JOIN users u ON u.id=c.author_id WHERE c.post_id=? AND c.hidden=0 ORDER BY c.created_at").all(id) as any[]; const reactions = db.prepare("SELECT emoji,count(*) count FROM reactions WHERE post_id=? GROUP BY emoji").all(id) as any[]; - let tags: string[] = []; try { tags = canonicalTags(JSON.parse(post.tags_json)); } catch {} const publishedAt = post.remote_created_at || post.created_at; const canExport = Boolean(user && (post.author_id === user.id || (post.source_id && db.prepare("SELECT 1 FROM source_members WHERE source_id=? AND user_id=?").get(post.source_id, user.id)))); return

@{post.username} · {post.remote_display_name || post.name || "Hub"} · {new Date(publishedAt).toLocaleString("zh-TW")}{post.remote_url && <> · 在 Memos 開啟}

{canExport &&

匯出 JSON匯出 Markdown

}
+ let tags: string[] = []; try { tags = canonicalTags(JSON.parse(post.tags_json)); } catch {} const publishedAt = post.remote_created_at || post.created_at; const canExport = Boolean(user && (post.author_id === user.id || (post.source_id && db.prepare("SELECT 1 FROM source_members WHERE source_id=? AND user_id=?").get(post.source_id, user.id)))); return

{postAuthorLabel(post)} · {new Date(publishedAt).toLocaleString("zh-TW")}{post.remote_url && <> · 在 Memos 開啟}

{canExport &&

匯出 JSON匯出 Markdown

}
{reactions.map((reaction: any) => {reaction.emoji} {reaction.count})}{user && <>
}{user && ["👍", "❤️", "🎉", "🤔"].map((emoji) =>
)}

留言

{user ? <>