/** * # 路由 文章详情 /blog/$slug/ * 原站模板形态:左主栏(白卡正文 + 目录)+ 右侧栏(社交图标 / CONTACT US 表单 / * IoT Gateways 产品分组折叠菜单)+ 底部 Related Posts;Elementor 文章通栏直出。 */ import { createFileRoute, Link } from "@tanstack/react-router"; import { getLayoutData, Layout } from "~/components/website/Layout"; import { ElementorRenderer, extractElementor } from "~/components/elementor/Renderer"; import { InquiryForm } from "~/components/website/InquiryForm"; import { Button } from "~/components/ui/button"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "~/components/ui/accordion"; import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from "~/components/ui/breadcrumb"; import { getPublishedContent, getMenuTree, listPosts, type MenuNode, type PostListItem } from "~/server/data"; import { PostHero } from "~/components/website/PostHero"; import { resolveImageVariants } from "~/utils/image-variant"; import { useT } from "~/i18n"; /** # 常量 文章页三列布局样式(single 模板取证:60/776/360,容器 1200,移动端堆叠) */ const POST_LAYOUT_CSS = ` .ds-postlayout { background: #fff; } .ds-postlayout .ds-wrap { display: flex; align-items: flex-start; max-width: 1200px; margin: 0 auto; padding: 32px 0 40px; } .ds-postlayout .ds-rail { width: 60px; flex: 0 0 60px; } .ds-postlayout .ds-main { width: 776px; flex: 0 0 776px; min-width: 0; padding: 0 32px; } .ds-postlayout .ds-side { width: 360px; flex: 0 0 360px; padding: 0 10px; display: flex; flex-direction: column; gap: 20px; } @media (max-width: 1024px) { .ds-postlayout .ds-wrap { flex-direction: column; } .ds-postlayout .ds-rail { display: none; } .ds-postlayout .ds-main, .ds-postlayout .ds-side { width: 100%; flex: 1 1 auto; } } `; /** # 组件 分享竖栏(原站 elementor-share-buttons:facebook/twitter/linkedin 40px 方块) */ function ShareRail() { const btns = [ { label: "Share on facebook", bg: "#1868d5", path: "M504 256C504 119 393 8 256 8S8 119 8 256c0 123.78 90.69 226.38 209.25 245V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.28c-30.8 0-40.41 19.12-40.41 38.73V256h68.78l-11 71.69h-57.78V501C413.31 482.38 504 379.78 504 256z" }, { label: "Share on twitter", bg: "#1d9bf0", path: "M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z" }, { label: "Share on linkedin", bg: "#0a66c2", path: "M504 256c0 137-111 248-248 248S8 393 8 256 119 8 256 8s248 111 248 248zm-379-56.5v193h64.7V199.5H125zm32.4-84.2c-11.3 0-20.4 9.2-20.4 20.4 0 11.3 9.2 20.4 20.4 20.4 11.3 0 20.4-9.2 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4zM399 265.9c0-50.7-27-74.2-63-74.2-29 0-42 16-49.2 27.2v-23.4H222c.9 17.6 0 193 0 193h64.7v-107.8c0-5.8.4-11.6 2.1-15.7 4.7-11.6 15.3-23.7 33.1-23.7 23.4 0 32.7 17.8 32.7 43.9v103.3H419l-.1-122.6z" }, ]; return (
{btns.map((b) => ( e.preventDefault()} className="flex h-10 w-10 items-center justify-center rounded text-white" style={{ background: b.bg }}> ))}
); } /** # 组件 文章页侧栏(社交图标 + CONTACT US 表单 + IoT Gateways 折叠组) */ function PostSidebarWidget(props: { groups: MenuNode[]; productInterest: string }) { const t = useT(); return ( <>
{props.groups.length > 0 ? (

{t("blog_gateways")}

{props.groups.map((g) => ( {g.title} ))}
) : null} ); } /** # 组件 社交品牌图标(lucide 新版已移除品牌图标,内联 SVG) */ function BrandIcon(props: { label: string; bg: string; path: string }) { return ( ); } export const Route = createFileRoute("/blog/$slug")({ loader: async ({ params }) => { const [layout, post, latest, productMenu] = await Promise.all([ getLayoutData(), getPublishedContent(params.slug, "post").catch(() => null), listPosts(1, 6).catch(() => ({ items: [] as PostListItem[], total: 0, page: 1, pageSize: 6 })), getMenuTree("menu-1").catch(() => [] as MenuNode[]), ]); // 侧栏 IoT Gateways 折叠组 = 主菜单 Products 分支的子分组(By Protocols / Applications ...) const gwGroups = productMenu.find((n) => n.title.trim().toLowerCase() === "products")?.children ?? []; if (!post) return { layout, post: null, related: [] as PostListItem[], gwGroups }; // meta 收窄为具体 JSON 类型;image widget WP 尺寸变体解析 const meta = (post.meta ?? null) as Record | null; resolveImageVariants(meta?.elementor); const terms = post.terms.map((t) => ({ name: t.name, slug: t.slug, taxonomy: t.taxonomy })); const related = latest.items.filter((p) => p.slug !== params.slug).slice(0, 4); return { layout, post: { ...post, meta, terms }, related, gwGroups }; }, head: ({ loaderData }) => ({ meta: [ { title: loaderData?.post ? loaderData.post.seoTitle || `${loaderData.post.title} | Dusun IoT Blog` : "Post Not Found | Dusun IoT" }, { name: "description", content: loaderData?.post?.seoDescription || loaderData?.post?.excerpt || "Blog post from Dusun IoT." }, ], }), component: BlogPostPage, }); /** # 组件 文章详情页 */ function BlogPostPage() { const t = useT(); const data = Route.useLoaderData(); if (!data.post) { return (

404

{t("blog_not_found")}

); } const post = data.post; const cats = post.terms.filter((t) => t.taxonomy === "category"); const elementor = extractElementor(post.meta); // Elementor 文章(原站 single 模板):hero + 面包屑条 + 三列正文 // (cd804a1 取证 2026-11-03:分享栏 60px / 正文 776px / 侧栏 360px,容器 1200) if (elementor) { return (
| null)?.page_css as string | undefined} />
); } // HTML 文章:从正文提取 h2/h3 生成目录(并注入锚点 id) const toc = post.html ? buildToc(post.html) : null; return (
Home Blog {post.title}
{/* 主栏:白卡正文(目录 + 内容) */}

{post.title}

{post.publishedAt ? new Date(post.publishedAt).toISOString().slice(0, 10) : ""} {cats.length > 0 ? ( <> {" · "} {cats.map((c, i) => ( {i > 0 ? ", " : ""} {c.name} ))} ) : null}
{post.featuredUrl ? ( {post.featuredAlt ) : null} {toc && toc.items.length >= 3 ? ( {t("blog_toc")} ) : null} {elementor ? ( ) : post.html ? (
) : (

{post.excerpt || t("blog_importing")}

)}
{/* 侧栏:社交图标 / CONTACT US 表单 / IoT Gateways 产品分组 */}
{/* 底部 Related Posts(原站:4 列卡片,灰渐变占位 + 标题) */} {data.related.length > 0 ? (

{t("blog_related_posts")}

{data.related.map((p) => (
{p.imageUrl ? {p.title} : null}
{p.title}
))}
) : null}
); } /** # 函数 从 HTML 提取 h2/h3 生成目录条目,并给标题注入锚点 id * 参数 html 文章正文 HTML * 返回值 items 目录条目(level/text/id);html 注入 id 后的正文 */ function buildToc(html: string): { items: { level: number; text: string; id: string }[]; html: string } { const items: { level: number; text: string; id: string }[] = []; let i = 0; const out = html.replace(/]*)>([\s\S]*?)<\/h\1>/gi, (m, lvl: string, attrs: string, inner: string) => { const text = inner.replace(/<[^>]*>/g, "").trim(); if (!text || items.length >= 40) return m; const id = `toc-${i++}`; items.push({ level: Number(lvl), text, id }); return `${inner}`; }); return { items, html: out }; }