netlify-development by mindrally/skills
npx skills add https://github.com/mindrally/skills --skill netlify-development此技能提供了在 Netlify 上构建和部署项目的全面指南,涵盖无服务器函数、边缘函数、后台函数、计划函数、Netlify Blobs、图片 CDN 和部署配置。
config 对象使用代码内配置(优于 netlify.toml)| 类型 | 用例 | 超时时间 | 路径约定 |
|---|---|---|---|
| 无服务器函数 | 标准 API 端点 | 10秒(专业版 26秒) | /.netlify/functions/名称 |
| 边缘函数 | 请求/响应修改 | 50毫秒 CPU | 自定义路径 |
| 后台函数 | 长时间运行的异步任务 | 15分钟 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
-background 后缀 |
| 计划函数 | 基于 Cron 的任务 | 10秒(专业版 26秒) | 配置的计划 |
// netlify/functions/hello.mts
import type { Context } from '@netlify/functions';
export default async (request: Request, context: Context) => {
try {
// 验证请求
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
const body = await request.json();
// 业务逻辑
const result = await processData(body);
return Response.json(result);
} catch (error) {
console.error('Function error:', error);
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
}
};
export const config = {
path: '/api/hello',
};
export const config = {
// 自定义路径(替代 /.netlify/functions/name)
path: '/api/users',
// HTTP 方法(可选,默认允许所有方法)
method: ['GET', 'POST'],
// 速率限制
rateLimit: {
windowSize: 60,
windowLimit: 100,
},
};
/.netlify/functions/{函数名称}// netlify/edge-functions/geo-redirect.ts
import type { Context } from '@netlify/edge-functions';
export default async (request: Request, context: Context) => {
const country = context.geo.country?.code || 'US';
// 根据国家/地区重定向
if (country === 'DE') {
return Response.redirect(new URL('/de', request.url));
}
// 继续访问源站
return context.next();
};
export const config = {
path: '/*',
excludedPath: ['/api/*', '/_next/*'],
};
export default async (request: Request, context: Context) => {
// 从源站获取响应
const response = await context.next();
// 修改头部
response.headers.set('X-Custom-Header', 'value');
// 转换 HTML
const html = await response.text();
const modifiedHtml = html.replace('</body>', '<script>...</script></body>');
return new Response(modifiedHtml, {
status: response.status,
headers: response.headers,
});
};
-background 后缀// netlify/functions/process-video-background.mts
import type { Context } from '@netlify/functions';
import { getStore } from '@netlify/blobs';
export default async (request: Request, context: Context) => {
const { videoId } = await request.json();
// 长时间运行的处理
const result = await processVideo(videoId);
// 存储结果以供稍后检索
const store = getStore('processed-videos');
await store.setJSON(videoId, result);
// 返回值被忽略
return new Response('Processing complete');
};
export const config = {
path: '/api/process-video',
};
// netlify/functions/get-video-status.mts
import { getStore } from '@netlify/blobs';
export default async (request: Request, context: Context) => {
const url = new URL(request.url);
const videoId = url.searchParams.get('id');
const store = getStore('processed-videos');
const result = await store.get(videoId, { type: 'json' });
if (!result) {
return Response.json({ status: 'processing' });
}
return Response.json({ status: 'complete', data: result });
};
// netlify/functions/daily-cleanup.mts
import type { Context } from '@netlify/functions';
export default async (request: Request, context: Context) => {
console.log('Running daily cleanup...');
// 清理逻辑
await cleanupOldRecords();
return new Response('Cleanup complete');
};
export const config = {
schedule: '@daily', // 或 '0 0 * * *' 表示 UTC 午夜
};
// 常见模式
export const config = {
schedule: '@hourly', // 每小时
schedule: '@daily', // 每天午夜
schedule: '@weekly', // 每周
schedule: '*/15 * * * *', // 每 15 分钟
schedule: '0 9 * * 1-5', // 工作日上午 9 点
};
import { getStore } from '@netlify/blobs';
// 获取存储
const store = getStore('my-store');
// 存储数据
await store.set('key', 'string value');
await store.setJSON('json-key', { foo: 'bar' });
// 检索数据
const value = await store.get('key');
const jsonValue = await store.get('json-key', { type: 'json' });
// 删除数据
await store.delete('key');
// 列出键
const { blobs } = await store.list();
import { getStore } from '@netlify/blobs';
const store = getStore('files');
// 存储二进制数据
const arrayBuffer = await file.arrayBuffer();
await store.set('uploads/file.pdf', arrayBuffer, {
metadata: { contentType: 'application/pdf' },
});
// 检索二进制数据
const blob = await store.get('uploads/file.pdf', { type: 'blob' });
// 站点全局存储(跨部署持久化)
const siteStore = getStore({
name: 'user-data',
siteID: context.site.id,
});
// 部署特定存储(限定于部署)
const deployStore = getStore({
name: 'cache',
deployID: context.deploy.id,
});
<!-- 基本优化 -->
<img src="/.netlify/images?url=/images/hero.jpg&w=800&q=80" alt="Hero">
<!-- 包含适配和格式 -->
<img src="/.netlify/images?url=/images/hero.jpg&w=400&h=300&fit=cover&fm=webp" alt="Hero">
url: 源图片路径(必需)w: 宽度(像素)h: 高度(像素)q: 质量(1-100)fit: cover, contain, fillfm: 格式(webp, avif, auto)function getOptimizedImageUrl(src: string, options: ImageOptions) {
const params = new URLSearchParams({
url: src,
w: String(options.width),
q: String(options.quality || 80),
fm: 'auto',
});
return `/.netlify/images?${params}`;
}
export default async (request: Request, context: Context) => {
// 访问环境变量
const apiKey = Netlify.env.get('API_KEY');
const dbUrl = process.env.DATABASE_URL;
if (!apiKey) {
console.error('API_KEY not configured');
return Response.json({ error: 'Configuration error' }, { status: 500 });
}
// 使用变量
};
export default async (request: Request, context: Context) => {
// 可用的上下文
const { site, deploy, geo, ip, requestId } = context;
console.log('Site ID:', site.id);
console.log('Deploy ID:', deploy.id);
console.log('Country:', geo.country?.code);
console.log('Request ID:', requestId);
};
[build]
command = "npm run build"
publish = "dist"
functions = "netlify/functions"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
[functions]
node_bundler = "esbuild"
[dev]
command = "npm run dev"
port = 3000
targetPort = 5173
// netlify/functions/upload.mts
import { getStore } from '@netlify/blobs';
export default async (request: Request, context: Context) => {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return Response.json({ error: 'No file provided' }, { status: 400 });
}
const store = getStore('uploads');
const key = `${Date.now()}-${file.name}`;
await store.set(key, await file.arrayBuffer(), {
metadata: {
contentType: file.type,
originalName: file.name,
},
});
return Response.json({ key, message: 'Upload successful' });
};
# 初始化新站点
netlify init
# 链接现有站点
netlify link
# 手动部署
netlify deploy
# 部署到生产环境
netlify deploy --prod
# 启动本地开发服务器
netlify dev
# 使用特定端口
netlify dev --port 8888
# 使用实时重载
netlify dev --live
# 直接调用函数
netlify functions:invoke hello --payload '{"name": "World"}'
# 仅提供函数服务
netlify functions:serve
interface ErrorResponse {
error: string;
code: string;
details?: unknown;
}
function errorResponse(status: number, error: ErrorResponse): Response {
return Response.json(error, { status });
}
export default async (request: Request, context: Context) => {
try {
// 验证
const body = await request.json();
if (!body.email) {
return errorResponse(400, {
error: 'Email is required',
code: 'MISSING_EMAIL',
});
}
// 业务逻辑
const result = await processRequest(body);
return Response.json(result);
} catch (error) {
console.error('Function error:', error);
return errorResponse(500, {
error: 'Internal server error',
code: 'INTERNAL_ERROR',
});
}
};
import { z } from 'zod';
const RequestSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
});
export default async (request: Request, context: Context) => {
const body = await request.json();
const result = RequestSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error.issues },
{ status: 400 }
);
}
// 使用已验证的数据
const { email, name } = result.data;
};
async function verifyToken(request: Request): Promise<User | null> {
const auth = request.headers.get('Authorization');
if (!auth?.startsWith('Bearer ')) {
return null;
}
const token = auth.slice(7);
// 验证令牌逻辑
return verifyJWT(token);
}
export default async (request: Request, context: Context) => {
const user = await verifyToken(request);
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// 已认证的请求处理
};
@netlify/functions 导入添加版本号-background 后缀每周安装次数
95
代码仓库
GitHub 星标数
44
首次出现
2026年1月25日
安全审计
安装于
gemini-cli81
opencode80
codex76
cursor75
github-copilot73
claude-code71
This skill provides comprehensive guidelines for building and deploying projects on Netlify, covering serverless functions, edge functions, background functions, scheduled functions, Netlify Blobs, Image CDN, and deployment configuration.
config objects (preferred over netlify.toml)| Type | Use Case | Timeout | Path Convention |
|---|---|---|---|
| Serverless | Standard API endpoints | 10s (26s Pro) | /.netlify/functions/name |
| Edge | Request/response modification | 50ms CPU | Custom paths |
| Background | Long-running async tasks | 15 minutes | -background suffix |
| Scheduled | Cron-based tasks | 10s (26s Pro) | Configured schedule |
// netlify/functions/hello.mts
import type { Context } from '@netlify/functions';
export default async (request: Request, context: Context) => {
try {
// Validate request
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
const body = await request.json();
// Business logic
const result = await processData(body);
return Response.json(result);
} catch (error) {
console.error('Function error:', error);
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
}
};
export const config = {
path: '/api/hello',
};
export const config = {
// Custom path (instead of /.netlify/functions/name)
path: '/api/users',
// HTTP methods (optional, allows all by default)
method: ['GET', 'POST'],
// Rate limiting
rateLimit: {
windowSize: 60,
windowLimit: 100,
},
};
/.netlify/functions/{function_name}// netlify/edge-functions/geo-redirect.ts
import type { Context } from '@netlify/edge-functions';
export default async (request: Request, context: Context) => {
const country = context.geo.country?.code || 'US';
// Redirect based on country
if (country === 'DE') {
return Response.redirect(new URL('/de', request.url));
}
// Continue to origin
return context.next();
};
export const config = {
path: '/*',
excludedPath: ['/api/*', '/_next/*'],
};
export default async (request: Request, context: Context) => {
// Get response from origin
const response = await context.next();
// Modify headers
response.headers.set('X-Custom-Header', 'value');
// Transform HTML
const html = await response.text();
const modifiedHtml = html.replace('</body>', '<script>...</script></body>');
return new Response(modifiedHtml, {
status: response.status,
headers: response.headers,
});
};
-background suffix// netlify/functions/process-video-background.mts
import type { Context } from '@netlify/functions';
import { getStore } from '@netlify/blobs';
export default async (request: Request, context: Context) => {
const { videoId } = await request.json();
// Long-running processing
const result = await processVideo(videoId);
// Store result for later retrieval
const store = getStore('processed-videos');
await store.setJSON(videoId, result);
// Return value is ignored
return new Response('Processing complete');
};
export const config = {
path: '/api/process-video',
};
// netlify/functions/get-video-status.mts
import { getStore } from '@netlify/blobs';
export default async (request: Request, context: Context) => {
const url = new URL(request.url);
const videoId = url.searchParams.get('id');
const store = getStore('processed-videos');
const result = await store.get(videoId, { type: 'json' });
if (!result) {
return Response.json({ status: 'processing' });
}
return Response.json({ status: 'complete', data: result });
};
// netlify/functions/daily-cleanup.mts
import type { Context } from '@netlify/functions';
export default async (request: Request, context: Context) => {
console.log('Running daily cleanup...');
// Cleanup logic
await cleanupOldRecords();
return new Response('Cleanup complete');
};
export const config = {
schedule: '@daily', // or '0 0 * * *' for midnight UTC
};
// Common patterns
export const config = {
schedule: '@hourly', // Every hour
schedule: '@daily', // Every day at midnight
schedule: '@weekly', // Every week
schedule: '*/15 * * * *', // Every 15 minutes
schedule: '0 9 * * 1-5', // 9 AM on weekdays
};
import { getStore } from '@netlify/blobs';
// Get a store
const store = getStore('my-store');
// Store data
await store.set('key', 'string value');
await store.setJSON('json-key', { foo: 'bar' });
// Retrieve data
const value = await store.get('key');
const jsonValue = await store.get('json-key', { type: 'json' });
// Delete data
await store.delete('key');
// List keys
const { blobs } = await store.list();
import { getStore } from '@netlify/blobs';
const store = getStore('files');
// Store binary data
const arrayBuffer = await file.arrayBuffer();
await store.set('uploads/file.pdf', arrayBuffer, {
metadata: { contentType: 'application/pdf' },
});
// Retrieve binary data
const blob = await store.get('uploads/file.pdf', { type: 'blob' });
// Site-wide store (persists across deploys)
const siteStore = getStore({
name: 'user-data',
siteID: context.site.id,
});
// Deploy-specific store (scoped to deployment)
const deployStore = getStore({
name: 'cache',
deployID: context.deploy.id,
});
<!-- Basic optimization -->
<img src="/.netlify/images?url=/images/hero.jpg&w=800&q=80" alt="Hero">
<!-- With fit and format -->
<img src="/.netlify/images?url=/images/hero.jpg&w=400&h=300&fit=cover&fm=webp" alt="Hero">
url: Source image path (required)w: Width in pixelsh: Height in pixelsq: Quality (1-100)fit: cover, contain, fillfm: Format (webp, avif, auto)function getOptimizedImageUrl(src: string, options: ImageOptions) {
const params = new URLSearchParams({
url: src,
w: String(options.width),
q: String(options.quality || 80),
fm: 'auto',
});
return `/.netlify/images?${params}`;
}
export default async (request: Request, context: Context) => {
// Access environment variables
const apiKey = Netlify.env.get('API_KEY');
const dbUrl = process.env.DATABASE_URL;
if (!apiKey) {
console.error('API_KEY not configured');
return Response.json({ error: 'Configuration error' }, { status: 500 });
}
// Use variables
};
export default async (request: Request, context: Context) => {
// Available context
const { site, deploy, geo, ip, requestId } = context;
console.log('Site ID:', site.id);
console.log('Deploy ID:', deploy.id);
console.log('Country:', geo.country?.code);
console.log('Request ID:', requestId);
};
[build]
command = "npm run build"
publish = "dist"
functions = "netlify/functions"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
[functions]
node_bundler = "esbuild"
[dev]
command = "npm run dev"
port = 3000
targetPort = 5173
// netlify/functions/upload.mts
import { getStore } from '@netlify/blobs';
export default async (request: Request, context: Context) => {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return Response.json({ error: 'No file provided' }, { status: 400 });
}
const store = getStore('uploads');
const key = `${Date.now()}-${file.name}`;
await store.set(key, await file.arrayBuffer(), {
metadata: {
contentType: file.type,
originalName: file.name,
},
});
return Response.json({ key, message: 'Upload successful' });
};
# Initialize new site
netlify init
# Link existing site
netlify link
# Deploy manually
netlify deploy
# Deploy to production
netlify deploy --prod
# Start local development server
netlify dev
# With specific port
netlify dev --port 8888
# With live reload
netlify dev --live
# Invoke function directly
netlify functions:invoke hello --payload '{"name": "World"}'
# Serve functions only
netlify functions:serve
interface ErrorResponse {
error: string;
code: string;
details?: unknown;
}
function errorResponse(status: number, error: ErrorResponse): Response {
return Response.json(error, { status });
}
export default async (request: Request, context: Context) => {
try {
// Validation
const body = await request.json();
if (!body.email) {
return errorResponse(400, {
error: 'Email is required',
code: 'MISSING_EMAIL',
});
}
// Business logic
const result = await processRequest(body);
return Response.json(result);
} catch (error) {
console.error('Function error:', error);
return errorResponse(500, {
error: 'Internal server error',
code: 'INTERNAL_ERROR',
});
}
};
import { z } from 'zod';
const RequestSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
});
export default async (request: Request, context: Context) => {
const body = await request.json();
const result = RequestSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: 'Validation failed', details: result.error.issues },
{ status: 400 }
);
}
// Use validated data
const { email, name } = result.data;
};
async function verifyToken(request: Request): Promise<User | null> {
const auth = request.headers.get('Authorization');
if (!auth?.startsWith('Bearer ')) {
return null;
}
const token = auth.slice(7);
// Verify token logic
return verifyJWT(token);
}
export default async (request: Request, context: Context) => {
const user = await verifyToken(request);
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Authenticated request handling
};
@netlify/functions imports-background suffix for background functionsWeekly Installs
95
Repository
GitHub Stars
44
First Seen
Jan 25, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli81
opencode80
codex76
cursor75
github-copilot73
claude-code71