From 0587abd8269882a071668b5b01709b1c952cabbe Mon Sep 17 00:00:00 2001 From: liyunze <50455574+liyunze-coding@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:21:30 +0800 Subject: [PATCH] added meta tags --- src/app.d.ts | 4 +- src/app.html | 4 -- src/lib/components/Seo.svelte | 14 ++++++ src/lib/seo.spec.ts | 29 +++++++++++ src/lib/seo.ts | 59 +++++++++++++++++++++++ src/lib/server/pageSeo.ts | 25 ++++++++++ src/routes/+layout.svelte | 5 ++ src/routes/[code]/+page.server.ts | 4 ++ src/routes/[code]/[id]/+page.server.ts | 4 ++ src/routes/[code]/[id]/+page.svelte | 5 ++ src/routes/notes/[id]/+page.server.ts | 4 ++ src/routes/notes/[id]/+page.svelte | 8 +-- src/routes/questions/[id]/+page.server.ts | 4 ++ src/routes/questions/[id]/+page.svelte | 8 +-- src/routes/units/[code]/+page.server.ts | 4 ++ src/routes/units/[code]/+page.svelte | 13 +++-- 16 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 src/lib/components/Seo.svelte create mode 100644 src/lib/seo.spec.ts create mode 100644 src/lib/seo.ts create mode 100644 src/lib/server/pageSeo.ts create mode 100644 src/routes/[code]/+page.server.ts create mode 100644 src/routes/[code]/[id]/+page.server.ts create mode 100644 src/routes/notes/[id]/+page.server.ts create mode 100644 src/routes/questions/[id]/+page.server.ts create mode 100644 src/routes/units/[code]/+page.server.ts diff --git a/src/app.d.ts b/src/app.d.ts index a732ec7..04043da 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -4,7 +4,9 @@ export {}; declare global { namespace App { - interface PageData {} + interface PageData { + seo?: import("$lib/seo").PageSeo; + } interface PageState {} interface Platform {} } diff --git a/src/app.html b/src/app.html index bbb66da..29292ee 100644 --- a/src/app.html +++ b/src/app.html @@ -3,10 +3,6 @@ - diff --git a/src/lib/components/Seo.svelte b/src/lib/components/Seo.svelte new file mode 100644 index 0000000..07e26bb --- /dev/null +++ b/src/lib/components/Seo.svelte @@ -0,0 +1,14 @@ + + + + {#if seo} + {seo.title} + + + + {/if} + diff --git a/src/lib/seo.spec.ts b/src/lib/seo.spec.ts new file mode 100644 index 0000000..453e077 --- /dev/null +++ b/src/lib/seo.spec.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { postSeo, unitSeo, withSiteName } from "./seo"; + +describe("postSeo", () => { + it("uses the post title and markdown preview", () => { + const seo = postSeo({ + title: "Week 3 notes", + content: "# Intro\n\nThese are **study** notes.", + unit: { code: "SIT232" }, + }); + + expect(seo.title).toBe(withSiteName("Week 3 notes")); + expect(seo.description).toBe("Intro These are study notes."); + }); +}); + +describe("unitSeo", () => { + it("includes unit codes, name, and description", () => { + const seo = unitSeo({ + code: "SIT232", + code2: "SIT772", + name: "Object-Oriented Development", + description: "Classes, objects, and design.", + }); + + expect(seo.title).toBe(withSiteName("SIT232 / SIT772 · Object-Oriented Development")); + expect(seo.description).toBe("Classes, objects, and design."); + }); +}); diff --git a/src/lib/seo.ts b/src/lib/seo.ts new file mode 100644 index 0000000..74cfda7 --- /dev/null +++ b/src/lib/seo.ts @@ -0,0 +1,59 @@ +import { previewMarkdown } from "$lib/markdown"; + +export const SITE_NAME = "Notebook"; +export const SITE_DESCRIPTION = + "A resource hub for Deakin University students. Shared study notes, questions and answers for SIT and Mathematics units."; + +export type PageSeo = { + title: string; + description: string; +}; + +export function withSiteName(title: string): string { + return `${title} — ${SITE_NAME}`; +} + +function unitLabel(unit: { code: string; code2?: string }): string { + return unit.code2 ? `${unit.code} / ${unit.code2}` : unit.code; +} + +export function postSeo(post: { + title: string; + content: string; + unit?: { code: string; code2?: string } | null; +}): PageSeo { + const preview = previewMarkdown(post.content); + const unit = post.unit ? unitLabel(post.unit) : ""; + const description = + preview || + (unit ? `${post.title} — ${unit} on DSEC Notebook.` : `${post.title} on DSEC Notebook.`); + + return { + title: withSiteName(post.title), + description, + }; +} + +export function unitSeo(unit: { + code: string; + code2?: string; + name: string; + description?: string; +}): PageSeo { + const codes = unitLabel(unit); + const description = + unit.description?.trim() || + `Notes and questions for ${unit.name} (${codes}) on DSEC Notebook.`; + + return { + title: withSiteName(`${codes} · ${unit.name}`), + description, + }; +} + +export function notFoundSeo(kind: "Post" | "Unit"): PageSeo { + return { + title: withSiteName(`${kind} not found`), + description: SITE_DESCRIPTION, + }; +} diff --git a/src/lib/server/pageSeo.ts b/src/lib/server/pageSeo.ts new file mode 100644 index 0000000..1762958 --- /dev/null +++ b/src/lib/server/pageSeo.ts @@ -0,0 +1,25 @@ +import { call } from "$lib/server/api"; +import { notFoundSeo, postSeo, unitSeo, type PageSeo } from "$lib/seo"; + +function unitMatchesCode( + unit: { code?: string; code2?: string } | null | undefined, + code: string, +): boolean { + const requested = code.toLowerCase(); + return [unit?.code, unit?.code2].some((value) => value?.toLowerCase() === requested); +} + +export async function loadPostSeo(id: string, requestedCode?: string): Promise<{ seo: PageSeo }> { + const note = await call("details:getNoteWithDetails", { id }); + const post = note ?? (await call("details:getQuestionWithDetails", { id })); + if (!post || (requestedCode && !unitMatchesCode(post.unit, requestedCode))) { + return { seo: notFoundSeo("Post") }; + } + return { seo: postSeo(post) }; +} + +export async function loadUnitSeo(code: string): Promise<{ seo: PageSeo }> { + const unit = await call("units:getByCode", { code }); + if (!unit) return { seo: notFoundSeo("Unit") }; + return { seo: unitSeo(unit) }; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index cea1dff..468e9f9 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,5 +1,7 @@ +{#if loading || !postType} + +{/if} + {#if loading}

Loading

diff --git a/src/routes/notes/[id]/+page.server.ts b/src/routes/notes/[id]/+page.server.ts new file mode 100644 index 0000000..b7d7a59 --- /dev/null +++ b/src/routes/notes/[id]/+page.server.ts @@ -0,0 +1,4 @@ +import { loadPostSeo } from "$lib/server/pageSeo"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = ({ params }) => loadPostSeo(params.id); diff --git a/src/routes/notes/[id]/+page.svelte b/src/routes/notes/[id]/+page.svelte index 9699890..7e18375 100644 --- a/src/routes/notes/[id]/+page.svelte +++ b/src/routes/notes/[id]/+page.svelte @@ -7,6 +7,8 @@ import Markdown from "$lib/components/Markdown.svelte"; import MarkdownEditor from "$lib/components/MarkdownEditor.svelte"; import CommentThread from "$lib/components/CommentThread.svelte"; + import Seo from "$lib/components/Seo.svelte"; + import { postSeo } from "$lib/seo"; import { timeAgo } from "$lib/time"; import type { NoteDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types"; @@ -28,6 +30,8 @@ let editError = $state(""); let editLoading = $state(false); + const seo = $derived.by(() => (note ? postSeo({ ...note, unit }) : page.data.seo)); + $effect(() => { const id = page.params.id; let cancelled = false; @@ -157,9 +161,7 @@ } - - {note?.title ?? "Loading..."} — Notebook - +
{#if loading} diff --git a/src/routes/questions/[id]/+page.server.ts b/src/routes/questions/[id]/+page.server.ts new file mode 100644 index 0000000..b7d7a59 --- /dev/null +++ b/src/routes/questions/[id]/+page.server.ts @@ -0,0 +1,4 @@ +import { loadPostSeo } from "$lib/server/pageSeo"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = ({ params }) => loadPostSeo(params.id); diff --git a/src/routes/questions/[id]/+page.svelte b/src/routes/questions/[id]/+page.svelte index 9e51d36..6db2795 100644 --- a/src/routes/questions/[id]/+page.svelte +++ b/src/routes/questions/[id]/+page.svelte @@ -7,6 +7,8 @@ import Markdown from "$lib/components/Markdown.svelte"; import MarkdownEditor from "$lib/components/MarkdownEditor.svelte"; import CommentThread from "$lib/components/CommentThread.svelte"; + import Seo from "$lib/components/Seo.svelte"; + import { postSeo } from "$lib/seo"; import { timeAgo } from "$lib/time"; import type { QuestionDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types"; @@ -27,6 +29,8 @@ let editError = $state(""); let editLoading = $state(false); + const seo = $derived.by(() => (question ? postSeo({ ...question, unit }) : page.data.seo)); + $effect(() => { const id = page.params.id; let cancelled = false; @@ -147,9 +151,7 @@ } - - {question?.title ?? "Loading..."} — Notebook - +
{#if loading} diff --git a/src/routes/units/[code]/+page.server.ts b/src/routes/units/[code]/+page.server.ts new file mode 100644 index 0000000..93dcf81 --- /dev/null +++ b/src/routes/units/[code]/+page.server.ts @@ -0,0 +1,4 @@ +import { loadUnitSeo } from "$lib/server/pageSeo"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = ({ params }) => loadUnitSeo(params.code); diff --git a/src/routes/units/[code]/+page.svelte b/src/routes/units/[code]/+page.svelte index 45dfb05..1b8d1d9 100644 --- a/src/routes/units/[code]/+page.svelte +++ b/src/routes/units/[code]/+page.svelte @@ -5,6 +5,8 @@ import FeedRow from "$lib/components/FeedRow.svelte"; import { postPath } from "$lib/paths"; import { getToken, initAuth, isAuthenticated } from "$lib/stores/auth"; + import Seo from "$lib/components/Seo.svelte"; + import { unitSeo } from "$lib/seo"; import { timeAgo } from "$lib/time"; import type { NoteDoc, QuestionDoc, UnitDoc } from "$lib/types"; @@ -22,6 +24,7 @@ const isPinned = $derived(unit ? pinnedIds.includes(unit._id) : false); const canPin = $derived(isPinned || pinnedIds.length < MAX_PINS); + const seo = $derived.by(() => (unit ? unitSeo(unit) : page.data.seo)); const visibleNotes = $derived.by(() => { const q = searchQuery.trim().toLowerCase(); @@ -98,15 +101,15 @@ } - - {unit?.code ?? "Unit"} — Notebook - +
{#if loading}

Loading

{:else if unit} -

Home · Unit

+

+ Home · Unit +

{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""} @@ -171,7 +174,7 @@ title={note.title} content={note.content} unitCode={unit.code + (unit.code2 ? ` / ${unit.code2}` : "")} - meta="{note.authorName} · {timeAgo( + meta="{timeAgo( note.createdAt, )} · {note.commentCount} comment{note.commentCount === 1 ? '' : 's'}" voteCount={note.voteCount}