尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Next.js与Supabase构建现代化食谱网站:技术选型与全栈实践

基于Next.js与Supabase构建现代化食谱网站:技术选型与全栈实践 1. 从零到一为什么我们需要一个现代化的食谱网站最近几年我身边的朋友圈里晒厨艺、分享自制美食的人越来越多了。但一个普遍的现象是大家分享的渠道非常分散有的发在朋友圈九宫格配文寥寥有的存在手机备忘录时间一久就找不到了更常见的是收藏夹里塞满了来自不同平台、质量参差不齐的食谱链接真到想做饭时却无从下手。这让我开始思考一个真正好用、属于个人或小团队的食谱网站到底应该是什么样子它不应该只是一个菜谱的陈列柜而应该是一个能激发创作、方便管理、并且易于分享的“数字厨房”。这就是“ReChef Recipe Website”项目诞生的初衷。“ReChef”这个名字可以理解为“Recipe Chef”食谱厨师的缩写也暗含了“Re-create”再创造和“Record Chef”记录厨师的意味。它的核心目标是构建一个功能完整、体验流畅的现代化食谱管理平台。想象一下你可以在这里系统地记录每一道菜的灵感来源、所需食材的精确克数、分步图解的操作过程甚至还能关联上你购买食材的电商链接、计算大致的成本。它既是你的私人美食数据库也可以是一个向朋友和家人开放分享的美食博客。这个项目适合所有对烹饪有热情并且希望用数字化的方式将其沉淀下来的人。无论你是一个刚开始学做饭的新手想要建立自己的基础菜谱库还是一个经验丰富的美食爱好者希望整理和分享自己的独家秘方抑或是一个小型的美食创作者或私厨需要一个专业的平台来展示你的作品“ReChef”都能提供一个可靠的起点。接下来我将从技术选型、核心功能设计、前后端实现细节以及部署上线的完整链路手把手带你搭建起这个属于你自己的“ReChef”。2. 技术栈选型为什么是Next.js Supabase Tailwind CSS在启动任何项目之前技术选型是决定开发效率和最终产品质量的关键一步。对于“ReChef”这样一个兼具内容管理CRUD、用户系统和良好展示需求的网站我经过一番权衡最终确定了Next.js (App Router) Supabase Tailwind CSS这个组合。下面我详细拆解一下每个选择背后的理由以及它们如何协同工作。2.1 前端框架Next.js 与它的App Router首先为什么是Next.js而不是纯粹的React或者Vite核心原因在于Next.js提供了一套“开箱即用”的全栈解决方案这对于我们这种个人或小团队项目来说能极大地降低架构复杂度。服务端渲染SSR与静态生成SSG食谱网站的大部分页面比如具体的菜谱详情页内容相对固定非常适合使用SSG在构建时生成静态HTML。这能带来极快的首屏加载速度和优秀的SEO表现。而对于需要动态数据的页面如用户个人主页、搜索页Next.js的SSR能力可以保证数据是最新的。Next.js 13的App Router架构通过generateStaticParams和fetchAPI让这两种渲染策略的配置变得异常清晰和简单。API Routes内置我们不需要单独搭建一个后端服务器。Next.js允许在app/api目录下直接创建API端点。这意味着处理表单提交、用户认证回调、甚至一些简单的服务端逻辑都可以在前端项目内完成部署也只需要一个地方管理成本直线下降。优秀的开发体验基于TurboPack的快速刷新、直观的文件式路由App Router、以及丰富的插件生态能让我们的开发过程非常顺畅。在“ReChef”中我们将利用App Router来组织页面/app/recipes/page.js是食谱列表页/app/recipes/[id]/page.js是动态路由的食谱详情页。服务端组件用于获取初始数据客户端组件处理交互这种混合模式能很好地平衡性能与体验。2.2 后端即服务BaaSSupabase 一站式解决方案对于数据库、用户认证、实时订阅和存储这些后端功能自己从零搭建和维护一套系统比如Node.js Express PostgreSQL Redis不仅工作量大而且对运维有要求。Supabase作为开源的Firebase替代品完美地解决了这个问题。PostgreSQL数据库Supabase提供了一个全托管的、带有实时功能的PostgreSQL数据库。我们可以直接使用熟悉的SQL来设计表结构这对于“食谱”这种关系型数据食谱表、用户表、食材表、步骤表、收藏关系表非常友好。通过其在线Table Editor我们甚至可以不写一行SQL就创建和管理表。内置的身份认证Supabase Auth支持邮箱/密码、第三方OAuthGoogle, GitHub等等多种登录方式。我们只需要几行代码就能集成完整的注册、登录、会话管理功能省去了自己处理密码哈希、JWT令牌的麻烦。存储与CDN用户上传的食谱封面图、步骤图需要存储。Supabase Storage提供了S3兼容的对象存储并自动配好了CDN上传和访问都非常方便。自动生成的API当我们创建好数据库表后Supabase会自动为每张表生成一套完整的RESTful API和实时订阅的端点。同时它提供了强大的JavaScript/TypeScript客户端库在前端可以直接像调用本地函数一样操作数据库。在“ReChef”中我们将创建profiles用户信息、recipes食谱、recipe_ingredients食谱-食材关联、recipe_steps步骤等表。所有对数据的增删改查都将通过supabase/supabase-js这个库来完成。2.3 样式方案Tailwind CSS 实用优先在样式方面我放弃了传统的CSS-in-JS如styled-components或SCSS模块而选择了Tailwind CSS。原因如下开发速度无需在HTML/JSX和CSS文件之间反复切换也无需为类名绞尽脑汁。通过组合实用的原子类可以快速实现任何设计。这对于需要快速迭代的原型项目或个人项目来说效率提升是巨大的。设计一致性通过配置tailwind.config.js文件可以严格定义项目的颜色体系、间距尺度、字体大小等设计Token。这能强制保证整个网站视觉风格的一致性避免出现“一个按钮一种蓝”的情况。极致的生产体积Tailwind使用PurgeCSS或JIT引擎在生产构建时会自动移除所有未使用的CSS最终生成的CSS文件通常只有几KB对性能非常友好。对于“ReChef”这种内容展示型网站清晰的排版、舒适的间距和一致的视觉层次很重要。Tailwind能让我们用最小的成本达到专业的设计效果。2.4 辅助工具选型除了核心三件套还有一些工具能显著提升体验NextAuth.js 或 Supabase Auth Helpers虽然Supabase自带Auth但NextAuth.js提供了更深度与Next.js集成的会话管理方案并且支持更多的身份提供商。Supabase也提供了专门的supabase/ssr和supabase/auth-helpers-nextjs包来简化在Next.js中的集成。我们将根据简化程度选择其一。React Hook Form Zod用于处理食谱创建、编辑等复杂表单。React Hook Form性能优异Zod用于模式验证和类型推断两者结合能提供类型安全且高效的表单体验。Cloudinary 或 Supabase Storage对于图片处理如果需求超出简单存储如需要裁剪、压缩、添加水印Cloudinary是更专业的选择。但Supabase Storage也能满足基本需求我们会优先使用Supabase以保持技术栈简洁。部署平台VercelNext.js的亲爹部署体验无缝衔接。只需关联Git仓库每次推送代码就能自动部署预览和生产环境并自动配置SSL证书。这套技术栈的组合确保了“ReChef”项目在拥有现代Web应用所有特性的同时保持了架构的简洁和开发的敏捷性。3. 数据库设计与核心业务逻辑技术栈确定后下一步就是设计数据的“骨架”——数据库。一个好的数据库设计是应用稳定、高效和易于扩展的基础。对于“ReChef”我们需要围绕“食谱”这个核心实体构建一个规范化的关系模型。3.1 核心表结构设计我们将使用Supabase的PostgreSQL数据库。以下是核心表的设计思路1.profiles表 (扩展自auth.users)这是用户信息表与Supabase Auth的auth.users表通过id关联。auth.users存储核心认证信息邮箱、密码哈希而profiles存储公开的个人资料。-- 在Supabase SQL编辑器中执行 create table public.profiles ( id uuid references auth.users(id) on delete cascade primary key, username text unique, full_name text, avatar_url text, bio text, updated_at timestamp with time zone default now() ); -- 启用行级安全策略 (RLS) alter table public.profiles enable row level security; -- 创建策略用户可读所有资料但只能更新自己的 create policy 公开可读 on public.profiles for select using (true); create policy 用户更新个人资料 on public.profiles for update using (auth.uid() id);2.recipes表 (食谱主表)这是最重要的表存储食谱的元信息。create table public.recipes ( id bigint generated by default as identity primary key, user_id uuid references public.profiles(id) on delete cascade not null, title text not null, description text, prep_time integer, -- 准备时间单位分钟 cook_time integer, -- 烹饪时间单位分钟 servings integer, -- 份量 difficulty text check (difficulty in (easy, medium, hard)), cover_image_url text, -- 封面图URL is_published boolean default false, -- 是否公开发布 created_at timestamp with time zone default now(), updated_at timestamp with time zone default now() ); -- 启用RLS alter table public.recipes enable row level security; -- 策略已发布的食谱所有人可读未发布的仅作者可见用户可增删改自己的食谱 create policy 已发布食谱公开可读 on public.recipes for select using (is_published true or auth.uid() user_id); create policy 用户管理个人食谱 on public.recipes for all using (auth.uid() user_id);3.recipe_ingredients表 (食谱食材关联表)由于一个食谱有多个食材且一个食材可能用于多个食谱这里设计为关联表。我们没有单独创建ingredients表而是将食材名称、用量和单位直接存储在这里简化模型。如果未来需要复杂的食材库可以再拆分。create table public.recipe_ingredients ( id bigint generated by default as identity primary key, recipe_id bigint references public.recipes(id) on delete cascade not null, sort_order integer not null, -- 排序 name text not null, -- 食材名称如“鸡胸肉” quantity numeric, -- 数量如 200 unit text, -- 单位如 “g”, “个”, “适量” note text -- 备注如“切块” ); -- RLS策略继承自recipes表通过recipe_id关联 alter table public.recipe_ingredients enable row level security; create policy 基于食谱权限访问 on public.recipe_ingredients for all using ( exists (select 1 from public.recipes where id recipe_ingredients.recipe_id and (is_published true or auth.uid() user_id)) );4.recipe_steps表 (食谱步骤表)存储烹饪步骤包含文字描述和步骤图片。create table public.recipe_steps ( id bigint generated by default as identity primary key, recipe_id bigint references public.recipes(id) on delete cascade not null, step_number integer not null, description text not null, image_url text -- 步骤图URL ); -- RLS策略同上 alter table public.recipe_steps enable row level security; create policy 基于食谱权限访问 on public.recipe_steps for all using ( exists (select 1 from public.recipes where id recipe_steps.recipe_id and (is_published true or auth.uid() user_id)) );5.favorites表 (收藏表)记录用户与食谱之间的多对多收藏关系。create table public.favorites ( user_id uuid references public.profiles(id) on delete cascade, recipe_id bigint references public.recipes(id) on delete cascade, created_at timestamp with time zone default now(), primary key (user_id, recipe_id) -- 联合主键防止重复收藏 ); -- RLS用户只能查看和管理自己的收藏 alter table public.favorites enable row level security; create policy 用户管理个人收藏 on public.favorites for all using (auth.uid() user_id);3.2 行级安全策略RLS的重要性你可能注意到了每个表都启用了enable row level security并创建了策略Policy。这是Supabase也是现代数据库保障数据安全的核心机制。它的作用是在数据库层面就过滤掉用户无权访问的数据行。例如对于recipes表我们的策略是is_published true or auth.uid() user_id。这意味着任何查询在到达数据库时会自动加上这个条件。未登录用户只能看到已发布的食谱登录用户除了能看到所有已发布的还能看到自己创建的所有食谱无论是否发布。这样我们在前端代码中无需编写复杂的权限判断逻辑大大简化了开发并且从根本上防止了越权访问。3.3 业务逻辑层设计数据库是底层我们还需要在应用层Next.js的API Routes或Server Actions封装一些业务逻辑创建食谱这是一个事务性操作。需要在一个事务内依次插入recipes主记录然后批量插入recipe_ingredients和recipe_steps。如果中间任何一步失败整个操作应该回滚。我们可以使用Supabase Client的rpc()调用一个自定义的数据库函数或者在Next.js的Server Action中使用supabase.from().insert()并手动管理顺序和错误。更新食谱比创建更复杂因为涉及到关联数据的更新可能增删了食材或步骤。一个稳妥的做法是先更新主表然后删除该食谱所有旧的关联数据再插入新的关联数据。同样这最好放在一个数据库事务中。删除食谱由于我们设置了外键约束on delete cascade删除recipes表中的一条记录会自动删除其在recipe_ingredients、recipe_steps和favorites中的关联记录非常方便。图片上传在上传封面图或步骤图到Supabase Storage前最好在前端进行一些预处理比如压缩图片以减少带宽和存储成本。可以使用browser-image-compression这样的库。实操心得数据库设计中的“过度工程化”陷阱在最初设计时我曾考虑将ingredients独立成表并建立units单位表以实现食材的标准化和单位换算。但这立刻引入了复杂性用户输入“番茄”时是匹配“西红柿”还是创建新食材单位“一勺”如何标准化对于“ReChef”这样一个个人项目过早引入这种复杂性是致命的。我最终采用了上述的简化方案将食材名称和单位作为自由文本存储。核心原则是在满足核心需求记录和展示的前提下保持最简单的结构。标准化和智能推荐可以作为未来迭代的功能而不是V1.0的负担。4. 前端实现构建用户界面与交互有了坚实的后端和数据模型我们现在可以把注意力转移到用户能看到和交互的部分。前端实现我们将遵循Next.js App Router的最佳实践采用服务端组件获取初始数据客户端组件处理交互的混合模式。4.1 项目初始化与配置首先使用create-next-app初始化项目并选择TypeScript和Tailwind CSS。npx create-next-applatest rechef --typescript --tailwind --app cd rechef npm install supabase/supabase-js supabase/ssr接下来配置环境变量。在项目根目录创建.env.local文件填入从Supabase项目设置中获取的URL和Anon Key。# .env.local NEXT_PUBLIC_SUPABASE_URL你的Supabase项目URL NEXT_PUBLIC_SUPABASE_ANON_KEY你的Supabase匿名密钥然后我们需要初始化Supabase客户端。为了在服务端和客户端都能安全使用我们创建一个工具文件/lib/supabase/client.ts和/lib/supabase/server.ts。// /lib/supabase/client.ts import { createBrowserClient } from supabase/ssr export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) }// /lib/supabase/server.ts import { createServerClient } from supabase/ssr import { cookies } from next/headers export async function createClient() { const cookieStore await cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) cookieStore.set(name, value, options) ) } catch {} }, }, } ) }4.2 核心页面实现1. 食谱列表页 (/app/recipes/page.tsx)这个页面将展示所有已发布的食谱并支持简单的搜索和过滤。import { createClient } from /lib/supabase/server import RecipeCard from /components/RecipeCard export default async function RecipesPage({ searchParams, }: { searchParams: { q?: string } }) { const supabase await createClient() // 构建查询只获取已发布的食谱按创建时间倒序 let query supabase .from(recipes) .select( id, title, description, cover_image_url, prep_time, cook_time, servings, difficulty, created_at, profiles!inner(username, avatar_url) ) .eq(is_published, true) .order(created_at, { ascending: false }) // 如果有搜索词添加模糊搜索在title和description上 if (searchParams.q) { query query.or(title.ilike.%${searchParams.q}%,description.ilike.%${searchParams.q}%) } const { data: recipes, error } await query if (error) { console.error(Error fetching recipes:, error) // 处理错误UI } return ( div classNamecontainer mx-auto px-4 py-8 h1 classNametext-3xl font-bold mb-2探索食谱/h1 p classNametext-gray-600 mb-8发现大家分享的美味灵感/p {/* 搜索框 */} div classNamemb-8 form input typesearch nameq placeholder搜索食谱或食材... classNamew-full max-w-md px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent defaultValue{searchParams.q || } / /form /div {/* 食谱网格 */} {recipes recipes.length 0 ? ( div classNamegrid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 {recipes.map((recipe) ( RecipeCard key{recipe.id} recipe{recipe} / ))} /div ) : ( div classNametext-center py-12 p classNametext-gray-500暂无食谱。成为第一个分享的人吧/p /div )} /div ) }RecipeCard是一个客户端组件用于展示食谱的预览卡片包含图片、标题、作者、耗时等信息。2. 食谱详情页 (/app/recipes/[id]/page.tsx)这是动态路由页面用于展示单个食谱的完整信息。import { notFound } from next/navigation import { createClient } from /lib/supabase/server import RecipeDetail from /components/RecipeDetail export default async function RecipePage({ params, }: { params: Promise{ id: string } }) { const { id } await params const supabase await createClient() // 查询食谱详情及关联的食材和步骤 const { data: recipe, error } await supabase .from(recipes) .select( *, profiles!inner(username, avatar_url), recipe_ingredients(*), recipe_steps(*) ) .eq(id, id) .single() // 期望只返回一条记录 if (error || !recipe) { notFound() // 如果没找到跳转到404页面 } // 检查权限如果食谱未发布且当前用户不是作者也返回404 if (!recipe.is_published) { const { data: { user } } await supabase.auth.getUser() if (!user || user.id ! recipe.user_id) { notFound() } } return RecipeDetail recipe{recipe} / }RecipeDetail组件负责渲染食谱的所有信息包括封面大图、标题描述、食材清单可设计成勾选清单方便烹饪时使用、详细的步骤图文以及作者信息和操作按钮编辑、删除、收藏。3. 创建/编辑食谱页 (/app/recipes/new/page.tsx和/app/recipes/[id]/edit/page.tsx)这是整个应用最复杂的交互页面。我们将使用react-hook-form和zod来管理表单状态和验证。// 这是一个客户端组件 use client import { useForm, useFieldArray } from react-hook-form import { zodResolver } from hookform/resolvers/zod import * as z from zod import { createClient } from /lib/supabase/client // 使用Zod定义表单验证模式 const recipeSchema z.object({ title: z.string().min(1, 标题不能为空), description: z.string().optional(), prepTime: z.number().int().min(0).optional(), cookTime: z.number().int().min(0).optional(), servings: z.number().int().min(1).optional(), difficulty: z.enum([easy, medium, hard]).optional(), ingredients: z.array(z.object({ name: z.string().min(1, 食材名称不能为空), quantity: z.number().optional(), unit: z.string().optional(), note: z.string().optional(), })).min(1, 至少需要一种食材), steps: z.array(z.object({ description: z.string().min(1, 步骤描述不能为空), imageFile: z.instanceof(File).optional(), // 用于前端上传 })).min(1, 至少需要一个步骤), coverImageFile: z.instanceof(File).optional(), }) type RecipeFormData z.infertypeof recipeSchema export default function RecipeForm({ initialData }: { initialData?: any }) { const supabase createClient() const { register, control, handleSubmit, formState: { errors, isSubmitting }, } useFormRecipeFormData({ resolver: zodResolver(recipeSchema), defaultValues: initialData || { ingredients: [{}], steps: [{}] }, }) // 使用useFieldArray动态管理食材和步骤数组 const { fields: ingredientFields, append: appendIngredient, remove: removeIngredient, } useFieldArray({ control, name: ingredients }) // ... 类似的为steps也定义 const onSubmit async (data: RecipeFormData) { // 1. 上传封面图到Supabase Storage获取URL let coverImageUrl initialData?.cover_image_url if (data.coverImageFile) { const fileExt data.coverImageFile.name.split(.).pop() const fileName ${Date.now()}.${fileExt} const { data: uploadData, error } await supabase.storage .from(recipe-covers) // 存储桶名称 .upload(fileName, data.coverImageFile) if (!error) { coverImageUrl uploadData.path } } // 2. 类似地上传步骤图片 // 3. 调用API Route或Server Action将数据和图片URL存入数据库 // 4. 处理成功/失败跳转或提示 } return ( form onSubmit{handleSubmit(onSubmit)} classNamespace-y-8 {/* 基础信息字段标题、描述、时间等 */} div label htmlFortitle食谱标题 */label input idtitle {...register(title)} / {errors.title p classNametext-red-500{errors.title.message}/p} /div {/* 动态食材列表 */} div h3食材清单/h3 {ingredientFields.map((field, index) ( div key{field.id} classNameflex gap-2 mb-2 input placeholder名称 {...register(ingredients.${index}.name)} / input placeholder数量 typenumber {...register(ingredients.${index}.quantity, { valueAsNumber: true })} / input placeholder单位 {...register(ingredients.${index}.unit)} / input placeholder备注 {...register(ingredients.${index}.note)} / button typebutton onClick{() removeIngredient(index)}删除/button /div ))} button typebutton onClick{() appendIngredient({})} 添加食材/button /div {/* 动态步骤列表支持图片上传 */} {/* ... */} button typesubmit disabled{isSubmitting}保存食谱/button /form ) }这个表单组件逻辑较多但结构清晰。useFieldArray是处理动态数组字段的神器。图片上传部分我们先将文件上传到Supabase Storage获得一个公开可访问的URL再将这个URL和其他表单数据一起提交到后端。4.3 用户认证与状态管理我们需要一个全局的方式来管理用户登录状态。可以在/app/layout.tsx中使用Supabase的getUser来获取服务端用户信息并通过Context或类似方案传递给客户端组件。更优雅的方式是使用supabase/ssr提供的createServerClient和createBrowserClient配合Next.js的Middleware。我们可以在Middleware中刷新用户会话并将用户信息注入到请求中。然后在/app目录下创建一个/auth/callback/route.ts来处理OAuth登录后的回调。实操心得处理图片上传的优化技巧前端压缩在上传前使用browser-image-compression库对用户上传的图片进行压缩可以显著减少上传流量和存储空间。通常可以将文件大小减少70%而肉眼几乎看不出质量损失。文件命名不要使用用户上传的文件原名这可能导致特殊字符问题或覆盖。使用Date.now()或UUID生成唯一文件名。存储桶策略在Supabase Storage中为recipe-covers和recipe-steps创建不同的存储桶并设置合理的公开权限。通常食谱图片是需要公开访问的所以存储桶策略应设置为public。上传进度对于大图或步骤多的食谱上传过程可能较长。实现一个上传进度条可以极大改善用户体验。Supabase JS Client的.upload()方法提供了onUploadProgress回调。5. 部署上线与性能优化当本地开发完成功能测试通过后下一步就是让“ReChef”在互联网上跑起来。部署不仅仅是把代码扔到服务器还涉及到环境配置、性能调优和持续集成。5.1 使用Vercel进行一键部署Vercel是Next.js应用的“官配”部署平台体验极其流畅。推送代码到Git仓库确保你的代码已经提交并推送到了GitHub、GitLab或Bitbucket。连接Vercel登录Vercel点击“Add New Project”导入你的Git仓库。配置环境变量在项目设置中添加之前在.env.local中定义的NEXT_PUBLIC_SUPABASE_URL和NEXT_PUBLIC_SUPABASE_ANON_KEY。切记不要将.env.local文件本身提交到仓库。构建与部署Vercel会自动检测到这是Next.js项目并使用正确的构建命令(next build)。点击部署后几分钟内你的网站就会拥有一个*.vercel.app的临时域名。关键配置点构建命令Vercel通常自动识别无需更改。输出目录Next.js标准输出无需更改。安装命令npm install。环境变量务必正确配置否则应用无法连接Supabase。部署成功后你可以为项目绑定自定义域名如recipe.yourname.comVercel会自动为你配置SSL证书。5.2 性能优化策略一个食谱网站图片多是最大的性能挑战。以下是几个关键的优化方向1. 图片优化重中之重使用Next.js Image组件这是Next.js性能优化的核心。它自动提供尺寸优化根据设备屏幕大小提供正确尺寸的图片。现代格式自动将图片转换为WebP等更高效的格式如果浏览器支持。懒加载图片进入视口时才加载。防止布局偏移自动计算占位空间。import Image from next/image // 对于存储在Supabase Storage的图片URL是固定的可以直接使用 Image src{${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/recipe-covers/${recipe.cover_image_url}} alt{recipe.title} width{800} // 指定渲染宽度 height{450} // 指定渲染高度保持宽高比 priority{true} // 对于首屏关键图片可以设置优先级 /Supabase Storage CDNSupabase Storage本身就通过CDN分发全球访问速度快。结合Next.js Image效果更佳。2. 数据获取与缓存服务端组件数据获取在page.tsx或layout.tsx中直接使用async/await获取数据Next.js会默认缓存这些请求除非使用{ cache: no-store }。对于不常变的食谱详情页可以设置较长的缓存时间。静态生成SSG对于食谱详情页如果内容不常更新可以在generateStaticParams中返回所有已发布食谱的ID让Next.js在构建时预生成静态页面获得最快的访问速度。// /app/recipes/[id]/page.tsx export async function generateStaticParams() { const supabase createClient() // 注意这里需要在非组件环境下创建客户端 const { data: recipes } await supabase .from(recipes) .select(id) .eq(is_published, true) return recipes?.map((recipe) ({ id: recipe.id.toString() })) || [] }3. 代码分割与懒加载Next.js默认基于路由进行代码分割。对于较大的客户端组件如复杂的食谱编辑器可以使用next/dynamic进行懒加载减少初始包大小。import dynamic from next/dynamic const HeavyEditor dynamic(() import(/components/HeavyEditor), { ssr: false })5.3 监控与维护网站上线后还需要一些基本的维护工作。错误监控集成像Sentry或Vercel Analytics这样的工具监控前端错误和性能指标。数据库备份在Supabase控制台设置自动定期备份策略。对于个人项目每周一次完整备份通常足够。存储桶清理如果允许用户删除食谱记得同时清理Storage中对应的图片文件避免存储空间浪费。这可以通过Supabase的数据库触发器Trigger或Edge Function来实现。成本控制Supabase免费层有额度限制500MB数据库1GB存储等。定期在控制台查看使用量确保不会意外超支。对于个人食谱网站免费层通常绰绰有余。踩坑实录Vercel部署中的环境变量与中间件第一次部署时我遇到了一个典型问题在本地运行良好的认证在Vercel上总是失败。排查后发现两个原因环境变量未生效我错误地将环境变量配置在了Vercel的“Development”环境而生产部署使用的是“Production”环境。确保在正确的环境Production下配置了NEXT_PUBLIC_SUPABASE_URL和NEXT_PUBLIC_SUPABASE_ANON_KEY。Middleware路径匹配我写了一个Middleware来刷新Supabase会话但它的路径匹配规则过于宽泛导致静态资源如图片、JS的请求也被拦截影响了性能。修正后的matcher应该排除这些静态资源// middleware.js export const config { matcher: [/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)], }这个小坑让我花了近一个小时调试。教训是部署后第一时间检查环境变量和中间件逻辑。走到这里一个功能完整、性能不错的“ReChef”食谱网站就已经从想法变成了现实。它具备了食谱的创建、编辑、发布、浏览、搜索和收藏等核心功能并且拥有现代化的技术架构和良好的用户体验。你可以在此基础上继续扩展比如添加食谱评分、评论系统、根据食材智能推荐、生成购物清单、甚至对接智能厨电的API。最重要的是你现在拥有了一个完全由自己掌控的数字厨房可以随时随地记录和分享你的美食创作了。
返回列表