fix(seo): 修复页面标题公司名重复与品牌名繁简体不一致问题

- 新增 COMPANY_INFO.displayName 属性用于页面标题和SEO元数据
- 统一所有页面 metadata 使用 displayName(简体"睿新致远")
- 视觉展示元素保留 shortName(繁体"睿新致遠"配合青柳隷書字体)
- 修复关于/联系/团队页面标题中公司名重复出现的问题
- 修复新闻ID从数字改为SEO友好slug
- 更新结构化数据使用完整公司名
- 修复ESLint报错:引号转义、组件displayName、any类型替换
This commit is contained in:
张翔
2026-05-03 09:15:14 +08:00
parent f0657ce9f4
commit 1bf22a8f95
29 changed files with 183 additions and 76 deletions
+30 -2
View File
@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import {
updateConsentDetailed,
trackButtonClick,
@@ -13,19 +13,37 @@ import { motion, AnimatePresence } from 'framer-motion';
const LEGACY_CONSENT_KEY = 'ga_consent';
let hasConsentBeenHandled = false;
function getInitialShowConsent(): boolean {
if (typeof window === 'undefined') {return false;}
if (hasConsentBeenHandled) {return false;}
const stored = getStoredPreferences();
if (stored) {return false;}
const legacyConsent = localStorage.getItem(LEGACY_CONSENT_KEY);
if (legacyConsent) {return false;}
return false;
}
export function CookieConsent() {
const [showConsent, setShowConsent] = useState(false);
const [showConsent, setShowConsent] = useState(getInitialShowConsent);
const [showSettings, setShowSettings] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const [preferences, setPreferences] = useState<CookiePreferences>(getDefaultPreferences());
const consentCheckedRef = useRef(false);
useEffect(() => {
if (consentCheckedRef.current) {return;}
consentCheckedRef.current = true;
const stored = getStoredPreferences();
if (stored) {
hasConsentBeenHandled = true;
updateConsentDetailed(stored);
} else {
const legacyConsent = localStorage.getItem(LEGACY_CONSENT_KEY);
if (legacyConsent) {
hasConsentBeenHandled = true;
const migratedPrefs: CookiePreferences = {
necessary: true,
analytics: legacyConsent === 'granted',
@@ -45,8 +63,18 @@ export function CookieConsent() {
return undefined;
}, []);
useEffect(() => {
const handleOpenSettings = () => {
setShowSettings(true);
setShowConsent(true);
};
window.addEventListener('open-cookie-settings', handleOpenSettings);
return () => window.removeEventListener('open-cookie-settings', handleOpenSettings);
}, []);
const handleSavePreferences = useCallback((prefs: CookiePreferences) => {
setIsAnimating(true);
hasConsentBeenHandled = true;
const finalPrefs = { ...prefs, timestamp: Date.now() };
storePreferences(finalPrefs);
updateConsentDetailed(finalPrefs);
+2
View File
@@ -35,6 +35,8 @@ jest.mock('lucide-react', () => ({
jest.mock('@/lib/constants', () => ({
COMPANY_INFO: {
name: '四川睿新致远科技有限公司',
shortName: '睿新致遠',
displayName: '睿新致远',
description: '以智慧连接数字趋势,以伙伴身份陪您成长',
email: 'contact@novalon.cn',
phone: '028-88888888',
+15 -8
View File
@@ -15,28 +15,32 @@ jest.mock('next/navigation', () => ({
}));
jest.mock('next/link', () => {
return ({ children, href, onClick, ...props }: any) => (
const MockLink = ({ children, href, onClick, ...props }: { children: React.ReactNode; href: string; onClick?: () => void; [key: string]: unknown }) => (
<a href={href} onClick={onClick} {...props}>
{children}
</a>
);
MockLink.displayName = 'MockLink';
return MockLink;
});
jest.mock('next/image', () => {
return ({ src, alt, width, height, className, ...props }: any) => (
const MockImage = ({ src, alt, width, height, className, ...props }: { src: string; alt: string; width: number; height: number; className?: string; [key: string]: unknown }) => (
<img src={src} alt={alt} width={width} height={height} className={className} {...props} />
);
MockImage.displayName = 'MockImage';
return MockImage;
});
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, className, ...props }: any) => (
div: ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
<div className={className} {...props}>
{children}
</div>
),
},
AnimatePresence: ({ children }: any) => <>{children}</>,
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
jest.mock('lucide-react', () => ({
@@ -44,18 +48,21 @@ jest.mock('lucide-react', () => ({
X: () => <span data-testid="x-icon" />,
}));
jest.mock('@/components/ui/button', () => ({
Button: ({ children, className, asChild, ...props }: any) => (
jest.mock('@/components/ui/button', () => {
const MockButton = ({ children, className, ...props }: { children: React.ReactNode; className?: string; [key: string]: unknown }) => (
<button className={className} {...props}>
{children}
</button>
),
}));
);
MockButton.displayName = 'MockButton';
return { Button: MockButton };
});
jest.mock('@/lib/constants', () => ({
COMPANY_INFO: {
name: '四川睿新致远科技有限公司',
shortName: '睿新致遠',
displayName: '睿新致远',
},
NAVIGATION: [
{ id: 'home', label: '首页', href: '/' },
+7 -3
View File
@@ -108,7 +108,11 @@ function HeaderContent() {
label={item.label}
items={MEGA_DROPDOWN_DATA[item.dropdownKey!] ?? []}
isOpen={openDropdown === item.id}
onToggle={() => setOpenDropdown(openDropdown === item.id ? null : item.id)}
onToggle={() => {
setOpenDropdown((prev) => prev === item.id ? null : item.id);
}}
onOpen={() => setOpenDropdown(item.id)}
onClose={() => setOpenDropdown((prev) => prev === item.id ? null : prev)}
/>
) : (
<StaticLink
@@ -218,13 +222,13 @@ function HeaderContent() {
</motion.div>
))}
<div className="mt-6 px-4 pt-6 border-t border-[#E2E8F0]">
<Button
<Button
className="w-full"
asChild
size="lg"
>
<StaticLink href="/contact" onClick={() => setIsOpen(false)}>
</StaticLink>
</Button>
</div>
+41 -2
View File
@@ -11,10 +11,15 @@ interface MegaDropdownProps {
items: MegaDropdownItem[];
isOpen: boolean;
onToggle: () => void;
onOpen?: () => void;
onClose?: () => void;
}
export function MegaDropdown({ label, items, isOpen, onToggle }: MegaDropdownProps) {
const HOVER_DELAY = 150;
export function MegaDropdown({ label, items, isOpen, onToggle, onOpen, onClose }: MegaDropdownProps) {
const dropdownRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
@@ -26,8 +31,42 @@ export function MegaDropdown({ label, items, isOpen, onToggle }: MegaDropdownPro
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen, onToggle]);
useEffect(() => {
return () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
};
}, []);
const handleMouseEnter = () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = null;
}
if (!isOpen) {
if (onOpen) {
onOpen();
} else {
onToggle();
}
}
};
const handleMouseLeave = () => {
hoverTimeoutRef.current = setTimeout(() => {
if (isOpen) {
if (onClose) {
onClose();
} else {
onToggle();
}
}
}, HOVER_DELAY);
};
return (
<div ref={dropdownRef} className="relative">
<div ref={dropdownRef} className="relative" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<button
onClick={onToggle}
className={`
+2 -1
View File
@@ -4,6 +4,7 @@ import { motion } from 'framer-motion';
import { StaticLink } from '@/components/ui/static-link';
import { Button } from '@/components/ui/button';
import { ArrowRight } from 'lucide-react';
import { COMPANY_INFO } from '@/lib/constants';
interface CTASectionProps {
title?: string;
@@ -16,7 +17,7 @@ interface CTASectionProps {
export function CTASection({
title = '开启您的数字化转型之旅',
description = '与睿新致遠一起,让技术成为您业务增长的核心引擎',
description = `${COMPANY_INFO.shortName}一起,让技术成为您业务增长的核心引擎`,
primaryLabel = '立即咨询',
primaryHref = '/contact',
secondaryLabel = '了解方案',
+3 -2
View File
@@ -1,6 +1,7 @@
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { OrganizationSchema, WebsiteSchema } from './structured-data';
import { COMPANY_INFO } from '@/lib/constants';
describe('StructuredData', () => {
beforeEach(() => {
@@ -18,7 +19,7 @@ describe('StructuredData', () => {
const { container } = render(<OrganizationSchema />);
const script = container.querySelector('script[type="application/ld+json"]');
const schema = JSON.parse(script?.textContent || '{}');
expect(schema.name).toBe('四川睿新致远科技有限公司');
expect(schema.name).toBe(COMPANY_INFO.name);
});
it('should contain organization type', () => {
@@ -54,7 +55,7 @@ describe('StructuredData', () => {
const { container } = render(<WebsiteSchema />);
const script = container.querySelector('script[type="application/ld+json"]');
const schema = JSON.parse(script?.textContent || '{}');
expect(schema.name).toBe('四川睿新致远科技有限公司');
expect(schema.name).toBe(COMPANY_INFO.name);
});
});
});
+5 -3
View File
@@ -1,8 +1,10 @@
import { COMPANY_INFO } from '@/lib/constants';
export function OrganizationSchema() {
const schema = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "四川睿新致远科技有限公司",
"name": COMPANY_INFO.name,
"alternateName": "诺瓦隆",
"url": "https://www.novalon.cn",
"logo": "https://www.novalon.cn/logo.svg",
@@ -32,7 +34,7 @@ export function WebsiteSchema() {
const schema = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "四川睿新致远科技有限公司",
"name": COMPANY_INFO.name,
"url": "https://www.novalon.cn",
"potentialAction": {
"@type": "SearchAction",
@@ -56,7 +58,7 @@ export function ServiceSchema() {
"serviceType": "企业数字化转型服务",
"provider": {
"@type": "Organization",
"name": "四川睿新致远科技有限公司"
"name": COMPANY_INFO.name
},
"description": "提供软件开发、云计算、数据分析、信息安全等一站式数字化转型解决方案",
"areaServed": {
+4 -3
View File
@@ -2,6 +2,7 @@
import { useRef, useState, useCallback, type ReactNode } from 'react';
import { motion } from 'framer-motion';
import { StaticLink } from '@/components/ui/static-link';
interface InkGlowCardProps {
children: ReactNode;
@@ -33,7 +34,7 @@ export function InkGlowCard({
const [isHovered, setIsHovered] = useState(false);
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!cardRef.current) return;
if (!cardRef.current) {return;}
const rect = cardRef.current.getBoundingClientRect();
setMousePos({
x: e.clientX - rect.left,
@@ -72,7 +73,7 @@ export function InkGlowCard({
} as React.CSSProperties}
>
{href ? (
<a
<StaticLink
href={href}
className="relative block rounded-2xl bg-white overflow-hidden transition-all duration-500"
style={{
@@ -86,7 +87,7 @@ export function InkGlowCard({
onMouseLeave={() => setIsHovered(false)}
>
{content}
</a>
</StaticLink>
) : (
<div
className="relative rounded-2xl bg-white overflow-hidden transition-all duration-500"