v3: comments say what the code can't — the documentation pass wanted JSDoc on every export and every field of an exported interface, which filled the code with restatements ("The dump.", "Whether it is loading.", "The props of X.", "Renders the X.") and module docs that told a reader nothing ("The hot and new feeds, and the props every main feed of the index takes."); some were wrong as well as vague. Every comment in api/, shared/ and src/ is rewritten under one rule, now stated in the README's "Comments" section: every module and export still has a JSDoc, and it gives context (who uses it and where), usage (what a caller must know: a precondition, what it leaves to the caller, a side effect) or its place in the chain (where its input comes from, where its output goes); a field, parameter, private function or local gets one only when it adds to its parent's, and context about a whole type goes on the type; a type's doc is phrased from the value's side ("Returned to Y by X when Z", "Passed to Y by X"); full sentences, as short as they can be, no label-colon-list; // goes wherever the code isn't obvious and says why, and the section banners and narration go. Every claim was checked against the code, which turned up some forty comments that were wrong: DumpCard's claimed it was only used by the hot and new feeds, the WebSocket welcome claimed to be a socket's first message (presence comes first), UpdateUserRequest said null removes a description (it keeps it), deletedDumpIds leaves out that it also holds dumps made private, THEME_COLOR claimed to mirror index.html's, and the like. The routers' JSDoc now says where they are mounted and what becomes of a thrown APIException. doc-lint.ts lets a field without a doc through; a module or export without one still fails. No code changes; 204 backend and 628 frontend tests pass, and doc:lint, deno check, deno lint and tsc are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-29 14:31:14 +00:00
parent 42e0356d21
commit 0d4e344d77
287 changed files with 5679 additions and 5135 deletions

View File

@@ -1,5 +1,6 @@
/**
* The Bandcamp link provider: previews from a release page's OG tags.
* Previews Bandcamp links from their page's OG tags, for
* `rich-content-service.ts`.
*
* @module
*/
@@ -11,7 +12,12 @@ import { fetchWithTimeout, readTextCapped } from "../fetch.ts";
const BANDCAMP_REGEX = /(?:^|\.)bandcamp\.com/;
/** The provider for bandcamp.com pages, with the embed player from `og:video`. */
/**
* Claims bandcamp.com and its subdomains, and `bandcamp.com.example.org` too,
* since the pattern isn't anchored at the end. Takes the embed player's URL
* from the page's `og:video`. The native player fetches the page's streams
* itself at play time, through `GET /api/bandcamp/tracks`.
*/
export const bandcampProvider: RichContentProvider = {
name: "bandcamp",

View File

@@ -1,5 +1,6 @@
/**
* The fallback link provider, for any page no other provider claims.
* Previews any page no other provider claims, for `rich-content-service.ts`,
* from whatever metadata its HTML carries.
*
* @module
*/
@@ -21,15 +22,16 @@ import {
import { fetchWithTimeout, readTextCapped } from "../fetch.ts";
/**
* The provider for any HTML page, previewed from its OG and Twitter tags, JSON-LD, and markup:
* title, description, artwork, icon and brand color. Matches every URL, so it
* must come last.
* Matches every URL, so it comes last in the service's list. Each field falls
* back from OG tags to Twitter tags, JSON-LD and the markup itself. The icon
* and brand color feed the placeholder the frontend draws when there is no
* thumbnail.
*/
export const genericProvider: RichContentProvider = {
name: "generic",
matches(_url: string): boolean {
return true; // fallback — always matches
return true;
},
async fetch(url: string): Promise<RichContent> {
@@ -44,9 +46,9 @@ export const genericProvider: RichContentProvider = {
const html = await readTextCapped(res);
const ld = extractJsonLd(html);
// If og:url is present but points to a different page (e.g. the homepage),
// the og: block is a site-level fallback, not page-specific metadata.
// In that case skip og:title and og:image so page-level signals win.
// An og:url naming another page (often the home page) means the OG tags
// are the site's defaults, not this page's: its title and image are
// skipped.
const ogUrl = extractOgTag(html, "url");
const useOg = !ogUrl || (() => {
try {
@@ -58,36 +60,29 @@ export const genericProvider: RichContentProvider = {
}
})();
// Title: og:title (page-matched) → twitter:title → JSON-LD → <title>
const title = (useOg ? extractOgTag(html, "title") : undefined) ??
extractMetaName(html, "twitter:title") ??
ld.title ??
extractPageTitle(html);
// Site name: og:site_name → hostname
const siteName = extractOgTag(html, "site_name") ??
new URL(url).hostname.replace(/^www\./, "");
// Description: og:description → twitter:description → JSON-LD → <meta name="description">
const description = extractOgTag(html, "description") ??
extractMetaName(html, "twitter:description") ??
ld.description ??
extractMetaName(html, "description");
// Image: og:image (page-matched) → twitter:image → JSON-LD → large <img> →
// first content <img>. The chain deliberately stops there: a favicon is not
// artwork, and pretending otherwise means every art-less page gets a 16×16
// icon cover-cropped into a 128×72 box. No match here means "no artwork",
// and the frontend draws a generated placeholder instead.
// Never the favicon: a 16×16 icon cropped into a thumbnail looks broken,
// and the placeholder drawn instead shows the icon properly.
const thumbnailUrl = (useOg ? extractOgTag(html, "image") : undefined) ??
extractMetaName(html, "twitter:image") ??
ld.thumbnailUrl ??
extractLargeImage(html, url) ??
extractFirstContentImage(html, url);
// Icon and brand color are independent facts about the page, so they're
// collected whether or not there's artwork. `/favicon.ico` is a guess; when
// it 404s the placeholder falls back to the site's initial.
// `/favicon.ico` is a guess. When it doesn't load, the placeholder shows
// the site's initial.
const faviconUrl = extractBestIcon(html, url) ??
`${new URL(url).origin}/favicon.ico`;

View File

@@ -1,20 +1,25 @@
/**
* The interface every link provider implements.
* Declares what a link provider implements. `rich-content-service.ts` asks its
* providers in turn and fetches a link's preview from the first that matches.
*
* @module
*/
import type { RichContent } from "../../../shared/dump.ts";
/** Metadata for the links one kind of site serves. */
/**
* Implemented by each file of `remote/providers/`, and listed in priority order
* by `rich-content-service.ts`.
*/
export interface RichContentProvider {
/** The provider's name, for logs. */
name: string;
/** Checks whether this provider handles `url`. */
/** Called on a URL that `isValidHttpUrl` accepted; never throws. */
matches(url: string): boolean;
/**
* Fetches `url`'s preview. Rejects when the page can't be fetched; a page
* that isn't HTML yields a preview with no metadata.
* Returns the preview of `url`, which ends up in the dump's `rich_content` or
* the create form's preview. It rejects when the page can't be fetched, and
* the service then falls back to a stub. A page that isn't HTML yields a
* preview without metadata.
*/
fetch(url: string): Promise<RichContent>;
}

View File

@@ -1,6 +1,7 @@
/**
* The link provider for this site's own pages, read from the database
* instead of fetched.
* Previews links to this site's own dump, user and playlist pages from the
* database, for `rich-content-service.ts`. No request goes out, so this works
* even when the site runs on a private host that `fetchWithTimeout` refuses.
*
* @module
*/
@@ -18,8 +19,8 @@ const USER_RE = /^\/users\/([^/]+)$/;
const PLAYLIST_RE = /^\/playlists\/([^/]+)$/;
/**
* Internal links carry gerbeur's own branding, so an art-less one still gets a
* recognizable placeholder instead of a generic tint.
* Gives a link to this site without a thumbnail a placeholder in the site's
* icon and color.
*/
const SELF_BRANDING = {
siteName: "gerbeur",
@@ -28,9 +29,9 @@ const SELF_BRANDING = {
} as const;
/**
* The provider for links to this site's dump, user and playlist pages, read
* from the database. Its `fetch` rejects with the services' `APIException` (404) for a
* page that doesn't exist or is private.
* Comes before the generic provider, and claims what `isSelfUrl` accepts. Its
* `fetch` rejects with the services' 404 `APIException` for a missing page or
* a private dump. A private playlist is previewed all the same.
*/
export const selfProvider: RichContentProvider = {
name: "self",
@@ -97,7 +98,7 @@ export const selfProvider: RichContentProvider = {
});
}
// Should not reach here if matches() is correct
// Unreachable: isSelfUrl accepts only the three paths above.
return Promise.resolve({ type: "generic", url });
},
};

View File

@@ -1,5 +1,6 @@
/**
* The SoundCloud link provider: previews from a track page's OG tags.
* Previews SoundCloud links from their page's OG tags, for
* `rich-content-service.ts`.
*
* @module
*/
@@ -9,7 +10,10 @@ import type { RichContentProvider } from "./provider.ts";
import { extractOgTag } from "../html.ts";
import { fetchWithTimeout, readTextCapped } from "../fetch.ts";
/** The provider for soundcloud.com pages, with SoundCloud's embed player. */
/**
* Claims soundcloud.com only, not www. or m.soundcloud.com, and builds the
* embed player's URL from the link itself.
*/
export const soundcloudProvider: RichContentProvider = {
name: "soundcloud",

View File

@@ -1,5 +1,7 @@
/**
* The YouTube link provider: videos, playlists and channels.
* Previews YouTube videos, playlists and channels, for
* `rich-content-service.ts`. Videos and playlists are read from YouTube's
* oEmbed endpoint, channels from their page's OG tags.
*
* @module
*/
@@ -9,7 +11,10 @@ import type { RichContentProvider } from "./provider.ts";
import { extractOgTag } from "../html.ts";
import { fetchWithTimeout, readTextCapped } from "../fetch.ts";
/** Returns the id of the video a youtu.be or youtube.com URL points at. */
/**
* Returns the id of the video a URL points at, on youtu.be, youtube.com or
* www.youtube.com only; `m.` and `music.` links go to the generic provider.
*/
function extractVideoId(url: string): string | null {
try {
const u = new URL(url);
@@ -32,7 +37,10 @@ function extractVideoId(url: string): string | null {
return null;
}
/** Returns the `list` parameter of a URL: the playlist it plays. */
/**
* Returns the `list` parameter of a URL, whatever its host: a link to another
* site with a `list` parameter is taken for a YouTube playlist.
*/
function extractPlaylistId(url: string): string | null {
try {
return new URL(url).searchParams.get("list");
@@ -42,9 +50,8 @@ function extractPlaylistId(url: string): string | null {
}
/**
* Returns the playback offset in seconds of a YouTube URL.
* Accepts `t` (plain seconds, `90s`, or `1h2m3s` / `20m5s` forms) and `start`
* (plain seconds). Returns null when absent or unparseable.
* Returns the start time of a YouTube URL in seconds, from `t` (`90`, `90s`,
* `1h2m3s`) or `start`; null when there is none that parses.
*/
function extractStartSeconds(url: string): number | null {
let raw: string | null;
@@ -66,8 +73,8 @@ function extractStartSeconds(url: string): number | null {
}
/**
* Returns the path of a channel URL: /channel/UC…, /@handle, /c/name or
* /user/name.
* Returns the path of a youtube.com channel URL (`/channel/UC…`, `/@handle`,
* `/c/name` or `/user/name`); null for any other URL.
*/
function extractChannelPath(url: string): string | null {
try {
@@ -90,7 +97,7 @@ function extractChannelPath(url: string): string | null {
/**
* Fetches the title and thumbnail of `url` from YouTube's oEmbed endpoint;
* empty when that fails.
* empty when that fails, since the preview can do without them.
*/
async function fetchOEmbed(
url: string,
@@ -109,14 +116,16 @@ async function fetchOEmbed(
}
await res.body?.cancel();
} catch {
// oembed failed — carry on
// YouTube unreachable, or its answer not JSON.
}
return {};
}
/**
* The provider for YouTube videos (with their playlist and start time in the embed),
* playlists (as a `videoseries` embed) and channels (not embeddable).
* Comes first in the service's list. A video's embed keeps the link's playlist
* and start time, a playlist embeds as a `videoseries`, and a channel gets no
* embed. Its `fetch` never rejects: whatever YouTube doesn't answer is left
* out.
*/
export const youtubeProvider: RichContentProvider = {
name: "youtube",
@@ -134,9 +143,8 @@ export const youtubeProvider: RichContentProvider = {
const listId = extractPlaylistId(url);
const channelPath = extractChannelPath(url);
// ── Channel ───────────────────────────────────────────────────────────────
if (channelPath && !videoId) {
// oEmbed doesn't support channel URLs — scrape og:image from the page instead
// oEmbed doesn't take channel URLs.
let title: string | undefined;
let thumbnailUrl: string | undefined;
try {
@@ -149,7 +157,7 @@ export const youtubeProvider: RichContentProvider = {
await res.body?.cancel();
}
} catch {
// scrape failed — carry on with undefined values
// YouTube unreachable: the preview goes without.
}
return {
type: "youtube",
@@ -157,11 +165,9 @@ export const youtubeProvider: RichContentProvider = {
url,
title,
thumbnailUrl,
// channels are not embeddable as a player
};
}
// ── Playlist (no specific video) ──────────────────────────────────────────
if (listId && !videoId) {
const { title, thumbnailUrl } = await fetchOEmbed(url);
return {
@@ -175,7 +181,7 @@ export const youtubeProvider: RichContentProvider = {
};
}
// ── Video (with optional playlist context) ────────────────────────────────
// Built, not taken from oEmbed, so it is there even when oEmbed fails.
const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
const { title } = await fetchOEmbed(url);