添加seo功能

This commit is contained in:
Eamon-meng
2026-05-15 16:56:05 +08:00
parent 670961c17d
commit fde097c601
23 changed files with 935 additions and 268 deletions

View File

@@ -0,0 +1,17 @@
import {ReactNode} from 'react'
import {Metadata} from 'next'
export async function generateMetadata(): Promise<Metadata> {
return {
title: '隐私政策',
description: '蓝狐代理隐私政策 - 了解我们如何收集、使用和保护您的个人信息',
openGraph: {
title: '隐私政策',
description: '蓝狐代理隐私政策 - 了解我们如何收集、使用和保护您的个人信息',
},
}
}
export default function PrivacyPolicyLayout({children}: {children: ReactNode}) {
return children
}

View File

@@ -0,0 +1,17 @@
import {ReactNode} from 'react'
import {Metadata} from 'next'
export async function generateMetadata(): Promise<Metadata> {
return {
title: '用户协议',
description: '蓝狐代理用户服务协议 - 使用服务前请仔细阅读用户协议条款',
openGraph: {
title: '用户协议',
description: '蓝狐代理用户服务协议 - 使用服务前请仔细阅读用户协议条款',
},
}
}
export default function UserAgreementLayout({children}: {children: ReactNode}) {
return children
}

View File

@@ -1,9 +1,34 @@
import {Metadata} from 'next'
import {siteConfig} from '@/config/site'
import {HeroSection} from './hero-section' import {HeroSection} from './hero-section'
import {StatsSection} from './stats-section' import {StatsSection} from './stats-section'
import {ProductTypesSection} from './product-types-section' import {ProductTypesSection} from './product-types-section'
import {AdvantagesSection} from './advantages-section' import {AdvantagesSection} from './advantages-section'
import {ArticlesSection} from './articles-section' import {ArticlesSection} from './articles-section'
export async function generateMetadata(): Promise<Metadata> {
return {
title: siteConfig.name,
description: siteConfig.description,
openGraph: {
title: siteConfig.name,
description: siteConfig.description,
url: siteConfig.url,
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: siteConfig.name,
},
],
},
alternates: {
canonical: siteConfig.url,
},
}
}
export default function Home() { export default function Home() {
return ( return (
<main className="flex flex-col gap-16 lg:gap-32 pb-16 lg:pb-32 bg-white"> <main className="flex flex-col gap-16 lg:gap-32 pb-16 lg:pb-32 bg-white">

View File

@@ -1,4 +1,6 @@
import {Metadata} from 'next'
import ScenePage, {ScenePageConfig} from '@/components/scene-page' import ScenePage, {ScenePageConfig} from '@/components/scene-page'
import {siteConfig} from '@/config/site'
import bannerImg from './_assets/banner.webp' import bannerImg from './_assets/banner.webp'
import solutionImg from './_assets/solution-main.webp' import solutionImg from './_assets/solution-main.webp'
import value1Img from './_assets/value-1.webp' import value1Img from './_assets/value-1.webp'
@@ -46,6 +48,28 @@ const config: ScenePageConfig = {
}, },
} }
export async function generateMetadata(): Promise<Metadata> {
return {
title: config.banner.title,
description: config.banner.description,
openGraph: {
title: config.banner.title,
description: config.banner.description,
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: config.banner.title,
},
],
},
alternates: {
canonical: `${siteConfig.url}/account-management`,
},
}
}
export default function AccountManagementPage() { export default function AccountManagementPage() {
return <ScenePage {...config}/> return <ScenePage {...config}/>
} }

View File

@@ -1,4 +1,6 @@
import {Metadata} from 'next'
import ScenePage, {ScenePageConfig} from '@/components/scene-page' import ScenePage, {ScenePageConfig} from '@/components/scene-page'
import {siteConfig} from '@/config/site'
import bannerImg from './_assets/banner.webp' import bannerImg from './_assets/banner.webp'
import solutionImg from './_assets/solution-main.webp' import solutionImg from './_assets/solution-main.webp'
import value1Img from './_assets/value-1.webp' import value1Img from './_assets/value-1.webp'
@@ -46,6 +48,28 @@ const config: ScenePageConfig = {
}, },
} }
export async function generateMetadata(): Promise<Metadata> {
return {
title: config.banner.title,
description: config.banner.description,
openGraph: {
title: config.banner.title,
description: config.banner.description,
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: config.banner.title,
},
],
},
alternates: {
canonical: `${siteConfig.url}/advertising`,
},
}
}
export default function AdvertisingPage() { export default function AdvertisingPage() {
return <ScenePage {...config}/> return <ScenePage {...config}/>
} }

View File

@@ -1,3 +1,5 @@
import {Metadata} from 'next'
import {siteConfig} from '@/config/site'
import BreadCrumb from '@/components/bread-crumb' import BreadCrumb from '@/components/bread-crumb'
import Wrap from '@/components/wrap' import Wrap from '@/components/wrap'
import Extract from '@/components/composites/extract' import Extract from '@/components/composites/extract'
@@ -5,6 +7,28 @@ import HomePage from '@/components/home/page'
export type CollectPageProps = {} export type CollectPageProps = {}
export async function generateMetadata(): Promise<Metadata> {
return {
title: 'IP提取',
description: '短效/长效IP提取高可用性代理IP支持API调用即时获取全国各地代理IP适用于数据采集、网络测试等场景',
openGraph: {
title: 'IP提取',
description: '短效/长效IP提取高可用性代理IP支持API调用即时获取全国各地代理IP',
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: 'IP提取',
},
],
},
alternates: {
canonical: `${siteConfig.url}/collect`,
},
}
}
export default function CollectPage(props: CollectPageProps) { export default function CollectPage(props: CollectPageProps) {
return ( return (
// <main className="mt-20 flex flex-col gap-4"> // <main className="mt-20 flex flex-col gap-4">

View File

@@ -0,0 +1,264 @@
'use client'
import {useState} from 'react'
import Image from 'next/image'
import {useRouter} from 'next/navigation'
import {useForm} from 'react-hook-form'
import {zodResolver} from '@hookform/resolvers/zod'
import {z} from 'zod'
import {toast} from 'sonner'
import HomePage from '@/components/home/page'
import Wrap from '@/components/wrap'
import {Form, FormField} from '@/components/ui/form'
import {Input} from '@/components/ui/input'
import {Button} from '@/components/ui/button'
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
import {merge} from '@/lib/utils'
import {submitInquiry} from '@/actions/inquiry'
import group from './_assets/Group.webp'
import SelfDesc from '@/components/features/self-desc'
const formSchema = z.object({
company: z.string().min(2, '企业名称至少2个字符'),
name: z.string().min(2, '联系人姓名至少2个字符'),
phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的11位手机号码'),
usage: z.string().min(1, '请选择您需要的用量'),
purpose: z.string().min(2, '请输入用途说明').max(200, '用途说明不超过200字符'),
})
type FormValues = z.infer<typeof formSchema>
export default function CustomPage() {
const router = useRouter()
const [isSubmitting, setIsSubmitting] = useState(false)
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
company: '',
name: '',
phone: '',
usage: '',
purpose: '',
},
})
const onSubmit = async (data: FormValues) => {
setIsSubmitting(true)
try {
const result = await submitInquiry(data)
if (result.success) {
toast.success('提交成功我们的专属顾问会在24小时内联系您')
form.reset()
}
else {
toast.error(result.message || '提交失败,请稍后重试')
}
}
catch (error) {
toast.error('网络错误,请稍后重试')
}
finally {
setIsSubmitting(false)
}
}
const scrollToForm = () => {
const formElement = document.getElementById('inquiry-form')
if (formElement) {
formElement.scrollIntoView({behavior: 'smooth', block: 'start'})
}
}
return (
<HomePage
path={[
{label: '业务定制', href: '/custom'},
]}
>
<Wrap className="flex flex-col gap-16">
{/* 1. 顶部介绍区 */}
<SelfDesc onInquiry={() => {
document.getElementById('inquiry-form')?.scrollIntoView({behavior: 'smooth', block: 'start'})
}}/>
{/* 2. 表单区 */}
<section id="inquiry-form" className="bg-white rounded-lg p-6 lg:p-12">
<div className="text-center mb-8 lg:mb-12">
<h2 className="text-2xl lg:text-3xl font-semibold"></h2>
<p className="text-gray-500 mt-2 text-sm lg:text-base">
24
</p>
</div>
<Form form={form} handler={form.handleSubmit(onSubmit)}>
<div className="mx-auto max-w-2xl space-y-6">
{/* 企业名称 */}
<FormField name="companyName">
{({id, field}) => (
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-4">
<label
htmlFor={id}
className="flex items-center gap-1 lg:w-32 lg:text-right lg:pt-2 text-sm"
>
<span className="text-red-500">*</span>
<span></span>
</label>
<div className="flex-1 lg:max-w-md">
<Input
{...field}
id={id}
placeholder="请输入企业名称"
disabled={isSubmitting}
aria-required="true"
/>
</div>
</div>
)}
</FormField>
{/* 联系人姓名 */}
<FormField name="contactName">
{({id, field}) => (
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-4">
<label
htmlFor={id}
className="flex items-center gap-1 lg:w-32 lg:text-right lg:pt-2 text-sm"
>
<span className="text-red-500">*</span>
<span></span>
</label>
<div className="flex-1 lg:max-w-md">
<Input
{...field}
id={id}
placeholder="请输入联系人姓名"
disabled={isSubmitting}
aria-required="true"
/>
</div>
</div>
)}
</FormField>
{/* 联系人手机号码 */}
<FormField name="phone">
{({id, field}) => (
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-4">
<label
htmlFor={id}
className="flex items-center gap-1 lg:w-32 lg:text-right lg:pt-2 text-sm"
>
<span className="text-red-500">*</span>
<span></span>
</label>
<div className="flex-1 lg:max-w-md">
<Input
{...field}
id={id}
type="tel"
placeholder="请输入11位手机号码"
disabled={isSubmitting}
aria-required="true"
/>
</div>
</div>
)}
</FormField>
{/* 每月需求用量 */}
<FormField name="monthlyUsage">
{({id, field}) => (
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-4">
<label
htmlFor={id}
className="flex items-center gap-1 lg:w-32 lg:text-right lg:pt-2 text-sm"
>
<span className="text-red-500">*</span>
<span></span>
</label>
<div className="flex-1 lg:max-w-md">
<Select
onValueChange={field.onChange}
value={field.value}
disabled={isSubmitting}
>
<SelectTrigger id={id} aria-required="true">
<SelectValue placeholder="请选择您需要的用量"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="less20">20</SelectItem>
<SelectItem value="20-100">20~100</SelectItem>
<SelectItem value="100-500">100~500</SelectItem>
<SelectItem value="more500">500</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
</FormField>
{/* 用途 */}
<FormField name="purpose">
{({id, field}) => (
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-4">
<label
htmlFor={id}
className="flex items-center gap-1 lg:w-32 lg:text-right lg:pt-2 text-sm"
>
<span className="text-red-500">*</span>
<span></span>
</label>
<div className="flex-1 lg:max-w-md">
<Input
{...field}
id={id}
placeholder="请输入用途,例如:数据采集、市场调研等"
disabled={isSubmitting}
aria-required="true"
/>
</div>
</div>
)}
</FormField>
<div className="pt-4 flex justify-center">
<Button
type="submit"
className="bg-blue-600 hover:bg-blue-700 px-12 py-2.5"
disabled={isSubmitting}
>
{isSubmitting ? '提交中...' : '提交'}
</Button>
</div>
</div>
</Form>
</section>
{/* 3. 底部引导区 */}
<section className="relative rounded-lg overflow-hidden h-48 lg:h-56">
<Image
src={group}
alt="立即试用背景"
fill
className="object-cover"
priority
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-full max-w-4xl px-6 flex flex-col lg:flex-row items-center gap-4 lg:gap-10 justify-center lg:justify-between">
<div className="text-blue-600 font-bold text-xl lg:text-2xl text-center lg:text-left">
5000IP
</div>
<Button
className={merge(
'bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-md whitespace-nowrap',
)}
onClick={() => router.push('/product')}
>
</Button>
</div>
</div>
</section>
</Wrap>
</HomePage>
)
}

View File

@@ -1,264 +1,27 @@
'use client' import {Metadata} from 'next'
import {useState} from 'react' import {siteConfig} from '@/config/site'
import Image from 'next/image' import CustomPage from './_client'
import {useRouter} from 'next/navigation'
import {useForm} from 'react-hook-form'
import {zodResolver} from '@hookform/resolvers/zod'
import {z} from 'zod'
import {toast} from 'sonner'
import HomePage from '@/components/home/page'
import Wrap from '@/components/wrap'
import {Form, FormField} from '@/components/ui/form'
import {Input} from '@/components/ui/input'
import {Button} from '@/components/ui/button'
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
import {merge} from '@/lib/utils'
import {submitInquiry} from '@/actions/inquiry'
import group from './_assets/Group.webp'
import SelfDesc from '@/components/features/self-desc'
const formSchema = z.object({ export async function generateMetadata(): Promise<Metadata> {
company: z.string().min(2, '企业名称至少2个字符'), return {
name: z.string().min(2, '联系人姓名至少2个字符'), title: '业务定制',
phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的11位手机号码'), description: '蓝狐代理为您提供企业级代理IP定制服务专属顾问1对1服务量身打造代理解决方案满足企业个性化需求',
usage: z.string().min(1, '请选择您需要的用量'), openGraph: {
purpose: z.string().min(2, '请输入用途说明').max(200, '用途说明不超过200字符'), title: '业务定制',
}) description: '蓝狐代理为您提供企业级代理IP定制服务专属顾问1对1服务量身打造代理解决方案',
images: [
type FormValues = z.infer<typeof formSchema> {
url: siteConfig.ogImage.url,
export default function CustomPage() { width: siteConfig.ogImage.width,
const router = useRouter() height: siteConfig.ogImage.height,
const [isSubmitting, setIsSubmitting] = useState(false) alt: '业务定制',
},
const form = useForm<FormValues>({ ],
resolver: zodResolver(formSchema), },
defaultValues: { alternates: {
company: '', canonical: `${siteConfig.url}/custom`,
name: '',
phone: '',
usage: '',
purpose: '',
}, },
})
const onSubmit = async (data: FormValues) => {
setIsSubmitting(true)
try {
const result = await submitInquiry(data)
if (result.success) {
toast.success('提交成功我们的专属顾问会在24小时内联系您')
form.reset()
} }
else {
toast.error(result.message || '提交失败,请稍后重试')
}
}
catch (error) {
toast.error('网络错误,请稍后重试')
}
finally {
setIsSubmitting(false)
}
}
const scrollToForm = () => {
const formElement = document.getElementById('inquiry-form')
if (formElement) {
formElement.scrollIntoView({behavior: 'smooth', block: 'start'})
}
}
return (
<HomePage
path={[
{label: '业务定制', href: '/custom'},
]}
>
<Wrap className="flex flex-col gap-16">
{/* 1. 顶部介绍区 */}
<SelfDesc onInquiry={() => {
document.getElementById('inquiry-form')?.scrollIntoView({behavior: 'smooth', block: 'start'})
}}/>
{/* 2. 表单区 */}
<section id="inquiry-form" className="bg-white rounded-lg p-6 lg:p-12">
<div className="text-center mb-8 lg:mb-12">
<h2 className="text-2xl lg:text-3xl font-semibold"></h2>
<p className="text-gray-500 mt-2 text-sm lg:text-base">
24
</p>
</div>
<Form form={form} handler={form.handleSubmit(onSubmit)}>
<div className="mx-auto max-w-2xl space-y-6">
{/* 企业名称 */}
<FormField name="companyName">
{({id, field}) => (
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-4">
<label
htmlFor={id}
className="flex items-center gap-1 lg:w-32 lg:text-right lg:pt-2 text-sm"
>
<span className="text-red-500">*</span>
<span></span>
</label>
<div className="flex-1 lg:max-w-md">
<Input
{...field}
id={id}
placeholder="请输入企业名称"
disabled={isSubmitting}
aria-required="true"
/>
</div>
</div>
)}
</FormField>
{/* 联系人姓名 */}
<FormField name="contactName">
{({id, field}) => (
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-4">
<label
htmlFor={id}
className="flex items-center gap-1 lg:w-32 lg:text-right lg:pt-2 text-sm"
>
<span className="text-red-500">*</span>
<span></span>
</label>
<div className="flex-1 lg:max-w-md">
<Input
{...field}
id={id}
placeholder="请输入联系人姓名"
disabled={isSubmitting}
aria-required="true"
/>
</div>
</div>
)}
</FormField>
{/* 联系人手机号码 */}
<FormField name="phone">
{({id, field}) => (
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-4">
<label
htmlFor={id}
className="flex items-center gap-1 lg:w-32 lg:text-right lg:pt-2 text-sm"
>
<span className="text-red-500">*</span>
<span></span>
</label>
<div className="flex-1 lg:max-w-md">
<Input
{...field}
id={id}
type="tel"
placeholder="请输入11位手机号码"
disabled={isSubmitting}
aria-required="true"
/>
</div>
</div>
)}
</FormField>
{/* 每月需求用量 */}
<FormField name="monthlyUsage">
{({id, field}) => (
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-4">
<label
htmlFor={id}
className="flex items-center gap-1 lg:w-32 lg:text-right lg:pt-2 text-sm"
>
<span className="text-red-500">*</span>
<span></span>
</label>
<div className="flex-1 lg:max-w-md">
<Select
onValueChange={field.onChange}
value={field.value}
disabled={isSubmitting}
>
<SelectTrigger id={id} aria-required="true">
<SelectValue placeholder="请选择您需要的用量"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="less20">20</SelectItem>
<SelectItem value="20-100">20~100</SelectItem>
<SelectItem value="100-500">100~500</SelectItem>
<SelectItem value="more500">500</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
</FormField>
{/* 用途 */}
<FormField name="purpose">
{({id, field}) => (
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-4">
<label
htmlFor={id}
className="flex items-center gap-1 lg:w-32 lg:text-right lg:pt-2 text-sm"
>
<span className="text-red-500">*</span>
<span></span>
</label>
<div className="flex-1 lg:max-w-md">
<Input
{...field}
id={id}
placeholder="请输入用途,例如:数据采集、市场调研等"
disabled={isSubmitting}
aria-required="true"
/>
</div>
</div>
)}
</FormField>
<div className="pt-4 flex justify-center">
<Button
type="submit"
className="bg-blue-600 hover:bg-blue-700 px-12 py-2.5"
disabled={isSubmitting}
>
{isSubmitting ? '提交中...' : '提交'}
</Button>
</div>
</div>
</Form>
</section>
{/* 3. 底部引导区 */}
<section className="relative rounded-lg overflow-hidden h-48 lg:h-56">
<Image
src={group}
alt="立即试用背景"
fill
className="object-cover"
priority
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-full max-w-4xl px-6 flex flex-col lg:flex-row items-center gap-4 lg:gap-10 justify-center lg:justify-between">
<div className="text-blue-600 font-bold text-xl lg:text-2xl text-center lg:text-left">
5000IP
</div>
<Button
className={merge(
'bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-md whitespace-nowrap',
)}
onClick={() => router.push('/product')}
>
</Button>
</div>
</div>
</section>
</Wrap>
</HomePage>
)
} }
export default CustomPage

View File

@@ -1,4 +1,6 @@
import {Metadata} from 'next'
import ScenePage, {ScenePageConfig} from '@/components/scene-page' import ScenePage, {ScenePageConfig} from '@/components/scene-page'
import {siteConfig} from '@/config/site'
import bannerImg from './_assets/banner.webp' import bannerImg from './_assets/banner.webp'
import solutionImg from './_assets/solution-main.webp' import solutionImg from './_assets/solution-main.webp'
import value1Img from './_assets/value-1.webp' import value1Img from './_assets/value-1.webp'
@@ -46,6 +48,28 @@ const config: ScenePageConfig = {
}, },
} }
export async function generateMetadata(): Promise<Metadata> {
return {
title: config.banner.title,
description: config.banner.description,
openGraph: {
title: config.banner.title,
description: config.banner.description,
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: config.banner.title,
},
],
},
alternates: {
canonical: `${siteConfig.url}/data-capture`,
},
}
}
export default function DataCapturePage() { export default function DataCapturePage() {
return <ScenePage {...config}/> return <ScenePage {...config}/>
} }

View File

@@ -1,3 +1,28 @@
import {Metadata} from 'next'
import {siteConfig} from '@/config/site'
export async function generateMetadata(): Promise<Metadata> {
return {
title: '帮助中心',
description: '蓝狐代理帮助中心 - 产品使用教程、常见问题解答、行业资讯、代理IP设置指南',
openGraph: {
title: '帮助中心',
description: '蓝狐代理帮助中心 - 产品使用教程、常见问题解答、行业资讯',
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: '帮助中心',
},
],
},
alternates: {
canonical: `${siteConfig.url}/docs`,
},
}
}
export default function DocsIndexPage() { export default function DocsIndexPage() {
return ( return (
<div className="text-center text-slate-500 py-12"> <div className="text-center text-slate-500 py-12">

View File

@@ -1,4 +1,6 @@
import {Metadata} from 'next'
import ScenePage, {ScenePageConfig} from '@/components/scene-page' import ScenePage, {ScenePageConfig} from '@/components/scene-page'
import {siteConfig} from '@/config/site'
import bannerImg from './_assets/banner.webp' import bannerImg from './_assets/banner.webp'
import solutionImg from './_assets/solution-main.webp' import solutionImg from './_assets/solution-main.webp'
import value1Img from './_assets/value-1.webp' import value1Img from './_assets/value-1.webp'
@@ -46,6 +48,28 @@ const config: ScenePageConfig = {
}, },
} }
export async function generateMetadata(): Promise<Metadata> {
return {
title: config.banner.title,
description: config.banner.description,
openGraph: {
title: config.banner.title,
description: config.banner.description,
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: config.banner.title,
},
],
},
alternates: {
canonical: `${siteConfig.url}/e-commerce`,
},
}
}
export default function ECommercePage() { export default function ECommercePage() {
return <ScenePage {...config}/> return <ScenePage {...config}/>
} }

View File

@@ -1,4 +1,6 @@
import {Metadata} from 'next'
import ScenePage, {ScenePageConfig} from '@/components/scene-page' import ScenePage, {ScenePageConfig} from '@/components/scene-page'
import {siteConfig} from '@/config/site'
import bannerImg from './_assets/banner.webp' import bannerImg from './_assets/banner.webp'
import solutionImg from './_assets/solution-main.webp' import solutionImg from './_assets/solution-main.webp'
import value1Img from './_assets/value-1.webp' import value1Img from './_assets/value-1.webp'
@@ -46,6 +48,28 @@ const config: ScenePageConfig = {
}, },
} }
export async function generateMetadata(): Promise<Metadata> {
return {
title: config.banner.title,
description: config.banner.description,
openGraph: {
title: config.banner.title,
description: config.banner.description,
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: config.banner.title,
},
],
},
alternates: {
canonical: `${siteConfig.url}/market-research`,
},
}
}
export default function MarketResearchPage() { export default function MarketResearchPage() {
return <ScenePage {...config}/> return <ScenePage {...config}/>
} }

View File

@@ -1,4 +1,6 @@
import {Metadata} from 'next'
import ScenePage, {ScenePageConfig} from '@/components/scene-page' import ScenePage, {ScenePageConfig} from '@/components/scene-page'
import {siteConfig} from '@/config/site'
import bannerImg from './_assets/banner.webp' import bannerImg from './_assets/banner.webp'
import solutionImg from './_assets/solution-main.webp' import solutionImg from './_assets/solution-main.webp'
import value1Img from './_assets/value-1.webp' import value1Img from './_assets/value-1.webp'
@@ -46,6 +48,28 @@ const config: ScenePageConfig = {
}, },
} }
export async function generateMetadata(): Promise<Metadata> {
return {
title: config.banner.title,
description: config.banner.description,
openGraph: {
title: config.banner.title,
description: config.banner.description,
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: config.banner.title,
},
],
},
alternates: {
canonical: `${siteConfig.url}/network-testing`,
},
}
}
export default function NetworkTestingPage() { export default function NetworkTestingPage() {
return <ScenePage {...config}/> return <ScenePage {...config}/>
} }

View File

@@ -1,7 +1,9 @@
import {Suspense} from 'react'
import {Metadata} from 'next'
import {siteConfig} from '@/config/site'
import BreadCrumb from '@/components/bread-crumb' import BreadCrumb from '@/components/bread-crumb'
import Wrap from '@/components/wrap' import Wrap from '@/components/wrap'
import Purchase, {TabType} from '@/components/composites/purchase' import Purchase, {TabType} from '@/components/composites/purchase'
import {Suspense} from 'react'
import HomePage from '@/components/home/page' import HomePage from '@/components/home/page'
export type ProductPageProps = { export type ProductPageProps = {
@@ -10,6 +12,28 @@ export type ProductPageProps = {
}> }>
} }
export async function generateMetadata(): Promise<Metadata> {
return {
title: '产品中心',
description: '为您的业务提供多样化代理产品 - 短效代理、长效代理、固定IP代理、SOCKS5代理高可用性、低延迟',
openGraph: {
title: '产品中心',
description: '为您的业务提供多样化代理产品 - 短效代理、长效代理、固定IP代理、SOCKS5代理高可用性、低延迟',
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: '产品中心',
},
],
},
alternates: {
canonical: `${siteConfig.url}/product`,
},
}
}
export default function ProductPage(props: ProductPageProps) { export default function ProductPage(props: ProductPageProps) {
return ( return (
<HomePage path={[ <HomePage path={[

View File

@@ -1,4 +1,6 @@
import {Metadata} from 'next'
import ScenePage, {ScenePageConfig} from '@/components/scene-page' import ScenePage, {ScenePageConfig} from '@/components/scene-page'
import {siteConfig} from '@/config/site'
import bannerImg from './_assets/banner.webp' import bannerImg from './_assets/banner.webp'
import solutionImg from './_assets/solution-main.webp' import solutionImg from './_assets/solution-main.webp'
import value1Img from './_assets/value-1.webp' import value1Img from './_assets/value-1.webp'
@@ -46,6 +48,28 @@ const config: ScenePageConfig = {
}, },
} }
export async function generateMetadata(): Promise<Metadata> {
return {
title: config.banner.title,
description: config.banner.description,
openGraph: {
title: config.banner.title,
description: config.banner.description,
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: config.banner.title,
},
],
},
alternates: {
canonical: `${siteConfig.url}/seo-optimization`,
},
}
}
export default function SeoOptimizationPage() { export default function SeoOptimizationPage() {
return <ScenePage {...config}/> return <ScenePage {...config}/>
} }

View File

@@ -1,4 +1,6 @@
import {Metadata} from 'next'
import ScenePage, {ScenePageConfig} from '@/components/scene-page' import ScenePage, {ScenePageConfig} from '@/components/scene-page'
import {siteConfig} from '@/config/site'
import bannerImg from './_assets/banner.webp' import bannerImg from './_assets/banner.webp'
import solutionImg from './_assets/solution-main.webp' import solutionImg from './_assets/solution-main.webp'
import value1Img from './_assets/value-1.webp' import value1Img from './_assets/value-1.webp'
@@ -46,6 +48,28 @@ const config: ScenePageConfig = {
}, },
} }
export async function generateMetadata(): Promise<Metadata> {
return {
title: config.banner.title,
description: config.banner.description,
openGraph: {
title: config.banner.title,
description: config.banner.description,
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: config.banner.title,
},
],
},
alternates: {
canonical: `${siteConfig.url}/social-media`,
},
}
}
export default function SocialMediaPage() { export default function SocialMediaPage() {
return <ScenePage {...config}/> return <ScenePage {...config}/>
} }

View File

@@ -1,19 +1,71 @@
import './globals.css' import './globals.css'
import {ReactNode} from 'react' import {ReactNode} from 'react'
import {Metadata} from 'next' import {Metadata, Viewport} from 'next'
import {Toaster} from '@/components/ui/sonner' import {Toaster} from '@/components/ui/sonner'
import Effects from '@/app/effects' import Effects from '@/app/effects'
import {ProfileStoreProvider} from '@/components/stores/profile' import {ProfileStoreProvider} from '@/components/stores/profile'
import {LayoutStoreProvider} from '@/components/stores/layout' import {LayoutStoreProvider} from '@/components/stores/layout'
import {ClientStoreProvider} from '@/components/stores/client' import {ClientStoreProvider} from '@/components/stores/client'
import {getProfile} from '@/actions/auth' import {getProfile} from '@/actions/auth'
import Script from 'next/script'
import {AppStoreProvider} from '@/components/stores/app' import {AppStoreProvider} from '@/components/stores/app'
import {getApiUrl} from '@/actions/base' import {getApiUrl} from '@/actions/base'
import {siteConfig} from '@/config/site'
import {JsonLd} from '@/components/seo/json-ld'
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
themeColor: '#3b82f6',
}
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
return { return {
title: '蓝狐代理', metadataBase: new URL(siteConfig.url),
title: {
default: siteConfig.name,
template: `%s`,
},
description: siteConfig.description,
keywords: siteConfig.keywords,
robots: {
index: true,
follow: true,
googleBot: {
'index': true,
'follow': true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
openGraph: {
type: 'website',
locale: siteConfig.locale,
url: siteConfig.url,
siteName: siteConfig.name,
title: siteConfig.name,
description: siteConfig.description,
images: [
{
url: siteConfig.ogImage.url,
width: siteConfig.ogImage.width,
height: siteConfig.ogImage.height,
alt: siteConfig.name,
},
],
},
twitter: {
card: 'summary_large_image',
title: siteConfig.name,
description: siteConfig.description,
images: [siteConfig.ogImage.url],
},
alternates: {
canonical: siteConfig.url,
},
icons: {
icon: '/favicon.ico',
},
} }
} }
@@ -27,6 +79,16 @@ export default async function RootLayout(props: Readonly<{
<Effects>{props.children}</Effects> <Effects>{props.children}</Effects>
</StoreProviders> </StoreProviders>
<Toaster position="top-center" richColors expand/> <Toaster position="top-center" richColors expand/>
<JsonLd
schema={{
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': `${siteConfig.url}/#organization`,
'name': siteConfig.name,
'url': siteConfig.url,
'description': siteConfig.description,
}}
/>
</body> </body>
</html> </html>
) )

21
src/app/manifest.ts Normal file
View File

@@ -0,0 +1,21 @@
import {MetadataRoute} from 'next'
import {siteConfig} from '@/config/site'
export default function manifest(): MetadataRoute.Manifest {
return {
name: siteConfig.name,
short_name: siteConfig.shortName,
description: siteConfig.description,
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#3b82f6',
icons: [
{
src: '/favicon.ico',
sizes: '48x48',
type: 'image/x-icon',
},
],
}
}

18
src/app/robots.ts Normal file
View File

@@ -0,0 +1,18 @@
import {MetadataRoute} from 'next'
import {siteConfig} from '@/config/site'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: [
'/api/',
'/admin/',
'/profile/',
'/settings/',
],
},
sitemap: `${siteConfig.url}/sitemap.xml`,
}
}

178
src/app/sitemap.ts Normal file
View File

@@ -0,0 +1,178 @@
import {MetadataRoute} from 'next'
import {siteConfig} from '@/config/site'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = siteConfig.url
const now = new Date()
return [
{
url: baseUrl,
lastModified: now,
changeFrequency: 'daily',
priority: 1.0,
},
{
url: `${baseUrl}/product`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${baseUrl}/collect`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${baseUrl}/custom`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/data-capture`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.7,
},
{
url: `${baseUrl}/e-commerce`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.7,
},
{
url: `${baseUrl}/market-research`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.7,
},
{
url: `${baseUrl}/seo-optimization`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.7,
},
{
url: `${baseUrl}/social-media`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.7,
},
{
url: `${baseUrl}/advertising`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.7,
},
{
url: `${baseUrl}/account-management`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.7,
},
{
url: `${baseUrl}/network-testing`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.7,
},
{
url: `${baseUrl}/docs`,
lastModified: now,
changeFrequency: 'weekly',
priority: 0.6,
},
{
url: `${baseUrl}/docs/product/city-lines`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/faqs/faq-general`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/faqs/faq-billing`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/client/android-proxy`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/client/browser-proxy`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/client/ios-proxy`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/client/windows10-proxy`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/news/news-announce`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/news/news-latest`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/operation/extract-link`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/operation/payment-records`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/operation/profile-settings`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/operation/verify-guide`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/docs/operation/whitelist-guide`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: `${baseUrl}/login`,
lastModified: now,
changeFrequency: 'monthly',
priority: 0.3,
},
]
}

View File

@@ -0,0 +1,8 @@
export function JsonLd({schema}: {schema: Record<string, unknown>}) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{__html: JSON.stringify(schema)}}
/>
)
}

20
src/config/site.ts Normal file
View File

@@ -0,0 +1,20 @@
export const siteConfig = {
name: '蓝狐代理',
shortName: '蓝狐代理',
url: (process.env.API_BASE_URL || 'https://prov.lanhuip.com').replace(/\/$/, ''),
description: '蓝狐代理 - 稳定、高速、安全的代理服务提供HTTP代理、SOCKS5代理、动态IP、静态IP、爬虫代理等产品保护您的隐私畅游互联网',
keywords: ['代理ip', '国内代理ip', 'http代理', '动态ip', '静态ip', '爬虫代理', '独享代理', 'socks5代理'],
author: '蓝狐团队',
locale: 'zh_CN',
social: {
twitter: '@lanhuproxy',
github: 'lanhu-proxy',
},
ogImage: {
url: '/og-image.jpg',
width: 1200,
height: 630,
},
}
export type SiteConfig = typeof siteConfig

View File

@@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -13,13 +17,16 @@
"isolatedModules": true, "isolatedModules": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"allowArbitraryExtensions": true,
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"
} }
], ],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": [
"./src/*"
]
} }
}, },
"include": [ "include": [
@@ -30,5 +37,7 @@
".next/dev/types/**/*.ts", ".next/dev/types/**/*.ts",
"**/*.mts" "**/*.mts"
], ],
"exclude": ["node_modules"] "exclude": [
"node_modules"
]
} }