Skip to main content

博客维护和优化

持续集成/持续部署

自动化测试

单元测试

// __tests__/components/PostList.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import PostList from '@/components/PostList';

describe('PostList Component', () => {
const mockPosts = [
{
id: '1',
title: 'Test Post',
excerpt: 'Test excerpt',
date: '2024-01-01'
}
];

it('renders posts correctly', () => {
render(<PostList posts={mockPosts} />);
expect(screen.getByText('Test Post')).toBeInTheDocument();
expect(screen.getByText('Test excerpt')).toBeInTheDocument();
});

it('filters posts based on search', () => {
render(<PostList posts={mockPosts} />);
const searchInput = screen.getByPlaceholderText('搜索文章...');
fireEvent.change(searchInput, { target: { value: 'nonexistent' } });
expect(screen.queryByText('Test Post')).not.toBeInTheDocument();
});
});

E2E测试

// cypress/e2e/blog.cy.ts
describe('Blog', () => {
beforeEach(() => {
cy.visit('/');
});

it('navigates to post detail', () => {
cy.get('article').first().click();
cy.url().should('include', '/posts/');
cy.get('h1').should('be.visible');
});

it('filters posts using search', () => {
cy.get('input[type="search"]').type('test');
cy.get('article').should('have.length.gt', 0);
});
});

部署流水线

GitHub Actions配置

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'

- name: Install dependencies
run: npm ci

- name: Run linter
run: npm run lint

- name: Run tests
run: npm test

- name: Run E2E tests
run: npm run test:e2e

deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}

性能优化

图片优化

图片组件

// components/OptimizedImage.tsx
import Image from 'next/image';
import { useState } from 'react';

interface OptimizedImageProps {
src: string;
alt: string;
width: number;
height: number;
}

export function OptimizedImage({ src, alt, width, height }: OptimizedImageProps) {
const [isLoading, setIsLoading] = useState(true);

return (
<div className="relative">
<Image
src={src}
alt={alt}
width={width}
height={height}
quality={75}
loading="lazy"
onLoadingComplete={() => setIsLoading(false)}
className={`
duration-700 ease-in-out
${isLoading ? 'scale-110 blur-2xl grayscale' : 'scale-100 blur-0 grayscale-0'}
`}
/>
{isLoading && (
<div className="absolute inset-0 bg-gray-200 animate-pulse" />
)}
</div>
);
}

图片处理工具

// lib/imageUtils.ts
import sharp from 'sharp';

export async function optimizeImage(buffer: Buffer) {
return sharp(buffer)
.resize(800, null, {
withoutEnlargement: true,
fit: 'inside'
})
.webp({ quality: 80 })
.toBuffer();
}

代码优化

代码分割

// pages/posts/[slug].tsx
import dynamic from 'next/dynamic';

const CommentSection = dynamic(() => import('@/components/CommentSection'), {
loading: () => <CommentSkeleton />,
ssr: false
});

const RelatedPosts = dynamic(() => import('@/components/RelatedPosts'), {
loading: () => <RelatedPostsSkeleton />
});

性能监控

// lib/analytics.ts
export function reportWebVitals(metric: any) {
const body = {
name: metric.name,
value: metric.value,
label: metric.label,
id: metric.id
};

if (process.env.NODE_ENV === 'production') {
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify(body)
});
}
}

内容管理

文章管理系统

文章编辑器

// components/MDXEditor.tsx
import { MDXEditor, MDXEditorMethods } from '@mdxeditor/editor';
import { useRef } from 'react';

interface EditorProps {
initialContent: string;
onSave: (content: string) => void;
}

export function Editor({ initialContent, onSave }: EditorProps) {
const editorRef = useRef<MDXEditorMethods>(null);

const handleSave = () => {
const content = editorRef.current?.getMarkdown();
if (content) {
onSave(content);
}
};

return (
<div className="editor-container">
<MDXEditor
ref={editorRef}
markdown={initialContent}
onChange={handleSave}
plugins={[
// 添加所需插件
]}
/>
</div>
);
}

草稿系统

// lib/drafts.ts
import { prisma } from './prisma';

export async function saveDraft(postId: string, content: string) {
return prisma.draft.upsert({
where: { postId },
update: { content },
create: {
postId,
content,
lastSaved: new Date()
}
});
}

export async function publishDraft(postId: string) {
const draft = await prisma.draft.findUnique({
where: { postId }
});

if (!draft) {
throw new Error('Draft not found');
}

await prisma.$transaction([
prisma.post.update({
where: { id: postId },
data: { content: draft.content }
}),
prisma.draft.delete({
where: { postId }
})
]);
}

SEO优化

Meta标签管理

// components/SEO.tsx
import { NextSeo } from 'next-seo';

interface SEOProps {
title: string;
description: string;
canonical?: string;
openGraph?: {
title: string;
description: string;
images: Array<{ url: string; alt: string }>;
};
}

export function SEO({ title, description, canonical, openGraph }: SEOProps) {
return (
<NextSeo
title={title}
description={description}
canonical={canonical}
openGraph={{
title: openGraph?.title || title,
description: openGraph?.description || description,
images: openGraph?.images,
site_name: '我的技术博客'
}}
twitter={{
handle: '@yourhandle',
cardType: 'summary_large_image'
}}
/>
);
}

站点地图生成

// scripts/generate-sitemap.ts
import { SitemapStream, streamToPromise } from 'sitemap';
import { createWriteStream } from 'fs';
import { getAllPosts } from '@/lib/posts';

async function generateSitemap() {
const sitemap = new SitemapStream({
hostname: 'https://yourblog.com'
});

// 添加静态页面
sitemap.write({
url: '/',
changefreq: 'daily',
priority: 1
});

// 添加博客文章
const posts = await getAllPosts();
posts.forEach(post => {
sitemap.write({
url: `/posts/${post.slug}`,
changefreq: 'weekly',
priority: 0.7,
lastmod: post.updatedAt
});
});

sitemap.end();

const sitemapXML = await streamToPromise(sitemap);
createWriteStream('./public/sitemap.xml').write(sitemapXML);
}

安全维护

安全更新

依赖检查

// scripts/check-dependencies.ts
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

async function checkDependencies() {
try {
// 检查npm包更新
const { stdout: npmOutput } = await execAsync('npm audit');
console.log('NPM Security Audit:', npmOutput);

// 使用Snyk检查漏洞
const { stdout: snykOutput } = await execAsync('snyk test');
console.log('Snyk Security Test:', snykOutput);
} catch (error) {
console.error('Security check failed:', error);
process.exit(1);
}
}

安全中间件

// middleware/security.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
const response = NextResponse.next();

// 添加安全头部
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval';"
);

return response;
}

备份策略

数据备份

// scripts/backup.ts
import { exec } from 'child_process';
import { uploadToS3 } from './s3';

async function backupDatabase() {
const timestamp = new Date().toISOString();
const filename = `backup-${timestamp}.sql`;

try {
// 导出数据库
await exec(`pg_dump ${process.env.DATABASE_URL} > ${filename}`);

// 上传到S3
await uploadToS3(filename, `backups/${filename}`);

// 清理本地文件
await exec(`rm ${filename}`);

console.log(`Backup completed: ${filename}`);
} catch (error) {
console.error('Backup failed:', error);
}
}

自动备份配置

# .github/workflows/backup.yml
name: Database Backup

on:
schedule:
- cron: '0 0 * * *' # 每天运行

jobs:
backup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

- name: Run backup script
run: npm run backup
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}