我用 Next.js 14 构建了免费名片生成器——我的收获
Source: Dev.to

上周,我推出了 FreeCard.app —— 一个完全免费的名片和二维码生成器。在本文中,我将分享整个过程、技术决策、遇到的挑战以及学到的经验。
🎯 问题
我对现有的名片生成器感到沮丧:
- ❌ 大多数工具必须注册才能预览设计
- ❌ “免费”工具在导出文件上添加水印
- ❌ 高级功能被锁在每月 10‑20 美元的付费墙后面
- ❌ 为了一个简单的任务而设计的界面过于复杂
我只想要一个直接可用的工具:输入姓名,选择模板,下载。完成。
💡 解决方案
FreeCard.app – 一个真正免费的名片生成器,具备:
- ✅ 25+ 专业模板
- ✅ 自动二维码生成(vCard 格式)
- ✅ 自定义颜色和字体
- ✅ PNG、PDF 和 vCard 导出
- ✅ 邮件签名生成器
- ✅ 无需注册
- ✅ 永不添加水印
商业模式? 简单——Google AdSense。没有高级版,也没有增销。
🛠️ 技术栈
| 层 | 技术 |
|---|---|
| 前端 | Next.js 14 (App Router) |
| 语言 | TypeScript |
| 样式 | Tailwind CSS + shadcn/ui |
| 状态 | Zustand |
| 数据库 | MongoDB + Mongoose |
| 认证 | NextAuth.js v5 |
| 导出 | html-to-image + jsPDF |
| 二维码 | qrcode.react |
| 托管 | Vercel |
为什么选择这套技术栈?
- Next.js 14 App Router – 默认使用服务器组件 = 更快的首次加载。新路由已可用于生产。
- Zustand 优于 Redux – 对于此类工具,Zustand 的简洁性占优势。无需模板代码,直接可用:
// store/useCardStore.ts
import { create } from 'zustand';
interface CardStore {
card: CardData;
setField: (field: string, value: string) => void;
setTemplate: (templateId: string) => void;
}
export const useCardStore = create((set) => ({
card: initialCard,
setField: (field, value) =>
set((state) => ({ card: { ...state.card, [field]: value } })),
setTemplate: (templateId) =>
set((state) => ({ card: { ...state.card, template: templateId } })),
}));
- shadcn/ui – 不是组件库,而是一套可复制粘贴的组件集合。完全可控,默认配置优秀,开箱即用且可访问。
🎨 模板系统
构建一个灵活的模板系统是最棘手的部分之一。每个模板必须:
- 接受相同的属性(name、title、colours 等)
- 根据设计呈现不同的效果
- 能导出为 PNG/PDF
一个简化的示例:
// templates/ModernDark.tsx
interface TemplateProps {
card: CardData;
showQR?: boolean;
}
export function ModernDark({ card, showQR = true }: TemplateProps) {
const { fullName, title, email, phone, colors } = card;
return (
<div style={{ backgroundColor: colors?.background ?? '#fff' }}>
<h1>{fullName || 'Your Name'}</h1>
<h2>{title || 'Your Title'}</h2>
{/* …more fields… */}
{showQR && <QRCode value={generateVCard(card)} />}
</div>
);
}
📤 导出挑战
将 HTML 导出为 PNG/PDF 听起来很简单,直到你真的去尝试。以下是我成功使用的方法。
PNG 导出
import { toPng } from 'html-to-image';
export async function exportToPNG(elementId: string) {
const element = document.getElementById(elementId);
if (!element) throw new Error('Element not found');
const dataUrl = await toPng(element, {
quality: 1,
pixelRatio: 3, // High resolution
backgroundColor: '#ffffff',
});
// Trigger download
const link = document.createElement('a');
link.download = 'business-card.png';
link.href = dataUrl;
link.click();
}
PDF 导出(标准名片尺寸)
import jsPDF from 'jspdf';
import { toPng } from 'html-to-image';
export async function exportToPDF(elementId: string) {
const element = document.getElementById(elementId);
const dataUrl = await toPng(element, { pixelRatio: 3 });
// Standard business card: 3.5" × 2" (88.9 mm × 50.8 mm)
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'mm',
format: [88.9, 50.8],
});
pdf.addImage(dataUrl, 'PNG', 0, 0, 88.9, 50.8);
pdf.save('business-card.pdf');
}
vCard 生成
二维码可以编码 vCard 数据,方便人们扫描并保存联系人信息:
export function generateVCard(card: CardData): string {
return [
'BEGIN:VCARD',
'VERSION:3.0',
`FN:${card.fullName}`,
`ORG:${card.company || ''}`,
`TITLE:${card.title || ''}`,
`EMAIL:${card.email || ''}`,
`TEL:${card.phone || ''}`,
`URL:${card.website || ''}`,
'END:VCARD',
].join('\n');
}
🐛 挑战与解决方案
导出时字体未渲染
问题: 自定义字体在 PNG 导出时回退为系统字体。
解决方案: 在调用 toPng 之前确保字体已完全加载:
await document.fonts.ready;
await new Promise((resolve) => setTimeout(resolve, 100)); // 短暂延迟
const dataUrl = await toPng(element);
二维码尺寸
问题: 二维码在高分辨率下出现模糊或被裁剪。
解决方案: 将二维码以更大的尺寸渲染(pixelRatio = 3),然后在 PDF/PNG 导出时缩小。这可以在保持视觉占用面积小的同时,保留清晰的边缘。
颜色选择器性能
问题: 实时预览在持续更改颜色时出现卡顿。
解决方案: 使用防抖更新。
const debouncedSetColor = useMemo(
() => debounce((color: string) => setColor('primary', color), 50),
[]
);
📊 首周结果
- 🚀 在 Product Hunt 上发布
- 📈 500+ 独立访客
- 💾 200+ 张卡片已创建
- 🔗 来自 BetaList、AlternativeTo 的反向链接
- 💰 $0 收入(AdSense 待批准)
💸 成本明细
| 项目 | 月费用 |
|---|---|
| Vercel Hosting | $0 (Hobby) |
| MongoDB Atlas | $0 (Free tier) |
| Domain (.app) | ~ $1.25 (≈ $15/yr) |
| Total | ~ $1.25 |
就是这样——一个功能完整的 SaaS‑like 产品,成本约为 $15/年。
🎓 Lessons Learned
- Ship Fast, Iterate Later – 上线时只有 5 个模板;现在已经有 25 个。首个版本“够用”,用户会告诉我他们真正想要的功能。
- “Free” is a Feature – 在充斥着“免费增值”工具并伴随激进推销的市场中,真正免费本身就是一种差异化优势。
- Zustand > Redux for Small Projects – 如果你的状态可以放在一个文件里,就不需要 Redux。Zustand 的 API 使用起来非常愉快。
- shadcn/ui is Amazing – 复制‑粘贴组件意味着你拥有代码本身。无需与库的抽象层争斗。
- The App Router is Ready – 经过数月的“该用 Pages 还是 App Router?”的犹豫后——直接使用 App Router。它已经稳定,开发体验极佳。
🔮 接下来
- 更多模板(目标 50+)
- NFC 名片支持
- 可公开分享的链接(
freecard.app/c/username) - LinkedIn 个人资料导入
- 模板市场(用户提交的设计)
🙏 试一试
如果你需要名片,试试看:
无需注册,无水印,毫无废话。只有免费名片。
你怎么看? 我很想在评论中听到你的反馈。哪些功能会让它对你更有用?
如果你觉得这有用,请考虑:
- ⭐ 在 Product Hunt 上点赞
- 🐦 在 Twitter 上关注我,获取更多独立黑客内容
- 📧 与需要名片的人分享
标签
nextjs typescript react webdev opensource indiehacker buildinpublic sideproject
