const DEFAULT_SITE_TITLE = "SproutBlog"; export default { async fetch(request, env) { const url = new URL(request.url); const path = normalizePath(url.pathname); if (!env.DB) { return htmlResponse(renderLayout("配置缺失", `
请先在 wrangler.toml 里配置 D1 数据库。
${escapeHtml(error?.stack || error?.message || String(error))}
`, env), 500);
}
}
};
async function handleIndex(env, url) {
const q = (url.searchParams.get("q") || "").trim();
const posts = await listPosts(env.DB, { publishedOnly: true, q });
return htmlResponse(renderLayout(pageTitle(env, "首页"), `
${q ? `搜索:${escapeHtml(q)}
` : ""} ${posts.length ? posts.map(renderPostCard).join("") : "暂无文章。
"} `, env)); } async function handlePost(env, slug) { const post = await env.DB.prepare( "SELECT * FROM posts WHERE slug = ? AND published = 1 LIMIT 1" ).bind(decodeURIComponent(slug)).first(); if (!post) return notFoundPage(env); await env.DB.prepare( "UPDATE posts SET view_count = COALESCE(view_count, 0) + 1 WHERE id = ?" ).bind(post.id).run(); const views = (Number(post.view_count) || 0) + 1; return htmlResponse(renderLayout(`${post.title} - ${pageTitle(env)}`, `没有找到文章。
"}${escapeHtml(excerpt)}${excerpt.length >= 180 ? "…" : ""}
ID: ${post.id} | Slug: ${escapeHtml(post.slug)} | ${post.published ? "已发布" : "草稿"} | ${formatDateTime(post.updated_at)}
加载中…
页面不存在。
`, env), 404); } function corsHeaders() { return { "access-control-allow-origin": "*", "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", "access-control-allow-headers": "*" }; } async function requireAdmin(request, env) { const password = cleanText(env.ADMIN_PASSWORD); if (!password) return null; const cookies = parseCookie(request.headers.get("Cookie") || ""); const expected = await adminSessionCookieValue(env); if (cookies.sb_admin && cookies.sb_admin === expected) { return null; } const header = request.headers.get("Authorization") || ""; if (header.startsWith("Basic ")) { try { const decoded = atob(header.slice(6)); const split = decoded.indexOf(":"); const inputPassword = split >= 0 ? decoded.slice(split + 1) : ""; if (inputPassword === password) return null; } catch { // ignore } } return unauthorized(); } function parseCookie(header) { const out = {}; if (!header) return out; for (const part of header.split(";")) { const idx = part.indexOf("="); if (idx === -1) continue; const k = part.slice(0, idx).trim(); const v = part.slice(idx + 1).trim(); try { out[k] = decodeURIComponent(v); } catch { out[k] = v; } } return out; } async function adminSessionCookieValue(env) { const password = cleanText(env.ADMIN_PASSWORD); const enc = new TextEncoder(); const data = enc.encode(`${password}\0sproutblog_admin_v1`); const hash = await crypto.subtle.digest("SHA-256", data); return [...new Uint8Array(hash)].map((b) => b.toString(16).padStart(2, "0")).join(""); } function unauthorized() { return new Response("Authentication required", { status: 401, headers: { "content-type": "text/plain; charset=UTF-8" } }); } async function readBody(request) { const type = request.headers.get("content-type") || ""; if (type.includes("application/json")) { return await request.json(); } if (type.includes("multipart/form-data") || type.includes("application/x-www-form-urlencoded")) { const form = await request.formData(); const data = {}; for (const [key, value] of form.entries()) { data[key] = value; } return data; } return {}; } function cleanText(value) { return String(value ?? "").trim(); } function isTruthy(value) { return value === true || value === "true" || value === "1" || value === 1 || value === "on" || value === "yes"; } function blankPost() { return { id: "", title: "", slug: "", excerpt: "", content: "", published: 1 }; } function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function escapeAttr(value) { return escapeHtml(value).replace(/`/g, "`"); } function formatDateTime(value) { if (!value) return ""; const date = new Date(value); if (Number.isNaN(date.getTime())) return escapeHtml(value); const yyyy = date.getFullYear(); const mm = String(date.getMonth() + 1).padStart(2, "0"); const dd = String(date.getDate()).padStart(2, "0"); const hh = String(date.getHours()).padStart(2, "0"); const mi = String(date.getMinutes()).padStart(2, "0"); return `${yyyy}-${mm}-${dd} ${hh}:${mi}`; }