mirror of
https://github.com/Vb-Lg/class-history-template.git
synced 2026-11-03 17:36:47 +00:00
feat: sync security & SEO hardening from 2910 (keep placeholders)
This commit is contained in:
1 parent
2c1f508b14
commit
339e5b5639
15 files changed
+253
-128
No files matched your search
@@ -10,7 +10,7 @@
|
||||
|
||||
## 结构速览
|
||||
|
||||
- `src/site.config.ts`:班级信息占位值,各页面引用
|
||||
- `src/site.config.ts`:班级信息占位值(`SITE`)与首页各区块文案占位值(`HOME`:简介、人物志、荣誉、记忆墙、打字机句子),各页面引用
|
||||
- 示例内容(使用者应删除):`src/content/20250101.md`、`src/songs/20250101-example.json`;`public/pages/static/cover-placeholder.svg` 为占位封面
|
||||
- `functions/api/submit.ts`:投稿 → GitHub App(cms-bot,见 Wiki)建分支开 PR;`functions/api/messages.js`:留言墙(D1,绑定名 `DB`)
|
||||
- 构建:Node ≥ 22.12,`npm run build`;本地 `npm run dev` 没有 `/api/*`
|
||||
@@ -44,10 +44,8 @@ npm run dev # http://localhost:4321
|
||||
|
||||
## 定制班级信息
|
||||
|
||||
1. **`src/site.config.ts`**:班级名、顶栏 logo、首屏徽章、统计数字,改这一个文件即可。
|
||||
2. **`src/pages/index.astro`**:班级简介、人物志、荣誉、记忆墙的示例文案(搜索 `20XX` 和各区块的中文示例)。
|
||||
3. **`public/js/index.js`**:首屏打字机轮播文案(`lines` 数组)。
|
||||
4. 主题色(金色 `#e6c068`)在 `public/css/*.css` 中定义。
|
||||
1. **`src/site.config.ts`**:班级名、顶栏 logo、首屏徽章、统计数字,以及首页各区块文案(`HOME`:班级简介、人物志、荣誉、记忆墙、打字机句子、SEO 描述),改这一个文件即可。
|
||||
2. 主题色(金色 `#e6c068`)在 `public/css/*.css` 中定义。
|
||||
|
||||
## 内容格式
|
||||
|
||||
|
||||
@@ -17,11 +17,7 @@ function json(data, status = 200) {
|
||||
status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
// 如果前端和接口不同域,可加上 CORS 头;同域可删掉
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type'
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -53,6 +49,8 @@ const SCHEMA_SQL = [
|
||||
country TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
)`,
|
||||
'CREATE INDEX IF NOT EXISTS idx_messages_ip ON messages(ip)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at)',
|
||||
'ALTER TABLE messages ADD COLUMN ua TEXT',
|
||||
'ALTER TABLE messages ADD COLUMN country TEXT',
|
||||
];
|
||||
@@ -78,19 +76,6 @@ function ensureSchema(env) {
|
||||
return schemaPromise;
|
||||
}
|
||||
|
||||
/* ---------- OPTIONS(预检) ---------- */
|
||||
export async function onRequestOptions() {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
'Access-Control-Max-Age': '86400'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- GET:拉取列表 ---------- */
|
||||
export async function onRequestGet({ env }) {
|
||||
if (!env.DB) return json({ ok: false, error: '数据库未绑定' }, 500);
|
||||
|
||||
+35
-11
@@ -131,7 +131,7 @@ function extFromMime(mime: string): string {
|
||||
if (m.includes('webp')) return 'webp';
|
||||
if (m.includes('gif')) return 'gif';
|
||||
if (m.includes('avif')) return 'avif';
|
||||
if (m.includes('svg')) return 'svg';
|
||||
// SVG 刻意不支持:内含的 <script> 在同源下会执行,属存储型 XSS
|
||||
return 'bin';
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ async function handleSongSubmit(
|
||||
});
|
||||
if (!newCommit) throw new Error('创建 commit 失败');
|
||||
|
||||
const branch = `music/${ymd}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
const branch = `music/${ymd}-${crypto.randomUUID().slice(0, 8)}`;
|
||||
await api(`/repos/${owner}/${repo}/git/refs`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: newCommit.sha }),
|
||||
@@ -315,6 +315,11 @@ async function handleSongSubmit(
|
||||
}
|
||||
|
||||
// ---------- 主处理 ----------
|
||||
// 简单限流:同一 IP 两次投稿的最小间隔(秒)。只存在于当前 isolate 的内存里,
|
||||
// 属尽力而为;更严格的限流应另配 Cloudflare 的 WAF rate limiting 规则。
|
||||
const SUBMIT_COOLDOWN = 60;
|
||||
const lastSubmitAt = new Map<string, number>();
|
||||
|
||||
export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {
|
||||
const json = (data: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(data), {
|
||||
@@ -324,6 +329,26 @@ export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {
|
||||
|
||||
const meta = requestMeta(request);
|
||||
|
||||
const submitIp = meta.ip;
|
||||
if (submitIp) {
|
||||
const last = lastSubmitAt.get(submitIp);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (last !== undefined && now - last < SUBMIT_COOLDOWN) {
|
||||
return json(
|
||||
{ error: `投稿太频繁啦,请等 ${SUBMIT_COOLDOWN - (now - last)} 秒` },
|
||||
429,
|
||||
);
|
||||
}
|
||||
lastSubmitAt.set(submitIp, now);
|
||||
// 只保留最近 1000 个 IP,防 Map 无限增长
|
||||
if (lastSubmitAt.size > 1000) {
|
||||
for (const k of lastSubmitAt.keys()) {
|
||||
lastSubmitAt.delete(k);
|
||||
if (lastSubmitAt.size <= 500) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 1) 解析 multipart
|
||||
const ct = request.headers.get('content-type') || '';
|
||||
@@ -384,7 +409,9 @@ export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {
|
||||
|
||||
if (cover instanceof File && cover.size > 0) {
|
||||
if (cover.size > 5 * 1024 * 1024) return json({ error: '封面图超过 5 MB' }, 413);
|
||||
if (!cover.type.startsWith('image/')) return json({ error: '封面必须是图片' }, 400);
|
||||
if (!cover.type.startsWith('image/') || cover.type.includes('svg')) {
|
||||
return json({ error: '封面必须是 PNG/JPG/WebP/GIF/AVIF 图片' }, 400);
|
||||
}
|
||||
|
||||
const ext = extFromMime(cover.type);
|
||||
const base = `${yyyymmdd(date)}-cover`;
|
||||
@@ -395,16 +422,13 @@ export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {
|
||||
coverBlob = { content: abToBase64(buf), encoding: 'base64' };
|
||||
}
|
||||
|
||||
// 3) 决定文章文件名(同日冲突 → -1, -2, …)
|
||||
// 3) 决定文章文件名:优先用日期;已被占用(同日已有投稿)时退回
|
||||
// 日期-随机后缀,避免「先探测再提交」的竞态导致两个并发投稿撞名
|
||||
let articleName = `${yyyymmdd(date)}.md`;
|
||||
let articlePath = `${contentDir}/${articleName}`;
|
||||
let n = 0;
|
||||
// 注意:Contents API 的 GET 返回 404 表示不存在;gh() 已把 404 转成 null
|
||||
while (await api(`/repos/${owner}/${repo}/contents/${encodeURIComponent(articlePath)}?ref=${baseBranch}`)) {
|
||||
n += 1;
|
||||
articleName = `${yyyymmdd(date)}-${n}.md`;
|
||||
if (await api(`/repos/${owner}/${repo}/contents/${encodeURIComponent(articlePath)}?ref=${baseBranch}`)) {
|
||||
articleName = `${yyyymmdd(date)}-${Math.random().toString(36).slice(2, 8)}.md`;
|
||||
articlePath = `${contentDir}/${articleName}`;
|
||||
if (n > 50) return json({ error: '同日投稿过多' }, 409);
|
||||
}
|
||||
|
||||
const markdown = renderMarkdown({
|
||||
@@ -467,7 +491,7 @@ export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {
|
||||
});
|
||||
if (!newCommit) throw new Error('创建 commit 失败');
|
||||
|
||||
const branch = `chronicle/${yyyymmdd(date)}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
const branch = `chronicle/${yyyymmdd(date)}-${crypto.randomUUID().slice(0, 8)}`;
|
||||
await api(`/repos/${owner}/${repo}/git/refs`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: newCommit.sha }),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="14" fill="#0d1020"/>
|
||||
<path d="M32 11l6.2 12.6 13.8 2-10 9.7 2.4 13.7L32 42.5l-12.4 6.5 2.4-13.7-10-9.7 13.8-2z" fill="#e6c068"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 234 B |
+29
-3
@@ -1,5 +1,10 @@
|
||||
/* ================= 星空背景 ================= */
|
||||
(function () {
|
||||
// 用户偏好减少动态效果时不播星空,保留静态渐变背景
|
||||
const reducedMotion = window.matchMedia &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (reducedMotion) return;
|
||||
|
||||
const canvas = document.getElementById('stars');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let w, h, stars = [], dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
@@ -44,11 +49,31 @@
|
||||
/* ================= 打字机 ================= */
|
||||
(function () {
|
||||
const el = document.getElementById('typed');
|
||||
const lines = [
|
||||
// 句子由 src/site.config.ts 的 HOME.typedLines 注入(首页内联 JSON),保持单一定制入口
|
||||
let lines = null;
|
||||
try {
|
||||
const src = document.getElementById('typed-lines');
|
||||
if (src) {
|
||||
const parsed = JSON.parse(src.textContent);
|
||||
if (Array.isArray(parsed) && parsed.length && parsed.every(l => typeof l === 'string')) {
|
||||
lines = parsed;
|
||||
}
|
||||
}
|
||||
} catch (e) { /* 配置缺失时回退到内置句子 */ }
|
||||
if (!lines) {
|
||||
lines = [
|
||||
'我们的故事,从那年秋天开始',
|
||||
'三年同窗,一生朋友',
|
||||
'愿此去前程似锦,再相逢依然少年'
|
||||
];
|
||||
}
|
||||
// 减少动态效果偏好:直接完整展示第一句,不做打字动画
|
||||
const reducedMotion = window.matchMedia &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (reducedMotion) {
|
||||
el.textContent = lines[0];
|
||||
return;
|
||||
}
|
||||
let li = 0, ci = 0, deleting = false;
|
||||
|
||||
function tick() {
|
||||
@@ -262,11 +287,12 @@
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
btn.textContent = err.message && err.message.length <= 10 ? err.message : '发送失败';
|
||||
// 接口返回的中文提示(如「发得太快啦,请等 X 秒」)直接展示,超长才降级
|
||||
btn.textContent = err.message && err.message.length <= 30 ? err.message : '发送失败';
|
||||
setTimeout(() => {
|
||||
btn.textContent = BTN_TEXT;
|
||||
btn.disabled = false;
|
||||
}, 1600);
|
||||
}, 2400);
|
||||
sending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /api/
|
||||
@@ -37,6 +37,11 @@ const fullTitle = `${date.getFullYear()}·${pad(date.getMonth() + 1)}·${pad(dat
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{fullTitle}</title>
|
||||
{summary && <meta name="description" content={summary} />}
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
{summary && <meta property="og:description" content={summary} />}
|
||||
<meta property="og:type" content="article" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
</head>
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
import { SITE } from "../site.config";
|
||||
// Cloudflare Pages 会自动把构建产物里的 404.html 用作未匹配路由的兜底页
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>页面不存在 · {SITE.className}班史</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<style>
|
||||
.nf {
|
||||
text-align: center;
|
||||
padding: 80px 0 40px;
|
||||
}
|
||||
.nf-code {
|
||||
font-size: 5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--gold, #e6c068);
|
||||
text-shadow: 0 0 40px rgba(230, 192, 104, 0.35);
|
||||
}
|
||||
.nf-text {
|
||||
margin-top: 10px;
|
||||
color: var(--muted, #96a0b8);
|
||||
}
|
||||
.nf-actions {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.nf-actions a {
|
||||
color: var(--gold, #e6c068);
|
||||
text-decoration: none;
|
||||
border: 1px solid rgba(230, 192, 104, 0.35);
|
||||
padding: 8px 20px;
|
||||
border-radius: 999px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.nf-actions a:hover {
|
||||
background: rgba(230, 192, 104, 0.12);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<canvas id="stars"></canvas>
|
||||
<div class="glow g1"></div>
|
||||
<div class="glow g2"></div>
|
||||
<div class="glow g3"></div>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="nf container reveal">
|
||||
<div class="nf-code">404</div>
|
||||
<p class="nf-text">这一页还没被写进班史。</p>
|
||||
<div class="nf-actions">
|
||||
<a href="/">← 返回首页</a>
|
||||
<a href="/chronicle">📖 班史列表</a>
|
||||
<a href="/music">♫ 歌单</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="sig">{SITE.className} · 永远</div>
|
||||
<p class="small">© {SITE.className}班史 · 制作于某个想起你们的夜晚</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script is:inline src="/js/pages.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -32,6 +32,13 @@ const items = posts
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>班史列表 · {SITE.className}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={`${SITE.className}班史全部文章的归档列表,按时间倒序排列。`}
|
||||
/>
|
||||
<meta property="og:title" content={`班史列表 · ${SITE.className}`} />
|
||||
<meta property="og:type" content="website" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<style>
|
||||
.post-list {
|
||||
|
||||
@@ -11,7 +11,9 @@ const hasTurnstile = turnstileSiteKey.length > 0;
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>班史投稿</title>
|
||||
<title>班史投稿 · {SITE.className}</title>
|
||||
<meta property="og:title" content={`班史投稿 · ${SITE.className}`} />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
{
|
||||
hasTurnstile && (
|
||||
<script
|
||||
|
||||
+27
-90
@@ -1,14 +1,19 @@
|
||||
---
|
||||
// 文章列表已移至 /chronicle(班史列表页),首页编年史区块留空
|
||||
import { SITE } from "../site.config";
|
||||
import { SITE, HOME } from "../site.config";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{SITE.className} · 班史</title>
|
||||
<meta name="description" content={HOME.description} />
|
||||
<meta property="og:title" content={`${SITE.className} · 班史`} />
|
||||
<meta property="og:description" content={HOME.description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/css/index.css" />
|
||||
</head>
|
||||
|
||||
@@ -53,13 +58,7 @@ import { SITE } from "../site.config";
|
||||
<h2 class="sec-title">
|
||||
一间教室,<em>三年光阴</em>
|
||||
</h2>
|
||||
<p>
|
||||
那年秋天,几十个互不相识的名字被写进同一张座位表。风扇转过无数个燥热的午后,黑板擦落下的粉笔灰在阳光里飘成一场小雪。我们从陌生到熟悉,从并肩到并肩。
|
||||
</p>
|
||||
<p>
|
||||
这里有早读时此起彼伏的背书声,有课间十分钟的喧闹与追逐,有考试前互相打气的纸条,也有晚自习窗外那片被灯光照亮的夜色。这些琐碎的日常,后来都成了我们回不去的黄金时代。
|
||||
</p>
|
||||
<p>谨以此页,记录 {SITE.className} 走过的每一步。</p>
|
||||
{HOME.intro.map((para) => <p>{para}</p>)}
|
||||
</div>
|
||||
<div class="stats reveal">
|
||||
<div class="stat">
|
||||
@@ -88,7 +87,6 @@ import { SITE } from "../site.config";
|
||||
从初见的那一天起,每一段日子都有了名字。
|
||||
</p>
|
||||
</div>
|
||||
<!-- 文章列表已移至 /chronicle,此处留空 -->
|
||||
<div class="timeline"></div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -102,42 +100,14 @@ import { SITE } from "../site.config";
|
||||
</div>
|
||||
|
||||
<div class="people">
|
||||
{HOME.people.map((p) => (
|
||||
<div class="person reveal">
|
||||
<div class="avatar">👑</div>
|
||||
<h3>班长</h3>
|
||||
<div class="role">CAPTAIN</div>
|
||||
<p class="quote">"都安静一下,我说个事儿。"</p>
|
||||
</div>
|
||||
<div class="person reveal">
|
||||
<div class="avatar">📚</div>
|
||||
<h3>学习委员</h3>
|
||||
<div class="role">STUDY</div>
|
||||
<p class="quote">"作业收一下,最后一排。"</p>
|
||||
</div>
|
||||
<div class="person reveal">
|
||||
<div class="avatar">🏃</div>
|
||||
<h3>体育委员</h3>
|
||||
<div class="role">SPORTS</div>
|
||||
<p class="quote">"集合!快点儿,别磨蹭!"</p>
|
||||
</div>
|
||||
<div class="person reveal">
|
||||
<div class="avatar">🎨</div>
|
||||
<h3>文艺委员</h3>
|
||||
<div class="role">ART</div>
|
||||
<p class="quote">"这次板报,我们拿第一。"</p>
|
||||
</div>
|
||||
<div class="person reveal">
|
||||
<div class="avatar">🧹</div>
|
||||
<h3>劳动委员</h3>
|
||||
<div class="role">DUTY</div>
|
||||
<p class="quote">"今天值日的是第三组,别忘了。"</p>
|
||||
</div>
|
||||
<div class="person reveal">
|
||||
<div class="avatar">📣</div>
|
||||
<h3>宣传委员</h3>
|
||||
<div class="role">MEDIA</div>
|
||||
<p class="quote">"照片我都存着呢,一张没删。"</p>
|
||||
<div class="avatar">{p.avatar}</div>
|
||||
<h3>{p.title}</h3>
|
||||
<div class="role">{p.role}</div>
|
||||
<p class="quote">"{p.quote}"</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -150,48 +120,15 @@ import { SITE } from "../site.config";
|
||||
<p class="sec-desc">属于我们共同的勋章。</p>
|
||||
</div>
|
||||
<div class="honors">
|
||||
{HOME.honors.map((h) => (
|
||||
<div class="honor reveal">
|
||||
<div class="medal">🏆</div>
|
||||
<div class="medal">{h.medal}</div>
|
||||
<div>
|
||||
<h4>校运动会团体总分第一</h4>
|
||||
<p>20XX · 秋季运动会</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="honor reveal">
|
||||
<div class="medal">🥇</div>
|
||||
<div>
|
||||
<h4>优秀班集体</h4>
|
||||
<p>20XX—20XX 学年</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="honor reveal">
|
||||
<div class="medal">🎭</div>
|
||||
<div>
|
||||
<h4>校园艺术节最佳创意奖</h4>
|
||||
<p>20XX · 元旦汇演</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="honor reveal">
|
||||
<div class="medal">📖</div>
|
||||
<div>
|
||||
<h4>经典诵读比赛一等奖</h4>
|
||||
<p>20XX · 读书节</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="honor reveal">
|
||||
<div class="medal">🏀</div>
|
||||
<div>
|
||||
<h4>年级篮球赛亚军</h4>
|
||||
<p>20XX · 春季</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="honor reveal">
|
||||
<div class="medal">⭐</div>
|
||||
<div>
|
||||
<h4>文明班级 · 三连冠</h4>
|
||||
<p>连续三个学期</p>
|
||||
<h4>{h.title}</h4>
|
||||
<p>{h.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -207,14 +144,9 @@ import { SITE } from "../site.config";
|
||||
</p>
|
||||
</div>
|
||||
<div class="gallery">
|
||||
<div class="photo reveal"><span>开学第一天</span></div>
|
||||
<div class="photo reveal"><span>运动会合影</span></div>
|
||||
<div class="photo reveal"><span>元旦晚会</span></div>
|
||||
<div class="photo reveal"><span>研学路上</span></div>
|
||||
<div class="photo reveal"><span>教室里</span></div>
|
||||
<div class="photo reveal"><span>黑板的字</span></div>
|
||||
<div class="photo reveal"><span>操场黄昏</span></div>
|
||||
<div class="photo reveal"><span>毕业照</span></div>
|
||||
{HOME.gallery.map((label) => (
|
||||
<div class="photo reveal"><span>{label}</span></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -251,6 +183,11 @@ import { SITE } from "../site.config";
|
||||
</div>
|
||||
<button id="top" aria-label="回到顶部">↑</button>
|
||||
<!-- <Footer /> -->
|
||||
<script
|
||||
type="application/json"
|
||||
id="typed-lines"
|
||||
set:html={JSON.stringify(HOME.typedLines).replace(/</g, "\\u003c")}
|
||||
/>
|
||||
<script src="/js/index.js" is:inline></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,6 +12,8 @@ const hasTurnstile = turnstileSiteKey.length > 0;
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>音乐投稿 · {SITE.className}</title>
|
||||
<meta property="og:title" content={`音乐投稿 · ${SITE.className}`} />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
{
|
||||
hasTurnstile && (
|
||||
<script
|
||||
|
||||
@@ -40,6 +40,13 @@ const songsJson = JSON.stringify(songs).replace(/</g, "\\u003c");
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>班级歌单 · {SITE.className}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={`${SITE.className}的班级歌单:同学们推荐的歌曲与它们背后的故事。`}
|
||||
/>
|
||||
<meta property="og:title" content={`班级歌单 · ${SITE.className}`} />
|
||||
<meta property="og:type" content="website" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<style>
|
||||
.song-list {
|
||||
|
||||
@@ -20,3 +20,51 @@ export const SITE = {
|
||||
memories: 999,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** 首页各区块的展示文案:换成你们班自己的内容即可,页面会自动引用。 */
|
||||
export const HOME = {
|
||||
/** 首页 SEO 用的一句话描述 */
|
||||
description:
|
||||
"XX届X班的线上班史:编年史、班级歌单与留言墙,记录我们一起走过的日子。",
|
||||
/** 首屏打字机轮播的句子 */
|
||||
typedLines: [
|
||||
"我们的故事,从那年秋天开始",
|
||||
"三年同窗,一生朋友",
|
||||
"愿此去前程似锦,再相逢依然少年",
|
||||
],
|
||||
/** 「班级简介」正文段落 */
|
||||
intro: [
|
||||
"那年秋天,几十个互不相识的名字被写进同一张座位表。风扇转过无数个燥热的午后,黑板擦落下的粉笔灰在阳光里飘成一场小雪。我们从陌生到熟悉,从并肩到并肩。",
|
||||
"这里有早读时此起彼伏的背书声,有课间十分钟的喧闹与追逐,有考试前互相打气的纸条,也有晚自习窗外那片被灯光照亮的夜色。这些琐碎的日常,后来都成了我们回不去的黄金时代。",
|
||||
"谨以此页,记录我们班走过的每一步。",
|
||||
],
|
||||
/** 「班级人物志」卡片 */
|
||||
people: [
|
||||
{ avatar: "👑", title: "班长", role: "CAPTAIN", quote: "都安静一下,我说个事儿。" },
|
||||
{ avatar: "📚", title: "学习委员", role: "STUDY", quote: "作业收一下,最后一排。" },
|
||||
{ avatar: "🏃", title: "体育委员", role: "SPORTS", quote: "集合!快点儿,别磨蹭!" },
|
||||
{ avatar: "🎨", title: "文艺委员", role: "ART", quote: "这次板报,我们拿第一。" },
|
||||
{ avatar: "🧹", title: "劳动委员", role: "DUTY", quote: "今天值日的是第三组,别忘了。" },
|
||||
{ avatar: "📣", title: "宣传委员", role: "MEDIA", quote: "照片我都存着呢,一张没删。" },
|
||||
],
|
||||
/** 「班级荣誉」条目 */
|
||||
honors: [
|
||||
{ medal: "🏆", title: "校运动会团体总分第一", detail: "20XX · 秋季运动会" },
|
||||
{ medal: "🥇", title: "优秀班集体", detail: "20XX—20XX 学年" },
|
||||
{ medal: "🎭", title: "校园艺术节最佳创意奖", detail: "20XX · 元旦汇演" },
|
||||
{ medal: "📖", title: "经典诵读比赛一等奖", detail: "20XX · 读书节" },
|
||||
{ medal: "🏀", title: "年级篮球赛亚军", detail: "20XX · 春季" },
|
||||
{ medal: "⭐", title: "文明班级 · 三连冠", detail: "连续三个学期" },
|
||||
],
|
||||
/** 「班级记忆」照片占位标签 */
|
||||
gallery: [
|
||||
"开学第一天",
|
||||
"运动会合影",
|
||||
"元旦晚会",
|
||||
"研学路上",
|
||||
"教室里",
|
||||
"黑板的字",
|
||||
"操场黄昏",
|
||||
"毕业照",
|
||||
],
|
||||
} as const;
|
||||
Reference in new issue
Block a user