Skip to content

Commit 04a4ef9

Browse files
fix(community): CR fixes — sanitize OG cover URLs + Hero 按钮去嵌套
Copilot CR 指出的两条: PR #315 Hero.tsx:119 —— <a> 包 <button> 是嵌套交互元素(HTML 无效 + a11y 问题) 修法:把 <Link> 直接渲染成按钮样式,不再嵌套 <button> PR #316 LinkCard.tsx:52 / admin/community/page.tsx:143 —— OG 封面 URL 直接进 <img src> 没过白名单。 修法:用 lib/url-safety.ts 的 sanitizeMediaUrl 兜底,拦 javascript:/data: 协议 (后端 UrlNormalizer 是第一道防线,前端 sanitize 是 defense-in-depth)
1 parent 65293a0 commit 04a4ef9

3 files changed

Lines changed: 29 additions & 25 deletions

File tree

app/admin/community/page.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import { useEffect, useState } from "react";
1515
import { AdminGuard } from "@/app/admin/events/AdminGuard";
1616
import type { SharedLinkView } from "@/app/feed/types";
17-
import { sanitizeExternalUrl } from "@/lib/url-safety";
17+
import { sanitizeExternalUrl, sanitizeMediaUrl } from "@/lib/url-safety";
1818
import { approveLink, listPendingLinks, rejectLink } from "./lib";
1919

2020
export default function AdminCommunityPage() {
@@ -134,19 +134,23 @@ function AdminCommunityInner() {
134134
图床防盗链会检查 Referer,非本站来源返回"未经允许"裂图。
135135
next/image 的 remotePatterns 限制外站域名也一并规避。 */}
136136
<div className="w-full md:w-40 aspect-[16/9] flex-shrink-0 bg-neutral-100 dark:bg-neutral-900 relative overflow-hidden">
137-
{link.ogCover ? (
138-
// eslint-disable-next-line @next/next/no-img-element
139-
<img
140-
src={link.ogCover}
141-
alt={link.ogTitle ?? link.url}
142-
referrerPolicy="no-referrer"
143-
className="absolute inset-0 w-full h-full object-cover"
144-
/>
145-
) : (
146-
<span className="absolute inset-0 flex items-center justify-center text-3xl font-bold text-neutral-400">
147-
{link.host[0]?.toUpperCase() ?? "?"}
148-
</span>
149-
)}
137+
{(() => {
138+
// defense-in-depth:过 sanitizeMediaUrl 拦 javascript:/data: 协议
139+
const safeCover = sanitizeMediaUrl(link.ogCover);
140+
return safeCover ? (
141+
// eslint-disable-next-line @next/next/no-img-element
142+
<img
143+
src={safeCover}
144+
alt={link.ogTitle ?? link.url}
145+
referrerPolicy="no-referrer"
146+
className="absolute inset-0 w-full h-full object-cover"
147+
/>
148+
) : (
149+
<span className="absolute inset-0 flex items-center justify-center text-3xl font-bold text-neutral-400">
150+
{link.host[0]?.toUpperCase() ?? "?"}
151+
</span>
152+
);
153+
})()}
150154
</div>
151155

152156
{/* 中:元信息 */}

app/components/Hero.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,29 +94,26 @@ export async function Hero() {
9494
<p className="font-body text-sm mb-6 opacity-80">
9595
{t("join.body")}
9696
</p>
97-
{/* 双阅读入口:严肃文档 + 社区随手分享,视觉同构;投稿动作已在 Hero 左侧 Contribute/ShareLink */}
97+
{/* 双阅读入口:严肃文档 + 社区随手分享,视觉同构;投稿动作已在 Hero 左侧 Contribute/ShareLink。
98+
直接把 <Link> 渲染成按钮样式,避免 <a><button> 嵌套交互元素(HTML 无效 + 键盘/a11y 问题)。 */}
9899
<div className="flex flex-col gap-3">
99100
<Link
100101
href="/docs/learn/ai"
101-
className="block w-full"
102+
className="block w-full py-3 border border-[var(--background)] font-sans text-xs uppercase tracking-widest text-center hover:bg-[var(--background)] hover:text-[var(--foreground)] transition-all cursor-pointer"
102103
data-umami-event="navigation_click"
103104
data-umami-event-region="hero_cta"
104105
data-umami-event-label="Access Articles"
105106
>
106-
<button className="w-full py-3 border border-[var(--background)] font-sans text-xs uppercase tracking-widest hover:bg-[var(--background)] hover:text-[var(--foreground)] transition-all cursor-pointer">
107-
{t("cta.access")}
108-
</button>
107+
{t("cta.access")}
109108
</Link>
110109
<Link
111110
href="/feed"
112-
className="block w-full"
111+
className="block w-full py-3 border border-[var(--background)] font-sans text-xs uppercase tracking-widest text-center hover:bg-[var(--background)] hover:text-[var(--foreground)] transition-all cursor-pointer"
113112
data-umami-event="navigation_click"
114113
data-umami-event-region="hero_cta"
115114
data-umami-event-label="Community Feed"
116115
>
117-
<button className="w-full py-3 border border-[var(--background)] font-sans text-xs uppercase tracking-widest hover:bg-[var(--background)] hover:text-[var(--foreground)] transition-all cursor-pointer">
118-
{t("cta.feed")}
119-
</button>
116+
{t("cta.feed")}
120117
</Link>
121118
</div>
122119
</div>

app/feed/components/LinkCard.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useTranslations } from "next-intl";
1010
import type { SharedLinkView } from "@/app/feed/types";
1111
import { ReportButton } from "@/app/feed/components/ReportButton";
1212
import { Badge } from "@/components/ui/badge";
13+
import { sanitizeMediaUrl } from "@/lib/url-safety";
1314

1415
interface LinkCardProps {
1516
link: SharedLinkView;
@@ -27,6 +28,8 @@ function getHostInitial(host: string): string {
2728

2829
export function LinkCard({ link, categoryLabel, isLoggedIn }: LinkCardProps) {
2930
const t = useTranslations("feed.card");
31+
// defense-in-depth:过白名单协议拦 javascript:/data:,后端 UrlNormalizer 是第一道,这里是第二道
32+
const safeOgCover = sanitizeMediaUrl(link.ogCover);
3033

3134
return (
3235
<li className="group border border-[var(--foreground)] hover:border-[#CC0000] transition-colors duration-150 flex flex-col">
@@ -39,14 +42,14 @@ export function LinkCard({ link, categoryLabel, isLoggedIn }: LinkCardProps) {
3942
aria-label={link.ogTitle ?? link.url}
4043
>
4144
{/* OG 封面 / 占位块 */}
42-
{link.ogCover && !link.ogFetchFailed ? (
45+
{safeOgCover && !link.ogFetchFailed ? (
4346
// next/image 全站 unoptimized:true,用 img 即可(与 events 页一致)。
4447
// referrerPolicy="no-referrer":微信 mmbiz.qpic.cn 防盗链会检查 Referer,
4548
// 非 mp.weixin.qq.com 来源直接返回"未经允许使用"裂图;不发 Referer 时
4649
// 反而放行(微信客户端内打开文章浏览器也不发 Referer)。
4750
// eslint-disable-next-line @next/next/no-img-element
4851
<img
49-
src={link.ogCover}
52+
src={safeOgCover}
5053
alt={link.ogTitle ?? link.host}
5154
referrerPolicy="no-referrer"
5255
className="w-full aspect-[16/9] object-cover border-b border-[var(--foreground)]"

0 commit comments

Comments
 (0)