mirror of
https://github.com/dsec-hub/dsec-notebook.git
synced 2026-09-22 07:24:27 +00:00
commit
8028650ade
16 changed files with 178 additions and 16 deletions
4
src/app.d.ts
vendored
4
src/app.d.ts
vendored
|
|
@ -4,7 +4,9 @@ export {};
|
|||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface PageData {}
|
||||
interface PageData {
|
||||
seo?: import("$lib/seo").PageSeo;
|
||||
}
|
||||
interface PageState {}
|
||||
interface Platform {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A resource hub for Deakin University students. Shared study notes, questions and answers for SIT and Mathematics units."
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="Notebook" />
|
||||
|
|
|
|||
14
src/lib/components/Seo.svelte
Normal file
14
src/lib/components/Seo.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import type { PageSeo } from "$lib/seo";
|
||||
|
||||
let { seo }: { seo: PageSeo | undefined } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if seo}
|
||||
<title>{seo.title}</title>
|
||||
<meta name="description" content={seo.description} />
|
||||
<meta property="og:title" content={seo.title} />
|
||||
<meta property="og:description" content={seo.description} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
29
src/lib/seo.spec.ts
Normal file
29
src/lib/seo.spec.ts
Normal file
|
|
@ -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.");
|
||||
});
|
||||
});
|
||||
59
src/lib/seo.ts
Normal file
59
src/lib/seo.ts
Normal file
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
25
src/lib/server/pageSeo.ts
Normal file
25
src/lib/server/pageSeo.ts
Normal file
|
|
@ -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) };
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import Navbar from "$lib/components/Navbar.svelte";
|
||||
import { SITE_DESCRIPTION } from "$lib/seo";
|
||||
import { initAuth } from "$lib/stores/auth";
|
||||
import { onMount } from "svelte";
|
||||
import "./layout.css";
|
||||
|
|
@ -24,6 +26,9 @@
|
|||
<svelte:head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
{#if !page.data.seo}
|
||||
<meta name="description" content={SITE_DESCRIPTION} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div class="bg-surface flex min-h-screen flex-col">
|
||||
|
|
|
|||
4
src/routes/[code]/+page.server.ts
Normal file
4
src/routes/[code]/+page.server.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { loadUnitSeo } from "$lib/server/pageSeo";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = ({ params }) => loadUnitSeo(params.code);
|
||||
4
src/routes/[code]/[id]/+page.server.ts
Normal file
4
src/routes/[code]/[id]/+page.server.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { loadPostSeo } from "$lib/server/pageSeo";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = ({ params }) => loadPostSeo(params.id, params.code);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import { query } from "$lib/api";
|
||||
import Seo from "$lib/components/Seo.svelte";
|
||||
import NotePage from "../../notes/[id]/+page.svelte";
|
||||
import QuestionPage from "../../questions/[id]/+page.svelte";
|
||||
|
||||
|
|
@ -35,6 +36,10 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
{#if loading || !postType}
|
||||
<Seo seo={page.data.seo} />
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="page">
|
||||
<p class="kicker py-16">Loading</p>
|
||||
|
|
|
|||
4
src/routes/notes/[id]/+page.server.ts
Normal file
4
src/routes/notes/[id]/+page.server.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { loadPostSeo } from "$lib/server/pageSeo";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = ({ params }) => loadPostSeo(params.id);
|
||||
|
|
@ -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 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{note?.title ?? "Loading..."} — Notebook</title>
|
||||
</svelte:head>
|
||||
<Seo {seo} />
|
||||
|
||||
<div class="page">
|
||||
{#if loading}
|
||||
|
|
|
|||
4
src/routes/questions/[id]/+page.server.ts
Normal file
4
src/routes/questions/[id]/+page.server.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { loadPostSeo } from "$lib/server/pageSeo";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = ({ params }) => loadPostSeo(params.id);
|
||||
|
|
@ -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 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{question?.title ?? "Loading..."} — Notebook</title>
|
||||
</svelte:head>
|
||||
<Seo {seo} />
|
||||
|
||||
<div class="page">
|
||||
{#if loading}
|
||||
|
|
|
|||
4
src/routes/units/[code]/+page.server.ts
Normal file
4
src/routes/units/[code]/+page.server.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { loadUnitSeo } from "$lib/server/pageSeo";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = ({ params }) => loadUnitSeo(params.code);
|
||||
|
|
@ -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 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{unit?.code ?? "Unit"} — Notebook</title>
|
||||
</svelte:head>
|
||||
<Seo {seo} />
|
||||
|
||||
<div class="page">
|
||||
{#if loading}
|
||||
<p class="kicker py-16">Loading</p>
|
||||
{:else if unit}
|
||||
<p class="kicker"><a href="/" class="hover:text-primary">Home</a> · Unit</p>
|
||||
<p class="kicker">
|
||||
<a href="/" class="hover:text-primary">Home</a> · Unit
|
||||
</p>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<h1 class="text-ink font-serif text-4xl font-medium">
|
||||
{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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue