Add cache headers, real 404s, social meta, JSON-LD, robots and sitemap
This commit is contained in:
parent
2041f6b899
commit
fc7c749856
19 changed files with 258 additions and 47 deletions
|
|
@ -11,6 +11,10 @@
|
|||
type Author,
|
||||
} from "$lib/components/ThreadItem.svelte";
|
||||
import SortToggle from "$lib/components/SortToggle.svelte";
|
||||
import Meta from "$lib/components/Meta.svelte";
|
||||
import { groupStore } from "$lib/group.svelte";
|
||||
import { groupsStore } from "$lib/groups.svelte";
|
||||
import { MODE } from "$lib/config";
|
||||
import type { NostrUser } from "@nostr/gadgets/metadata";
|
||||
import { auth, openLogin } from "$lib/auth.svelte";
|
||||
import { openDraft } from "$lib/draft.svelte";
|
||||
|
|
@ -45,6 +49,12 @@
|
|||
if (urlSort) sortPref.value = urlSort;
|
||||
});
|
||||
|
||||
const about = $derived(
|
||||
MODE === "full"
|
||||
? (groupsStore.list.find((g) => g.id === groupId)?.about ?? "")
|
||||
: (groupStore.data?.about ?? ""),
|
||||
);
|
||||
|
||||
const flags = $derived(getGroupFlags(groupId));
|
||||
const member = $derived(membershipOf(groupId));
|
||||
|
||||
|
|
@ -123,9 +133,7 @@
|
|||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
</svelte:head>
|
||||
<Meta {title} description={about} />
|
||||
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 py-2">
|
||||
|
|
|
|||
57
src/lib/components/Meta.svelte
Normal file
57
src/lib/components/Meta.svelte
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import { TITLE } from "$lib/config";
|
||||
import { groupStore } from "$lib/group.svelte";
|
||||
import { GROUP_ID, MODE } from "$lib/config";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: string;
|
||||
type?: "website" | "article";
|
||||
image?: string;
|
||||
jsonLd?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
let {
|
||||
title,
|
||||
description = "",
|
||||
type = "website",
|
||||
image,
|
||||
jsonLd,
|
||||
}: Props = $props();
|
||||
|
||||
// Escape "<" so the JSON can never close the script tag early.
|
||||
const ld = $derived(
|
||||
jsonLd ? JSON.stringify(jsonLd).replace(/</g, "\\u003c") : "",
|
||||
);
|
||||
|
||||
const siteName = $derived(
|
||||
TITLE || (MODE === "simple" ? (groupStore.data?.name ?? GROUP_ID) : ""),
|
||||
);
|
||||
const canonical = $derived(page.url.origin + page.url.pathname);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
<link rel="canonical" href={canonical} />
|
||||
{#if description}
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:description" content={description} />
|
||||
{/if}
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
{#if siteName}
|
||||
<meta property="og:site_name" content={siteName} />
|
||||
{/if}
|
||||
{#if image}
|
||||
<meta property="og:image" content={image} />
|
||||
{/if}
|
||||
<meta
|
||||
name="twitter:card"
|
||||
content={image ? "summary_large_image" : "summary"}
|
||||
/>
|
||||
{#if ld}
|
||||
{@html `<script type="application/ld+json">${ld}</script>`}
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
|
@ -21,6 +21,11 @@ if (MODE === "simple" && !GROUP_ID) {
|
|||
// Server-side rendering: opt-in, and only meaningful when the build targets the
|
||||
// Node adapter (svelte.config.js reads the same variable).
|
||||
export const SSR_ENABLED = env.PUBLIC_SSR === "yes";
|
||||
// Server-rendered pages and snapshots are anonymous, so a shared cache may
|
||||
// hold them: fresh for 5 minutes, served stale for an hour while revalidating.
|
||||
// Browsers always revalidate (max-age=0) so a login shows its content at once.
|
||||
export const CACHE_CONTROL =
|
||||
"public, max-age=0, s-maxage=300, stale-while-revalidate=3600";
|
||||
// Requires a relay with NIP-50 support.
|
||||
export const SEARCH_ENABLED = env.PUBLIC_SEARCH === "yes";
|
||||
export const LABELS = (env.PUBLIC_LABELS ?? "")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { browser } from "$app/environment";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import type { NostrUser } from "$lib/gadgets";
|
||||
import type { GroupMetadata, GroupSummary } from "./groups";
|
||||
import type { Resource } from "./resources";
|
||||
|
|
@ -37,14 +38,22 @@ type Fetch = typeof fetch;
|
|||
// HTML; in the browser it is consumed only while hydrating that HTML. Client
|
||||
// navigations get null, leaving the stores to fetch live (and authenticated)
|
||||
// data as they always did — the same path a client-only build takes.
|
||||
// A 404 from the endpoint becomes a real 404 page on the server, so crawlers
|
||||
// never index an empty shell; the client keeps its own not-found handling.
|
||||
export async function snapshot<T>(
|
||||
fetch: Fetch,
|
||||
url: string,
|
||||
): Promise<T | null> {
|
||||
if (browser && !hasInlinedResponse(url)) return null;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (res.status === 404 && !browser) error(404, "Not found");
|
||||
if (!res.ok) return null;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return null;
|
||||
return (await res.json()) as T;
|
||||
} catch {
|
||||
return null;
|
||||
|
|
|
|||
17
src/lib/seo.ts
Normal file
17
src/lib/seo.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Plain-text excerpt of a markdown body for description meta tags: the first
|
||||
// paragraph that isn't a heading or a bare media URL, trimmed to `max` chars.
|
||||
export function excerpt(markdown: string, max = 160): string {
|
||||
const lines = markdown.split("\n").map((l) => l.trim());
|
||||
const line = lines.find(
|
||||
(l) => l && !l.startsWith("#") && !/^https?:\/\/\S+$/.test(l),
|
||||
);
|
||||
if (!line) return "";
|
||||
const text = line
|
||||
.replace(/!\[[^\]]*\]\([^)]*\)/g, "") // images
|
||||
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // links → label
|
||||
.replace(/[*_`~]+/g, "")
|
||||
.replace(/nostr:([a-z0-9]{12})[a-z0-9]*/g, "@$1…") // keep a short handle
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return text.length > max ? text.slice(0, max - 1).trimEnd() + "…" : text;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue