feat(products): enable externalUrl redirect and external-site badge
- product 模型补 externalUrl 字段(CMS 可编辑),企业套装详情页支持外链跳转(复用 standalone 模板) - 矩阵卡 externalUrl 产品加「外部站点」徽标(对齐 Google / 字节 / 腾讯对标) - 清理 3 个无引用 AoyagiReisho 字体文件(性能优化) - 测试 17/17 + 视觉基线三端更新,type-check / lint 0 errors
This commit is contained in:
@@ -119,7 +119,7 @@
|
|||||||
- 方向:官网品牌宣传为主,产品矩阵页保留为聚合入口——每产品一句话定位 + 指标证据 + 外链独立站
|
- 方向:官网品牌宣传为主,产品矩阵页保留为聚合入口——每产品一句话定位 + 指标证据 + 外链独立站
|
||||||
- 成熟产品走 `externalUrl` 外链(NovaVis → novavis.p.novalon.cn 已实践);未成熟产品官网详情页占位,独立站上线后切换外链
|
- 成熟产品走 `externalUrl` 外链(NovaVis → novavis.p.novalon.cn 已实践);未成熟产品官网详情页占位,独立站上线后切换外链
|
||||||
- 参照案例(已核实):IHG 品牌组合页、字节跳动 bytedance.com/products、腾讯 tencent.com/business.html(模式 B:品牌+品牌矩阵聚合页);对照金蝶/用友(模式 C:产品为主,官网承载详情与转化)
|
- 参照案例(已核实):IHG 品牌组合页、字节跳动 bytedance.com/products、腾讯 tencent.com/business.html(模式 B:品牌+品牌矩阵聚合页);对照金蝶/用友(模式 C:产品为主,官网承载详情与转化)
|
||||||
- **实施状态**:Phase 1 机制(externalUrl 外链)已就绪;Phase 2 产品矩阵页品牌化改造已完成(`30018bf`);Phase 3 全站对齐核查通过(导航产品入口 / 首页 CTA / 方案页产品组合均已符合品牌化,无需改动);Phase 4 SEO 机制就绪(standalone 外链 redirect、sitemap 收录 /methodology);**信任证据**:方法论已 CMS 化并上线 /methodology 页面骨架(methodology 内容模型 + 首页 CTA 指向),案例与量化成果待真实素材补齐
|
- **实施状态**:Phase 1 机制(externalUrl 外链)已就绪;Phase 2 产品矩阵页品牌化改造已完成(`30018bf`);Phase 3 全站对齐核查通过 + Hero 视觉去产品化(`cbdc7dd`);Phase 4 SEO 机制就绪(standalone 外链 redirect、sitemap 收录 /methodology);**外链机制补全**:product 模型补 externalUrl + 详情页 redirect + 矩阵卡「外部站点」徽标(对齐 Google / 字节 / 腾讯对标案例);**信任证据**:方法论已 CMS 化并上线 /methodology 页面骨架(methodology 内容模型 + 首页 CTA 指向),案例与量化成果待真实素材补齐
|
||||||
|
|
||||||
## 项目进度
|
## 项目进度
|
||||||
|
|
||||||
@@ -174,6 +174,7 @@
|
|||||||
- [x] **文档同步 (2026-08-02)**:test-strategy-plan.md 版本 1.5,已标记已实现项,保留真实缺口
|
- [x] **文档同步 (2026-08-02)**:test-strategy-plan.md 版本 1.5,已标记已实现项,保留真实缺口
|
||||||
- [x] **官网产品模块定位决策(2026-08-19)**:确认 IHG/字节式品牌矩阵方向(品牌宣传为主 + 产品矩阵聚合页保留 + 成熟产品外链独立站),已写入 CONTEXT.md / README.md;Phase 0 文档化 + Phase 2 矩阵页品牌化改造完成,Phase 3 全站核查通过,Phase 4 SEO 机制就绪
|
- [x] **官网产品模块定位决策(2026-08-19)**:确认 IHG/字节式品牌矩阵方向(品牌宣传为主 + 产品矩阵聚合页保留 + 成熟产品外链独立站),已写入 CONTEXT.md / README.md;Phase 0 文档化 + Phase 2 矩阵页品牌化改造完成,Phase 3 全站核查通过,Phase 4 SEO 机制就绪
|
||||||
- [x] **方法论 CMS 化(Phase 4 信任证据·方法论骨架)**:新增 `methodology` 内容模型 + `/methodology` 页面骨架(Hero / 四阶段 / 空态 / CTA),首页 CTA「了解我们的方法论」指向 /methodology,sitemap 收录,视觉基线已生成
|
- [x] **方法论 CMS 化(Phase 4 信任证据·方法论骨架)**:新增 `methodology` 内容模型 + `/methodology` 页面骨架(Hero / 四阶段 / 空态 / CTA),首页 CTA「了解我们的方法论」指向 /methodology,sitemap 收录,视觉基线已生成
|
||||||
|
- [x] **产品外链机制补全(对齐 Google / 字节 / 腾讯对标)**:product 模型补 externalUrl 字段 + 企业套装详情页 redirect(复用 standalone 模板)+ 矩阵卡「外部站点」徽标;清理 3 个无引用 AoyagiReisho 字体文件(性能优化)
|
||||||
- [ ] **信任证据补齐(Phase 4 内容待办)**:首批客户共创案例与可量化成果——需提供真实素材后实施(零编造原则)
|
- [ ] **信任证据补齐(Phase 4 内容待办)**:首批客户共创案例与可量化成果——需提供真实素材后实施(零编造原则)
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 806 KiB After Width: | Height: | Size: 808 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.5 MiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import { COMPANY_INFO } from '@/lib/constants';
|
import { COMPANY_INFO } from '@/lib/constants';
|
||||||
import { getPublishedItemBySlug, getAllPublishedSlugs } from '@/lib/cms/data-server';
|
import { getPublishedItemBySlug, getAllPublishedSlugs } from '@/lib/cms/data-server';
|
||||||
import type { Product } from '@/lib/constants/products';
|
import type { Product } from '@/lib/constants/products';
|
||||||
@@ -36,6 +36,12 @@ export default async function ProductDetailPage({ params }: { params: Promise<{
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 若配置 externalUrl,则跳转到独立子域名产品站(对齐品牌矩阵「成熟产品外链」机制)
|
||||||
|
const externalUrl = item.data.externalUrl as string | undefined;
|
||||||
|
if (externalUrl) {
|
||||||
|
redirect(externalUrl);
|
||||||
|
}
|
||||||
|
|
||||||
const product = { ...item.data, title: item.data.title || item.title } as unknown as Product;
|
const product = { ...item.data, title: item.data.title || item.title } as unknown as Product;
|
||||||
return <ProductDetailContentV3 product={JSON.parse(JSON.stringify(product))} />;
|
return <ProductDetailContentV3 product={JSON.parse(JSON.stringify(product))} />;
|
||||||
}
|
}
|
||||||
@@ -77,6 +77,7 @@ jest.mock('lucide-react', () => {
|
|||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
ArrowRight: mockIcon('arrow-right'),
|
ArrowRight: mockIcon('arrow-right'),
|
||||||
|
ArrowUpRight: mockIcon('arrow-up-right'),
|
||||||
Package: mockIcon('package'),
|
Package: mockIcon('package'),
|
||||||
Cpu: mockIcon('cpu'),
|
Cpu: mockIcon('cpu'),
|
||||||
TrendingUp: mockIcon('trendingup'),
|
TrendingUp: mockIcon('trendingup'),
|
||||||
@@ -188,6 +189,7 @@ const mockSpecializedProducts: Product[] = [
|
|||||||
image: '/novavis.png',
|
image: '/novavis.png',
|
||||||
heroThemeId: 'novavis',
|
heroThemeId: 'novavis',
|
||||||
bundle: 'standalone',
|
bundle: 'standalone',
|
||||||
|
externalUrl: 'https://novavis.p.novalon.cn',
|
||||||
scenario: '面向执法机关的离线智能数据分析与资金关系图谱平台',
|
scenario: '面向执法机关的离线智能数据分析与资金关系图谱平台',
|
||||||
metrics: [
|
metrics: [
|
||||||
{ value: '100万+', label: '秒级处理' },
|
{ value: '100万+', label: '秒级处理' },
|
||||||
@@ -253,6 +255,13 @@ describe('ProductsContentV3 - 产品列表页集成测试', () => {
|
|||||||
expect(screen.getAllByText('NovaVis 数据可视化平台').length).toBeGreaterThanOrEqual(1);
|
expect(screen.getAllByText('NovaVis 数据可视化平台').length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show external-site badge for products with externalUrl', async () => {
|
||||||
|
const { default: ProductsContentV3 } = await import('./products-content-v3');
|
||||||
|
render(<ProductsContentV3 products={allMockProducts} />);
|
||||||
|
|
||||||
|
expect(screen.getAllByText('外部站点').length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('should render product capabilities on cards', async () => {
|
it('should render product capabilities on cards', async () => {
|
||||||
const { default: ProductsContentV3 } = await import('./products-content-v3');
|
const { default: ProductsContentV3 } = await import('./products-content-v3');
|
||||||
render(<ProductsContentV3 products={allMockProducts} />);
|
render(<ProductsContentV3 products={allMockProducts} />);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { EASE_OUT } from '@/components/ui/page-decoration';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { StaticLink } from '@/components/ui/static-link';
|
import { StaticLink } from '@/components/ui/static-link';
|
||||||
import { BentoItem } from '@/components/sections/bento-grid';
|
import { BentoItem } from '@/components/sections/bento-grid';
|
||||||
import { ArrowRight, Package, Cpu, TrendingUp, Users, Settings, FileText } from 'lucide-react';
|
import { ArrowRight, ArrowUpRight, Package, Cpu, TrendingUp, Users, Settings, FileText } from 'lucide-react';
|
||||||
import type { Product } from '@/lib/constants/products';
|
import type { Product } from '@/lib/constants/products';
|
||||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -176,7 +176,15 @@ function ProductCard({ product, size = 'small' }: { product: Product; size?: 'la
|
|||||||
{product.title}
|
{product.title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<span className="px-2 py-1 text-xs bg-brand/10 text-brand" aria-hidden="true">{bundleLabel}</span>
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<span className="px-2 py-1 text-xs bg-brand/10 text-brand" aria-hidden="true">{bundleLabel}</span>
|
||||||
|
{isExternal && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs border border-border-primary text-text-secondary">
|
||||||
|
<ArrowUpRight className="w-3 h-3" aria-hidden="true" />
|
||||||
|
外部站点
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className={cn('text-text-secondary mb-4 leading-relaxed flex-1', isLarge ? 'text-base' : 'text-sm')}>
|
<p className={cn('text-text-secondary mb-4 leading-relaxed flex-1', isLarge ? 'text-base' : 'text-sm')}>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -291,6 +291,12 @@ const productFields: FieldDefinition[] = [
|
|||||||
{ label: '已发布', value: '已发布' },
|
{ label: '已发布', value: '已发布' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'externalUrl',
|
||||||
|
type: 'text',
|
||||||
|
label: '外部跳转链接',
|
||||||
|
description: '若设置,访问该产品详情页时将跳转到此 URL(如独立子域名产品)',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'overview',
|
name: 'overview',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
|
|||||||
Reference in New Issue
Block a user