/** * # 路由 兜底 /$.tsx(catch-all) * 处理 WordPress 风格的页面/文章路径: * 1. 语言前缀(如 /zh-CN/xxx/):剥离语言段,渲染主语言内容占位(title 加语言标注)。 * 2. 单段路径:按 page 取内容。 * 3. 两段路径:先按 post(/%category%/%postname%/)取,再按 page。 * 4. 都没有 → 404 NotFound(导航 + 热门产品链接)。 */ import { useEffect, useState } from "react"; import { createFileRoute, Link, notFound, redirect } from "@tanstack/react-router"; import { getLayoutData, Layout, type LayoutData } from "~/components/website/Layout"; import { Pagination } from "~/components/website/Pagination"; import { ElementorRenderer, extractElementor, type ElElement } from "~/components/elementor/Renderer"; import { InquiryForm } from "~/components/website/InquiryForm"; import { Button } from "~/components/ui/button"; import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from "~/components/ui/breadcrumb"; import { ProductCard } from "~/components/website/ProductCard"; import { isLangCode, LANG_NAMES, pickContent, type CatchAllData } from "~/components/website/catchall-shared"; import { getEmbeddedTemplate, getPublishedContent, getPostCategoryBySlug, getProductChildCategory, hasProductSlug, listPosts, listProducts, loadPostsWidgetsData, resolveRedirect, type PostCardItem, type ProductListResult, } from "~/server/data"; import { useT } from "~/i18n"; /** # 函数 把 Elementor 文档归一化为元素数组(裸数组 / {elements} 两种形态) */ function asElements(doc: unknown): ElElement[] { if (Array.isArray(doc)) return doc as ElElement[]; const els = (doc as { elements?: unknown } | null)?.elements; return Array.isArray(els) ? (els as ElElement[]) : []; } /** # 函数 扫描文档里的 [elementor-template id="N"] 短代码并加载模板 * 参数 elements 主文档元素树 * 返回值 templates: shortcode widget id → 模板元素树;elements: 模板元素树拼接 * (供 posts widget 预取) */ async function loadEmbeddedTemplates(elements: ElElement[]): Promise<{ templates: Record; elements: ElElement[] }> { const templates: Record = {}; const collected: ElElement[] = []; const ids = new Map(); const walk = (els: ElElement[] | undefined) => { for (const el of els || []) { const st = (el.settings || {}) as Record; const shortcode = typeof st.shortcode === "string" ? st.shortcode : ""; const m = /\[elementor-template\s+id=["']?(\d+)/.exec(shortcode); if (m && el.id) ids.set(Number(m[1]), []); if (el.elements?.length) walk(el.elements); } }; walk(elements); for (const id of ids.keys()) { const row = await getEmbeddedTemplate(id); const doc = asElements(row?.meta ? (row.meta as Record).elementor : null); if (doc.length) { ids.set(id, doc); collected.push(...doc); } } // 按 widget id 回填模板(同一文档仅取第一个匹配的 widget) const walk2 = (els: ElElement[] | undefined) => { for (const el of els || []) { const st = (el.settings || {}) as Record; const shortcode = typeof st.shortcode === "string" ? st.shortcode : ""; const m = /\[elementor-template\s+id=["']?(\d+)/.exec(shortcode); if (m && el.id) { const doc = ids.get(Number(m[1])); if (doc?.length) templates[el.id] = doc; } if (el.elements?.length) walk2(el.elements); } }; walk2(elements); return { templates, elements: collected }; } export const Route = createFileRoute("/$")({ // 归档分页用 ?page=N validateSearch: (search: Record): { page?: number } => { const page = Number(search.page); return { page: Number.isFinite(page) && page > 0 ? Math.floor(page) : undefined }; }, loaderDeps: ({ search }) => ({ page: search.page }), loader: async ({ location, deps }) => { // 解析路径段:去首尾斜杠、过滤空段 const segments = location.pathname.split("/").filter(Boolean).map(decodeURIComponent); let lang = ""; if (segments.length >= 1 && isLangCode(segments[0])) { lang = segments[0]; segments.shift(); } const layout = await getLayoutData(); const hotProducts: ProductListResult = await listProducts({ sort: "views", pageSize: 4 }).catch( () => ({ items: [], total: 0, page: 1, pageSize: 4 }) as ProductListResult, ); // 语言占位阶段:内容库暂无翻译,取主语言内容渲染(title 加语言标注) let content: CatchAllData["content"] = null; if (segments.length === 1) { // WP 站点文章(post)与页面(page)都可能是单段路径(见原站 sitemap),两类都试 const page = await getPublishedContent(segments[0], "page").catch(() => null); if (page) content = pickContent(page); else { const post = await getPublishedContent(segments[0], "post").catch(() => null); if (post) content = pickContent(post); } } else if (segments.length >= 2) { // WP 路径结构 /%category%/.../%postname%/:末段即内容 slug(如 /blog/gateway//) const slug = segments[segments.length - 1]; // 先匹配文章再匹配页面 const post = await getPublishedContent(slug, "post").catch(() => null); if (post) content = pickContent(post); else { const page = await getPublishedContent(slug, "page").catch(() => null); if (page) content = pickContent(page); } } else if (segments.length === 0) { // 语言前缀首页 const home = await getPublishedContent("home", "page").catch(() => null); if (home) content = pickContent(home); } // 未命中内容时:先按 WP 结构化旧路径改判,再查 redirects 表,最后分类归档 if (!content && segments.length >= 1) { const last = segments[segments.length - 1]!; // 1) /product/product//:导入落地页里的重复前缀 → /product//(仅当产品存在) if (segments.length >= 3 && segments[0] === "product" && segments[1] === "product" && (await hasProductSlug(last).catch(() => false))) { const to = `/product/${last}/`; throw redirect({ to, href: to, statusCode: 301 as const }); } // 2) /product-specification/...:原站产品规格页。本地等价物是产品页;单段落产品列表 if (segments[0] === "product-specification") { if (segments.length === 1) { throw redirect({ to: "/collection/", href: "/collection/", statusCode: 301 as const }); } // 已导入的旧链规则优先(可能链向其它规格页 slug),无规则再按 slug 找产品,最后落产品列表 const rule = await resolveRedirect("/" + segments.join("/")) .then((r) => r?.target) .catch(() => null); if (rule) throw redirect({ to: rule, href: rule, statusCode: 301 as const }); const to = (await hasProductSlug(last).catch(() => false)) ? `/product/${last}/` : "/collection/"; throw redirect({ to, href: to, statusCode: 301 as const }); } // 3) /collection///:嵌套产品分类路径 → 子分类页(无该子分类则父分类兜底) if (segments[0] === "collection" && segments.length === 3) { const child = await getProductChildCategory(segments[1]!, segments[2]!).catch(() => null); const to = `/collection/${child ? child.slug : segments[1]!}/`; throw redirect({ to, href: to, statusCode: 301 as const }); } // 4) 数据库 redirects 表规则(链式解析并跳过自指规则防死循环) const target = await resolveRedirect("/" + segments.join("/")) .then((r) => r?.target) .catch(() => null); if (target) throw redirect({ to: target, href: target, statusCode: 301 as const }); // 5) WP /%category%/ 结构:单段/双段路径末段命中文章分类 → 分类归档页 if (segments.length <= 2) { const cat = await getPostCategoryBySlug(last).catch(() => null); if (cat) { const posts = await listPosts(deps.page || 1, 12, cat.slug).catch(() => ({ items: [], total: 0, page: deps.page || 1, pageSize: 12 })); return { layout, content: null, lang, hotProducts, archive: { name: cat.name, slug: cat.slug, posts }, templates: {}, postsData: {} } satisfies CatchAllData; } } // 未命中任何内容 → 404(SSR 返回 404 状态码,由 notFoundComponent 渲染) throw notFound(); } // 语言前缀首页等内容为空时的兜底 if (!content) throw notFound(); // 内嵌 Elementor 模板([elementor-template id="N"])+ posts widget 数据预取 const mainEls = asElements(extractElementor(content.meta)); const embedded = await loadEmbeddedTemplates(mainEls); const postsData = await loadPostsWidgetsData([...mainEls, ...embedded.elements]); return { layout, content, lang, hotProducts, archive: null, templates: embedded.templates, postsData } satisfies CatchAllData; }, head: ({ loaderData }) => { const langLabel = loaderData?.lang ? ` (${LANG_NAMES[loaderData.lang] || loaderData.lang})` : ""; if (loaderData?.archive) { return { meta: [ { title: `${loaderData.archive.name} | Dusun IoT` }, { name: "description", content: `Browse ${loaderData.archive.name} articles from Dusun IoT.` }, ], }; } return { meta: [ { title: loaderData?.content ? (loaderData.content.seoTitle || loaderData.content.title) + langLabel : "Page Not Found | Dusun IoT" }, { name: "description", content: loaderData?.content?.seoDescription || loaderData?.content?.excerpt || "Dusun IoT - IoT gateway manufacturer and solution provider." }, ], }; }, component: CatchAllPage, notFoundComponent: NotFoundShell, }); /** # 组件 分类归档页(WP /%category%/ 结构):面包屑 + 文章卡片分页列表,样式与 /blog/ 一致 */ function ArchivePage() { const t = useT(); const data = Route.useLoaderData(); const a = data.archive!; const page = a.posts.page; return (

{a.name.toUpperCase()}

Home {a.name}
{a.posts.items.map((post) => ( {/* 与 /blog/ 卡片同构:无特色图时灰色渐变占位 + 右上角分类徽章 */}
{post.imageUrl ? ( {post.title} ) : (
)} {post.categoryName || a.name}
{post.title}
{post.excerpt ?

{post.excerpt.slice(0, 140)}…

: null}
{post.authorName ? `${post.authorName} · ` : ""} {post.publishedAt ? new Date(post.publishedAt).toISOString().slice(0, 10) : ""}
{t("blog_read_more")}
))} {a.posts.items.length === 0 ?

{t("blog_no_posts")}

: null}
`?page=${p}`} />
); } /** # 组件 404 外壳:先渲染静态 404,再客户端补齐布局与热门产品(loader 已抛 notFound,无 loaderData) */ function NotFoundShell() { const t = useT(); const [state, setState] = useState<{ layout: LayoutData | null; hot: ProductListResult | null }>({ layout: null, hot: null }); useEffect(() => { let alive = true; Promise.all([getLayoutData(), listProducts({ sort: "views", pageSize: 4 }).catch(() => null)]) .then(([layout, hot]) => { if (alive) setState({ layout, hot }); }) .catch(() => {}); return () => { alive = false; }; }, []); if (state.layout && state.hot) { return ( ); } return (

404

{t("nf_msg")}

); } /** # 组件 兜底页面(内容模板) */ function CatchAllPage() { const t = useT(); const data = Route.useLoaderData(); // 分类归档命中:走归档模板 if (data.archive) return ; const c = data.content; const elementor = extractElementor(c.meta); const langLabel = data.lang ? ` (${LANG_NAMES[data.lang] || data.lang})` : ""; // Elementor 页面(原站 full-canvas 模板):通栏直出,不加面包屑/h1 if (elementor) { return ( {data.lang ? (
You are viewing the {langLabel} placeholder version. Translated content is coming soon — the English content is shown below.
) : null} [ id, , ]), ) : undefined } />
); } return (
Home {c.title} {data.lang ? (
You are viewing the {langLabel} placeholder version. Translated content is coming soon — the English content is shown below.
) : null}

{c.title} {langLabel}

{elementor ? ( ) : c.html ? (
) : (

{c.excerpt || "This page is being imported. Content will appear here soon."}

)}
); } /** # 组件 404 NotFound:导航 + 热门产品链接 */ function NotFoundView(props: { hotProducts: ProductListResult }) { const t = useT(); return (

404

{t("nf_msg")}

{props.hotProducts.items.length > 0 ? ( <>

Popular Products

{props.hotProducts.items.map((p) => ( ))}
) : null}
); }