fix(seo): 修复页面标题公司名重复与品牌名繁简体不一致问题
- 新增 COMPANY_INFO.displayName 属性用于页面标题和SEO元数据 - 统一所有页面 metadata 使用 displayName(简体"睿新致远") - 视觉展示元素保留 shortName(繁体"睿新致遠"配合青柳隷書字体) - 修复关于/联系/团队页面标题中公司名重复出现的问题 - 修复新闻ID从数字改为SEO友好slug - 更新结构化数据使用完整公司名 - 修复ESLint报错:引号转义、组件displayName、any类型替换
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,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: '/' },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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={`
|
||||
|
||||
@@ -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 = '了解方案',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user