v3: the backend is documented for deno doc, one way in every file, and the docs build and publish themselves — comments were in three shapes: file headers as // lines, as an untagged /** */ (which deno doc drops, only an @module block counts) or missing; 43 exports described by a // deno doc can't see, two JSDoc blocks sitting on the wrong declaration and one duplicated; endpoints commented in 5 of 18 route files, three ways, on router.get() calls no doc tool reads. The rule, applied to every non-test module of api/, migrations included: each opens with a /** … @module */ block; every export has JSDoc, and so does every field of an exported interface; a comment describing a declaration is /** */, // only notes code; functions open with a verb, everything else with a noun phrase; parameters are named in the prose, {@link} only for symbols in scope; whatever throws to its caller says so with @throws (for an APIException, the statuses and when); a router's JSDoc lists its endpoints, method and path, who may call it, what it does. deno doc --lint goes from 528 errors to none in api/ (shared/ keeps its 385; it is next). The lint also wanted explicit types on config.ts's settings and a few return types, JsonLdResult exported, and the routers typed Router. deno task doc builds the HTML reference into docs/ from every module of api/ and shared/ but the tests, deno task doc:lint lints it; the index page is the module doc of a root mod.ts (deno.json's exports), which introduces the backend, shared/ and the frontend to come and links every module, and mod.test.ts fails when one is missing. The docs workflow builds it on every push to main that touches them and publishes it to the gh-pages branch, like publish-assistant's. Two defects found along the way are fixed: a private playlist's cover was served to anyone who had its id or slug, and now needs its owner's media token, as private dump files do (playlistImageUrl adds it for private playlists only, so public cover URLs stay the same for everyone); og.ts and static.ts each filled index.html's placeholders, and now share lib/index-html.ts, so the OG middleware picks up a rebuilt index.html instead of keeping the first it read. The README states the documentation rule. 205 backend and 628 frontend tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* The Bandcamp link provider: previews from a release page's OG tags.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { RichContent } from "../../../shared/dump.ts";
|
||||
import type { RichContentProvider } from "./provider.ts";
|
||||
import { extractOgTag } from "../html.ts";
|
||||
@@ -5,6 +11,7 @@ import { fetchWithTimeout, readTextCapped } from "../fetch.ts";
|
||||
|
||||
const BANDCAMP_REGEX = /(?:^|\.)bandcamp\.com/;
|
||||
|
||||
/** The provider for bandcamp.com pages, with the embed player from `og:video`. */
|
||||
export const bandcampProvider: RichContentProvider = {
|
||||
name: "bandcamp",
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* The fallback link provider, for any page no other provider claims.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { RichContent } from "../../../shared/dump.ts";
|
||||
import type { RichContentProvider } from "./provider.ts";
|
||||
import {
|
||||
@@ -14,6 +20,11 @@ import {
|
||||
} from "../html.ts";
|
||||
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.
|
||||
*/
|
||||
export const genericProvider: RichContentProvider = {
|
||||
name: "generic",
|
||||
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
/**
|
||||
* The interface every link provider implements.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { RichContent } from "../../../shared/dump.ts";
|
||||
|
||||
/** Metadata for the links one kind of site serves. */
|
||||
export interface RichContentProvider {
|
||||
/** The provider's name, for logs. */
|
||||
name: string;
|
||||
/** Checks whether this provider handles `url`. */
|
||||
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.
|
||||
*/
|
||||
fetch(url: string): Promise<RichContent>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* The link provider for this site's own pages, read from the database
|
||||
* instead of fetched.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { RichContent } from "../../../shared/dump.ts";
|
||||
import type { RichContentProvider } from "./provider.ts";
|
||||
import { getDump } from "../../services/dump-service.ts";
|
||||
@@ -10,14 +17,21 @@ const DUMP_RE = /^\/dumps\/([^/]+)$/;
|
||||
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.
|
||||
/**
|
||||
* Internal links carry gerbeur's own branding, so an art-less one still gets a
|
||||
* recognizable placeholder instead of a generic tint.
|
||||
*/
|
||||
const SELF_BRANDING = {
|
||||
siteName: "gerbeur",
|
||||
faviconUrl: `${PUBLIC_URL}/apple-touch-icon.png`,
|
||||
accentColor: THEME_COLOR,
|
||||
} 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.
|
||||
*/
|
||||
export const selfProvider: RichContentProvider = {
|
||||
name: "self",
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
/**
|
||||
* The SoundCloud link provider: previews from a track page's OG tags.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { RichContent } from "../../../shared/dump.ts";
|
||||
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. */
|
||||
export const soundcloudProvider: RichContentProvider = {
|
||||
name: "soundcloud",
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
/**
|
||||
* The YouTube link provider: videos, playlists and channels.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { RichContent } from "../../../shared/dump.ts";
|
||||
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. */
|
||||
function extractVideoId(url: string): string | null {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
@@ -25,6 +32,7 @@ function extractVideoId(url: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns the `list` parameter of a URL: the playlist it plays. */
|
||||
function extractPlaylistId(url: string): string | null {
|
||||
try {
|
||||
return new URL(url).searchParams.get("list");
|
||||
@@ -34,7 +42,7 @@ function extractPlaylistId(url: string): string | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the playback offset in seconds from a YouTube URL.
|
||||
* 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.
|
||||
*/
|
||||
@@ -57,7 +65,10 @@ function extractStartSeconds(url: string): number | null {
|
||||
Number(hms[3] ?? 0);
|
||||
}
|
||||
|
||||
/** Matches /channel/UC…, /@handle, /c/name, /user/name */
|
||||
/**
|
||||
* Returns the path of a channel URL: /channel/UC…, /@handle, /c/name or
|
||||
* /user/name.
|
||||
*/
|
||||
function extractChannelPath(url: string): string | null {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
@@ -77,6 +88,10 @@ function extractChannelPath(url: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the title and thumbnail of `url` from YouTube's oEmbed endpoint;
|
||||
* empty when that fails.
|
||||
*/
|
||||
async function fetchOEmbed(
|
||||
url: string,
|
||||
): Promise<{ title?: string; thumbnailUrl?: string }> {
|
||||
@@ -99,6 +114,10 @@ async function fetchOEmbed(
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* The provider for YouTube videos (with their playlist and start time in the embed),
|
||||
* playlists (as a `videoseries` embed) and channels (not embeddable).
|
||||
*/
|
||||
export const youtubeProvider: RichContentProvider = {
|
||||
name: "youtube",
|
||||
|
||||
|
||||
Reference in New Issue
Block a user