Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,20 @@ export default defineConfig({
integrations: [
starlight({
title: 'Coral Docs',
description: 'Documentation for the Coral ecosystem of Jellyfin modules',
description:
"Install, configure and operate Coral's open-source Jellyfin modules. Each one runs as a Docker container and does one thing well.",
customCss: ['./src/styles.css'],
// Adds the JSON-LD graph, the social card and non-blocking fonts that
// Starlight does not ship. See src/components/Head.astro.
components: {
Head: './src/components/Head.astro',
},
// Freshness signal. The docs drifted a long way from the code once
// already; showing the date makes the next drift visible.
lastUpdated: true,
editLink: {
baseUrl: 'https://github.com/Get-Coral/docs/edit/main/',
},
expressiveCode: {
themes: ['github-dark'],
useStarlightDarkModeSwitch: false,
Expand Down
Binary file added public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "Coral Docs",
"short_name": "Coral Docs",
"description": "Install, configure and operate Coral's open-source Jellyfin modules.",
"start_url": "/",
"display": "standalone",
"background_color": "#090f22",
"theme_color": "#090f22",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml" }
]
}
Binary file removed src/assets/houston.webp
Binary file not shown.
101 changes: 101 additions & 0 deletions src/components/Head.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
/**
* Starlight's own Head, plus the things it does not ship: a social card, a
* JSON-LD graph, and non-render-blocking fonts.
*
* Before this existed the site emitted `twitter:card=summary_large_image` with
* no image behind it, so every share rendered blank, and no structured data at
* all while the marketing site next door published a full graph.
*/
import Default from "@astrojs/starlight/components/Head.astro";
import { moduleForDocId } from "../lib/modules";
import {
breadcrumbNode,
graph,
organizationNode,
softwareApplicationNode,
techArticleNode,
webSiteNode,
} from "../lib/schema";
import { absoluteUrl, SITE_NAME, SITE_URL } from "../lib/site";

const route = Astro.locals.starlightRoute;
const { entry, id, lastUpdated } = route;

const pageUrl = new URL(Astro.url.pathname, SITE_URL).toString();
const ogImage = absoluteUrl(`${id ? `/${id}` : ""}/og.png`);

const module = moduleForDocId(id);
const title = entry.data.title;
const description = entry.data.description ?? "";

/** "modules/aurora" -> Docs -> Modules -> Aurora. */
const SECTIONS: Record<string, string> = {
"getting-started": "Getting started",
modules: "Modules",
libraries: "Libraries",
contributing: "Contributing",
};

// Starlight serves a 404 at a real URL. It should not be indexed, and it
// should not claim to be an article about anything.
const is404 = id === "404";

// The splash page's collection id is "index", not "".
const isHome = id === "" || id === "index";
const section = isHome ? "" : id.split("/")[0];
const trail = [{ name: SITE_NAME, url: `${SITE_URL}/` }];
if (section && SECTIONS[section]) {
// There are no section landing pages, so this crumb points at the current
// page rather than at a URL that would 404.
trail.push({ name: SECTIONS[section], url: pageUrl });
}
if (!isHome) trail.push({ name: title, url: pageUrl });

const breadcrumb = trail.length > 1 ? breadcrumbNode(pageUrl, trail) : undefined;

const nodes = [
organizationNode(),
webSiteNode(),
techArticleNode({
url: pageUrl,
name: title,
description,
image: ogImage,
breadcrumbId: breadcrumb ? `${pageUrl}#breadcrumb` : undefined,
dateModified: lastUpdated?.toISOString(),
about: module ? `${pageUrl}#software` : undefined,
}),
];
if (breadcrumb) nodes.push(breadcrumb);
if (module) nodes.push(softwareApplicationNode(module));

const jsonLd = JSON.stringify(graph(nodes));
---

{is404 && <meta name="robots" content="noindex, follow" />}

<Default><slot /></Default>

{/* Fonts. These used to be an @import at the top of styles.css, which blocks
render until the remote stylesheet is fetched and parsed. */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&display=swap"
/>

<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#090f22" />

<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content={`${title} — ${SITE_NAME}`} />
<meta name="twitter:image" content={ogImage} />

<link rel="alternate" type="text/plain" href={`${SITE_URL}/llms.txt`} title="llms.txt" />

{!is404 && <script type="application/ld+json" is:inline set:html={jsonLd} />}
155 changes: 155 additions & 0 deletions src/lib/modules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { DOCKER_HUB_NAMESPACE, GITHUB_ORG_URL, MARKETING_URL } from "./site";

/**
* Operational facts about each module, verified against the source repos.
*
* `status` is the honest one. It exists because the docs previously described
* Encore as a working music-request app while its `src/` was still the
* unmodified template, and described Librarian features that had no code at
* all. A reader — human or model — needs to be able to tell Aurora from Encore
* without cloning both.
*
* shipping Feature-complete enough to run for its stated purpose.
* early Runs and does something useful, but the surface is small and moving.
* scaffold Published image exists, but it serves the template placeholder.
*
* Nothing here is aspirational. If a module gains a feature, the repo changes
* first and this file follows.
*/
export type ModuleStatus = "shipping" | "early" | "scaffold";

export interface CoralModule {
slug: string;
name: string;
/** Repo name on GitHub, where it differs from the slug. */
repo: string;
/** One self-contained sentence. Lifted whole by extractors, so it stands alone. */
summary: string;
status: ModuleStatus;
statusNote: string;
/** Docker Hub image. Every one of these was confirmed to exist on the registry. */
image: string;
/** Port inside the container. Every module is EXPOSE 3000. */
port: number;
accent: "teal" | "coral";
}

export const modules: CoralModule[] = [
{
slug: "aurora",
name: "Aurora",
repo: "aurora",
summary:
"Aurora is a cinematic web frontend for Jellyfin. It runs as one Docker container, reads your libraries over the Jellyfin API, and reports playback progress back to Jellyfin.",
status: "shipping",
statusNote:
"The most complete module. Rails, detail pages, search, playback reporting, TV mode, EN/NL translations, and PWA and Capacitor wrappers.",
image: "aurora",
port: 3000,
accent: "coral",
},
{
slug: "fathom",
name: "Fathom",
repo: "fathom",
summary:
"Fathom is a reading interface for books, manga, comics and PDFs that already live in Jellyfin. It runs as one Docker container and presents your reading libraries cover-first.",
status: "early",
statusNote:
"Browsing works: featured shelf, recent additions, libraries, collections, and a title detail view. There is no reading-progress tracking, rating or recommendation engine yet.",
image: "fathom",
port: 3000,
accent: "teal",
},
{
slug: "librarian",
name: "Librarian",
repo: "librarian",
summary:
"Librarian imports finished downloads into your media tree by hardlinking them, so the file appears in your library at zero extra bytes and the torrent keeps seeding.",
status: "early",
statusNote:
"The import workflow is real and in use: roots, import plans, hardlink-with-copy-fallback, path mappings and scan jobs. Duplicate detection, bulk metadata editing and analytics are not built.",
image: "librarian",
port: 3000,
accent: "teal",
},
{
slug: "kapow",
name: "KAPOW!",
repo: "KAPOW",
summary:
"KAPOW! is a karaoke queue system for bars, events and parties. The host opens a room, guests join from their own phones, everyone votes songs up the queue, and a separate display view drives the screen.",
status: "shipping",
statusNote:
"Rooms, guest join, search, voting, host controls and the display view all work. Unlike every other module it needs Supabase and a YouTube Data API key, so it is not self-contained.",
image: "kapow",
port: 3000,
accent: "coral",
},
{
slug: "marquee",
name: "Marquee",
repo: "marquee",
summary:
"Marquee turns a spare TV, tablet or wall panel into an always-on display showing what is playing now and what was recently added to Jellyfin. There is nothing to click.",
status: "early",
statusNote:
"The display and its first-run setup flow work. The surface is deliberately small — it is a screen, not an app.",
image: "marquee",
port: 3000,
accent: "teal",
},
{
slug: "encore",
name: "Encore",
repo: "encore",
summary:
"Encore is the reference Coral module scaffold. The published image currently serves a placeholder page; the moderated guest music requests it is named for are not built yet.",
status: "scaffold",
statusNote:
"`src/` is the unmodified Coral template. Useful as a starting point for a new module, not as something to deploy.",
image: "encore",
port: 3000,
accent: "coral",
},
{
slug: "tide",
name: "Tide",
repo: "tide",
summary:
"Tide is a torrent client with a web interface, built for self-hosted download boxes. It enforces real active-download and seeding limits and pauses torrents as it approaches its memory cap.",
status: "shipping",
statusNote:
"Queue controls, per-file piece priorities, swarm visibility, SQLite-backed state, a cgroup-aware memory guard, and optional Jellyfin sign-in.",
image: "tide",
port: 3000,
accent: "teal",
},
];

export const moduleBySlug = (slug: string) =>
modules.find((entry) => entry.slug === slug);

/** Matches "modules/aurora" — the Starlight page id — back to its metadata. */
export const moduleForDocId = (id: string) =>
id.startsWith("modules/") ? moduleBySlug(id.slice("modules/".length)) : undefined;

export const repoUrl = (module: CoralModule) =>
`${GITHUB_ORG_URL}/${module.repo}`;

export const imageRef = (module: CoralModule) =>
`${DOCKER_HUB_NAMESPACE}/${module.image}`;

export const containerUrl = (module: CoralModule) =>
`https://hub.docker.com/r/${DOCKER_HUB_NAMESPACE}/${module.image}`;

/** The marketing page owns positioning; the docs page owns operation. */
export const marketingUrl = (module: CoralModule) =>
`${MARKETING_URL}/apps/${module.slug}`;

export const STATUS_LABEL: Record<ModuleStatus, string> = {
shipping: "Shipping",
early: "Early",
scaffold: "Scaffold",
};
Loading