2026-07-19 03:16:18 +08:00
import type { Metadata } from "next" ;
import { notFound , redirect } from "next/navigation" ;
import { db } from "@/lib/db" ;
import { getSession } from "@/lib/auth" ;
import { Attachments } from "@/app/components/attachments" ;
import { Markdown } from "@/app/components/markdown" ;
2026-07-19 04:15:28 +08:00
import { canonicalTags } from "@/lib/tags" ;
2026-07-19 03:16:18 +08:00
export const dynamic = "force-dynamic" ;
export async function generateMetadata ({ params } : { params : Promise < { id : string } > }) : Promise < Metadata > {
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 ;
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" } };
}
2026-07-19 03:58:03 +08:00
export default async function PostPage ({ params , searchParams } : { params : Promise < { id : string } > ; searchParams : Promise < { reported? : string } > }) {
2026-07-19 03:16:18 +08:00
const { id : rawId } = await params ; const id = Number ( rawId );
2026-07-19 03:58:03 +08:00
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 ;
2026-07-19 03:16:18 +08:00
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 [];
2026-07-19 04:34:15 +08:00
let tags : string [] = []; try { tags = canonicalTags ( JSON . parse ( post . tags_json )); } catch {} const publishedAt = post . remote_created_at || post . created_at ; return < article >< p className = "meta" > @ { post . username } · { post . remote_display_name || post . name || "Hub" } · { new Date ( publishedAt ). toLocaleString ( "zh-TW" )}{ post . remote_url && <> · < a href = { post . remote_url } target = "_blank" rel = "noreferrer" > 在 Memos 開啟 </ a ></>}</ p >< section className = "card" >< Markdown content = { post . content } tags = { tags } /></ section >< Attachments json = { post . attachments_json } sourceBaseUrl = { post . source_base_url } />
2026-07-19 03:16:18 +08:00
< section className = "row" >{ reactions . map (( reaction : any ) => < span className = "tag" key = { reaction . emoji }>{ reaction . emoji } { reaction . count }</ span >)}{ user && <>< form action = "/api/bookmarks" method = "post" >< input type = "hidden" name = "postId" value = { id } />< input type = "hidden" name = "kind" value = "saved" />< button >{ bookmark ? . kind === "saved" ? "取消收藏" : "收藏" }</ button ></ form >< form action = "/api/bookmarks" method = "post" >< input type = "hidden" name = "postId" value = { id } />< input type = "hidden" name = "kind" value = "later" />< button >{ bookmark ? . kind === "later" ? "取消稍後閱讀" : "稍後閱讀" }</ button ></ form ></>}{ user && [ "👍" , "❤️" , "🎉" , "🤔" ]. map (( emoji ) => < form action = "/api/reactions" method = "post" key = { emoji }>< input type = "hidden" name = "postId" value = { id } />< input type = "hidden" name = "emoji" value = { emoji } />< button >{ emoji }</ button ></ form >)}</ section >
2026-07-19 03:58:03 +08:00
< section >< h2 > 留言 </ h2 >{ user ? <>< form action = "/api/comments" method = "post" >< input type = "hidden" name = "postId" value = { id } />< textarea name = "content" required placeholder = "在 Hub 留下留言" />< button > 送出留言 </ button ></ form >< details >< summary > 檢舉這篇貼文 </ summary >{ query . reported && < p > 已收到檢舉,管理員會審核。 </ p >}< form action = "/api/reports" method = "post" >< input type = "hidden" name = "postId" value = { id } />< label > 原因 < input name = "reason" required minLength = { 3 } maxLength = { 500 } /></ label >< button className = "danger" > 送出檢舉 </ button ></ form ></ details ></> : < p > 請先登入以留言、互動或檢舉。 </ p >}{ comments . map (( comment ) => < div className = "card" key = { comment . id }>< strong > @ { comment . username }</ strong >< p >{ comment . content }</ p >< span className = "meta" >{ new Date ( comment . created_at ). toLocaleString ( "zh-TW" )}</ span ></ div >)}</ section >
2026-07-19 03:16:18 +08:00
</ article >;
}