如何构建网站模板
欢迎阅读新博客!今天我们将讨论如何构建网站模板。
故事从同时处理多个项目开始,这是大多数开发者的常态。我从未见过只专注于一个项目的开发者。只要大部分工作没有白费,这并非浪费时间的行为。从轻松的角度看,每日的努力终有回报——这是我在竞赛考试中排名不佳时叔叔告诉我的。
处理多个项目并非浪费时间,反而能帮助你有效学习时间管理。快速将产品推向市场是个好方法。要将任何产品推向市场,就需要前端网站——这正是许多有C语言背景的软件开发者不愿做的工作,这也是本篇博客的由来。
2025年构建前端网站的技术栈
- React或Nextjs作为前端框架
- JSX或React作为开发语言(HTML、CSS和JS不再是开发者的首选)
- Tailwind CSS用于样式设计和响应式布局
- 图标主要使用react-icons和lucide-react
- 部署到Vercel进行免费部署(我们所有模板演示URL都以.vercel.app结尾)
为什么选择Nextjs
我偏好使用Nextjs,原因如下:
- 需要后端时提供快速的服务器端API
- 与Vercel的兼容性
- 市场接受度高
模板页面结构
每个模板都包含着陆页和以下几个页面:
- 着陆页
- 关于页面
- 博客列表页
- 博客详情页
- 订阅页面或表单
- 法律页面(Cookies、隐私政策、条款条件)
- 联系页面
- 服务页面
- 仪表板
- 定价页面
- 订阅表单
- 登录/注册表单或页面
具体取决于模板类别和用途。例如,为开发者构建AI产品前端网站时,需要除仪表板外的所有页面。仪表板主要用于CRM模板。
项目结构与代码组织
我使用最新版Nextjs,目录结构如下。首先是创建着陆页的各个部分,最简单的着陆页时序如下:
- 导航栏
- Hero区域
- 关于区域
- 功能/服务区域
- 客户评价
- 定价
- 常见问题
- 联系表单
- 包含社交链接的页脚
每个区域都有自己的数据和要渲染的UI,这再次取决于模板。
例如,我们的Eternal AI着陆页有超过5/6个区域。这有助于维护代码。当然,你也可以在一个文件中编辑和添加所有内容,放在section标签内。
由于Nextjs提供基于文件的路由,privacy、cookie和blogs文件夹就是模板的路由——博客页面、Cookie页面和隐私政策页面。
代码示例
Hero区域组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
import { ArrowRight } from "lucide-react";
import { motion } from "framer-motion";
const HeroSection = ({ onOpenLogin }) => {
return (
<motion.section
id="about"
className="my-16 z-50 scroll-mt-20"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.6 }}
>
<div className="text-center cursor-pointer py-2 px-4 mb-4 justify-center rounded-full border border-zinc-800 bg-gradient-to-tr from-black to-zinc-950/90 text-zinc-400 w-fit mx-auto flex gap-1 items-center">
<span className="inline-block w-4 h-4 align-middle mr-1">
<svg
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="32" height="32" rx="7" fill="#FF6600" />
<path
d="M10.645 8.5h2.105l3.25 6.195L19.197 8.5h1.977l-4.346 8.452V23.5h-1.782v-6.537L10.645 8.5Z"
fill="white"
/>
</svg>
</span>
<span className="text-xs font-medium">Backed by YC</span>
</div>
<br />
<div className="max-w-5xl mx-auto text-center">
<h2 className="text-5xl font-extrabold text-zinc-100 mx-auto break-words">
Ecosystem for
<span className="ml-4 py-4 px-8 relative group rounded-full bg-gradient-to-r from-blue-900 to-transparent ring-4 ring-blue-900 shadow-zinc-700 shadow-xl">
AI Agents
</span>
</h2>
<p className="text-zinc-400 my-8 max-w-md mx-auto">
Create intelligent agents to automate, analyze, and make decisions.
</p>
<div className="flex gap-4 justify-center my-10">
<button
onClick={() => onOpenLogin?.()}
className="relative flex gap-1 items-center p-2 rounded-full group bg-white text-black hover:text-white transition-all duration-300 ease-in cursor-pointer"
>
<div className="absolute left-0 bottom-0 w-0 z-0 invisible group-hover:visible transition-all duration-300 ease-in group-hover:w-full bg-blue-800 rounded-full h-full" />
<ArrowRight className="w-5 h-5 p-1 bg-blue-800 z-40 group-hover:bg-transparent rounded-full text-zinc-100 transition-all duration-300 ease-in" />
<span className="z-50 text-sm">Get Started</span>
</button>
<button
className={`w-fit py-2 px-4 rounded-full text-sm font-medium transition-all duration-300 hover:bg-zinc-200 bg-zinc-950 text-white hover:text-black hover:shadow-xl`}
>
Learn more
</button>
</div>
</div>
</motion.section>
);
};
export default HeroSection;
|
博客页面组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
"use client";
import { useState } from "react";
import Link from "next/link";
import { ArrowRight, Calendar, Clock } from "lucide-react";
import BackgroundDots from "../LandingPage/components/BackgroundDots";
import Navbar from "../LandingPage/components/Navbar";
import MobileMenu from "../LandingPage/components/MobileMenu";
import Footer from "../LandingPage/components/Footer";
// 将标题转换为URL slug的辅助函数
const titleToSlug = (title) => {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
};
// 示例博客数据
const blogs = [
{
title: "The Future of Artificial Intelligence in Business",
excerpt: "Discover how AI is revolutionizing the way businesses operate, from automation to intelligent decision-making processes that drive unprecedented growth.",
date: "March 15, 2024",
readTime: "8 min read",
},
// ... 更多博客数据
];
const BlogsPage = () => {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const scrollToSection = (sectionId) => {
if (typeof window === "undefined") return;
const element = document.getElementById(sectionId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth",
});
}
};
return (
<div className="relative w-full h-full bg-black min-h-screen">
<BackgroundDots />
{/* 背景SVG */}
<div className="absolute inset-0 top-0 left-0 right-0 bottom-0 opacity-5 pointer-events-none" style={{ zIndex: 0 }}>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" preserveAspectRatio="none" viewBox="0 0 1400 1400">
<filter id="a">
<feTurbulence type="fractalNoise" baseFrequency=".65" numOctaves="3" stitchTiles="stitch"></feTurbulence>
</filter>
<rect width="100%" height="100%" filter="url(#a)"></rect>
</svg>
</div>
<Navbar isMobileMenuOpen={isMobileMenuOpen} setIsMobileMenuOpen={setIsMobileMenuOpen} scrollToSection={scrollToSection} />
<MobileMenu isMobileMenuOpen={isMobileMenuOpen} setIsMobileMenuOpen={setIsMobileMenuOpen} scrollToSection={scrollToSection} />
<main className="relative z-10 pt-24 pb-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h1 className="text-5xl md:text-6xl font-bold text-white mb-4">Our Blog</h1>
<p className="text-xl text-zinc-400 max-w-2xl mx-auto">
Insights, tutorials, and stories about technology, design, and innovation
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{blogs.map((blog, index) => {
const slug = titleToSlug(blog.title);
return (
<Link key={index} href={`/blogs/${slug}`} className="group">
<article className="bg-gradient-to-r from-black to-zinc-950/60 backdrop-blur-sm hover:shadow-2xl hover:shadow-zinc-800 rounded-2xl p-6 transition-all duration-300 h-full flex flex-col">
<div className="flex-1">
<h2 className="text-2xl font-bold hover:text-white mb-3 text-zinc-200 transition-colors">
{blog.title}
</h2>
<p className="text-zinc-400 mb-4 line-clamp-3">{blog.excerpt}</p>
</div>
<div className="flex items-center justify-between text-sm text-zinc-500 mb-4">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
<span>{blog.date}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span>{blog.readTime}</span>
</div>
</div>
<div className="flex items-center gap-2 text-zinc-200 group-hover:gap-4 transition-all">
<span className="text-sm font-medium">Read more</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</div>
</article>
</Link>
);
})}
</div>
</div>
</main>
<Footer />
</div>
);
};
export default BlogsPage;
|
动态路由与博客系统
Nextjs处理动态文件路由,因此slug成为动态URL路由,例如yourdomain.com/blogs/{博客标题}。对于此URL,slug等于"博客标题"。
通过这种方式,可以将所有博客添加到不同的文件中,例如markdown文件内容,并使用动态文件路由进行渲染。这对于生产环境中的服务器端博客SEO非常有用。
样式与动画
Tailwind CSS主要处理从响应式到样式设计、添加主题和颜色的所有内容。运动设计使用framer-motion(framer团队的npm包)或GSAP核心(另一个JavaScript动画库)处理。当然也可以使用anime.js、react-spring或其他库。
对于登录/注册,我们偏好使用相同的基于文件夹的路由,或者主要在叠加层(如模态框)中添加登录/注册表单。
页脚组件示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
import { Heater } from "lucide-react";
import Link from "next/link";
import { navigationItems } from "../constants";
const Footer = () => {
return (
<footer className="relative w-full bg-gradient-to-r from-black to-zinc-950 border-t border-zinc-800 z-10">
<div className="w-full mx-auto p-10 bg-gradient-to-r from-black to-zinc-950 grid md:grid-cols-2 grid-cols-1 items-start justify-center transition-all duration-300 ease-in-out">
<div className="max-w-xl mx-auto ">
<Link href="/" className="flex items-center gap-2 mb-2">
<Heater className="w-5 h-5 text-white" />
<span className="text-white font-semibold text-lg">Eternal</span>
</Link>
<p className="text-zinc-500 text-xs">
© {new Date().getFullYear()} Eternal. All rights reserved.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 justify-center md:justify-start gap-8 mb-8">
<div>
<h5 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">Navigation</h5>
<ul className="space-y-3">
{navigationItems.map((item) => (
<li key={item.label}>
<Link href={item.href} className="text-zinc-400 hover:text-white transition-colors text-sm">
{item.label}
</Link>
</li>
))}
</ul>
</div>
<div>
<h5 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">Legal</h5>
<ul className="space-y-3">
<li>
<Link href="/privacy" className="text-zinc-400 hover:text-white transition-colors text-sm">
Privacy Policy
</Link>
</li>
<li>
<a href="#terms" className="text-zinc-400 hover:text-white transition-colors text-sm">
Terms of Service
</a>
</li>
<li>
<Link href="/cookie" className="text-zinc-400 hover:text-white transition-colors text-sm">
Cookie Policy
</Link>
</li>
<li>
<a href="#security" className="text-zinc-400 hover:text-white transition-colors text-sm">
Security
</a>
</li>
</ul>
</div>
<div>
<h5 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">Social</h5>
<ul className="space-y-3">
<li>
<a href="https://linkedin.com/company/eternal" target="_blank" rel="noopener noreferrer" className="text-zinc-400 hover:text-blue-400 transition-colors text-sm">
LinkedIn
</a>
</li>
<li>
<a href="https://twitter.com/eternal" target="_blank" rel="noopener noreferrer" className="text-zinc-400 hover:text-blue-400 transition-colors text-sm">
Twitter
</a>
</li>
<li>
<a href="https://instagram.com/eternal" target="_blank" rel="noopener noreferrer" className="text-zinc-400 hover:text-pink-400 transition-colors text-sm">
Instagram
</a>
</li>
</ul>
</div>
</div>
</div>
<div className="relative h-fit pt-10 text-center rounded-[4em]">
<p className="md:text-[12em] text-7xl shadow-xlfont-bold font-mono uppercase text-transparent bg-clip-text bg-gradient-to-tr from-black to-white">
Eternal
</p>
</div>
</footer>
);
};
export default Footer;
|
我偏好使用.map和filter方法,而不是重复硬编码相同的内容两三次——这是编写高性能代码的生产实践。
响应式设计与部署
完成后,我们主要需要检查响应式并使用Tailwind CSS类处理,如md、lg、xl、xxl、2xl、sm、xxs、xs,每个都可以通过名称理解。
如果使用Vercel,部署只需一键操作——只需将仓库推送到GitHub,并将GitHub连接到Vercel以导入和部署仓库,即可在生产环境中快速演示。
请随意尝试以相同的方式构建自己的模板,或者查看我们的模板。
下次见!
Shrey