refactor: replace Biome with oxlint + oxfmt

Migrate the entire monorepo from Biome 2.4.7 to oxlint 1.56.0 (linter)
and oxfmt 0.41.0 (formatter) for faster lint/format and broader rule
coverage.

- Add `.oxlintrc.json` with React, TypeScript, unicorn, import plugins
  and correctness/suspicious categories
- Add `.oxfmtrc.json` with 2-space indent, import sorting, and Tailwind
  class sorting (all 30+ custom className attributes migrated)
- Add `docs/.oxlintrc.json` and `docs/.oxfmtrc.json` with Next.js plugin
- Update all 12 workspace package.json scripts: `oxlint`, `oxfmt`,
  `oxfmt --check`
- Add `format:check` turbo task and CI step
- Update VS Code settings/extensions to use `oxc.oxc-vscode`
- Update CI path triggers from `biome.json` to new config files
- Remove all `biome-ignore` comments and fix shadowed variables
- Delete `biome.json` and `docs/biome.json`
- Reformat entire codebase with oxfmt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 13:26:50 -04:00
co-authored by Claude Opus 4.6
parent a80c8c9a0c
commit 2b0c683b7b
359 changed files with 3894 additions and 7208 deletions
+5 -5
View File
@@ -1,5 +1,6 @@
import { HomeLayout } from "fumadocs-ui/layouts/home";
import Link from "next/link";
import { TmdbLogo } from "@/components/tmdb-logo";
import { baseOptions } from "@/lib/layout.shared";
@@ -7,8 +8,8 @@ export default function Layout({ children }: LayoutProps<"/">) {
return (
<HomeLayout {...baseOptions()}>
{children}
<footer className="border-t border-fd-border px-6 py-8">
<div className="mx-auto flex max-w-5xl flex-col items-center gap-4 text-center text-xs text-fd-muted-foreground">
<footer className="border-fd-border border-t px-6 py-8">
<div className="text-fd-muted-foreground mx-auto flex max-w-5xl flex-col items-center gap-4 text-center text-xs">
<div className="flex flex-wrap items-center justify-center gap-x-2 gap-y-1">
<a
href="https://github.com/jakejarvis/sofa"
@@ -36,9 +37,8 @@ export default function Layout({ children }: LayoutProps<"/">) {
>
<TmdbLogo className="h-3" />
</a>
<p className="max-w-md text-fd-muted-foreground/80">
This product uses the TMDB API but is not endorsed or certified by
TMDB.
<p className="text-fd-muted-foreground/80 max-w-md">
This product uses the TMDB API but is not endorsed or certified by TMDB.
</p>
</div>
</div>
+26 -30
View File
@@ -9,6 +9,7 @@ import {
Smartphone,
} from "lucide-react";
import Link from "next/link";
import { ScrollIndicator } from "@/components/scroll-indicator";
import { SofaLogo } from "@/components/sofa-logo";
@@ -21,8 +22,7 @@ const features = [
},
{
title: "Media Server Sync",
description:
"Automatically log watches from Plex, Jellyfin, or Emby via webhooks.",
description: "Automatically log watches from Plex, Jellyfin, or Emby via webhooks.",
icon: MonitorPlay,
},
{
@@ -44,8 +44,7 @@ const features = [
},
{
title: "One Container",
description:
"Deploy with a single Docker image. No external database, no sidecar services.",
description: "Deploy with a single Docker image. No external database, no sidecar services.",
icon: Container,
},
];
@@ -55,16 +54,16 @@ const steps = [
step: "1",
title: "Get a TMDB API token",
content: (
<p className="text-sm text-fd-muted-foreground">
<p className="text-fd-muted-foreground text-sm">
Create a free account at{" "}
<a
href="https://www.themoviedb.org/settings/api"
target="_blank"
rel="noopener noreferrer"
className="text-fd-primary underline underline-offset-4 hover:text-fd-primary/80"
className="text-fd-primary hover:text-fd-primary/80 underline underline-offset-4"
>
themoviedb.org
<ExternalLinkIcon className="size-3 inline-block translate-y-[-2px] text-fd-muted-foreground ml-1" />
<ExternalLinkIcon className="text-fd-muted-foreground ml-1 inline-block size-3 translate-y-[-2px]" />
</a>
</p>
),
@@ -73,7 +72,7 @@ const steps = [
step: "2",
title: "Set three environment variables",
content: (
<pre className="overflow-x-auto mt-2 rounded-lg bg-fd-background/80 p-3 text-xs text-fd-muted-foreground ring-1 ring-white/[0.06]">
<pre className="bg-fd-background/80 text-fd-muted-foreground mt-2 overflow-x-auto rounded-lg p-3 text-xs ring-1 ring-white/[0.06]">
<code>{`TMDB_API_READ_ACCESS_TOKEN=...
BETTER_AUTH_SECRET=...
BETTER_AUTH_URL=https://sofa.example.com`}</code>
@@ -84,7 +83,7 @@ BETTER_AUTH_URL=https://sofa.example.com`}</code>
step: "3",
title: "Start the container",
content: (
<pre className="overflow-x-auto mt-2 rounded-lg bg-fd-background/80 p-3 text-xs text-fd-muted-foreground ring-1 ring-white/[0.06]">
<pre className="bg-fd-background/80 text-fd-muted-foreground mt-2 overflow-x-auto rounded-lg p-3 text-xs ring-1 ring-white/[0.06]">
<code>docker compose up -d</code>
</pre>
),
@@ -99,10 +98,10 @@ export default function HomePage() {
<div className="pointer-events-none absolute top-[60vh] -right-24 h-[400px] w-[400px] rounded-full bg-[oklch(0.8_0.14_65)] opacity-[0.04] blur-[100px]" />
{/* ── Hero ── */}
<section className="relative flex min-h-[calc(100vh-3.5rem)] flex-col items-center justify-center px-6 pb-16 pt-8 text-center">
<section className="relative flex min-h-[calc(100vh-3.5rem)] flex-col items-center justify-center px-6 pt-8 pb-16 text-center">
<div className="relative flex max-w-2xl flex-col items-center gap-8">
<p
className="animate-stagger-item text-xs font-medium tracking-[0.3em] text-fd-primary uppercase"
className="animate-stagger-item text-fd-primary text-xs font-medium tracking-[0.3em] uppercase"
style={{ "--stagger-index": 0 } as React.CSSProperties}
>
Self-hosted movie & TV tracker
@@ -112,7 +111,7 @@ export default function HomePage() {
className="animate-stagger-item"
style={{ "--stagger-index": 1 } as React.CSSProperties}
>
<SofaLogo className="size-20 text-fd-primary md:size-28" />
<SofaLogo className="text-fd-primary size-20 md:size-28" />
</div>
<h1
@@ -123,11 +122,10 @@ export default function HomePage() {
</h1>
<p
className="animate-stagger-item max-w-md text-base leading-relaxed text-fd-muted-foreground md:text-lg"
className="animate-stagger-item text-fd-muted-foreground max-w-md text-base leading-relaxed md:text-lg"
style={{ "--stagger-index": 3 } as React.CSSProperties}
>
Sofa lives on your own server and seamlessly plugs into your
existing home media stack.
Sofa lives on your own server and seamlessly plugs into your existing home media stack.
</p>
<div
@@ -136,7 +134,7 @@ export default function HomePage() {
>
<Link
href="/docs"
className="inline-flex h-11 items-center rounded-lg bg-fd-primary px-7 text-sm font-medium text-fd-primary-foreground transition-shadow hover:shadow-lg hover:shadow-fd-primary/20"
className="bg-fd-primary text-fd-primary-foreground hover:shadow-fd-primary/20 inline-flex h-11 items-center rounded-lg px-7 text-sm font-medium transition-shadow hover:shadow-lg"
>
Get Started
</Link>
@@ -144,7 +142,7 @@ export default function HomePage() {
href="https://github.com/jakejarvis/sofa"
target="_blank"
rel="noopener noreferrer"
className="inline-flex h-11 items-center rounded-lg px-7 text-sm font-medium ring-1 ring-fd-border transition-colors hover:bg-fd-accent hover:ring-fd-primary/30"
className="ring-fd-border hover:bg-fd-accent hover:ring-fd-primary/30 inline-flex h-11 items-center rounded-lg px-7 text-sm font-medium ring-1 transition-colors"
>
GitHub
</a>
@@ -160,12 +158,12 @@ export default function HomePage() {
{features.map((feature, i) => (
<div
key={feature.title}
className="animate-stagger-item rounded-xl bg-fd-card/50 p-6 ring-1 ring-white/[0.06]"
className="animate-stagger-item bg-fd-card/50 rounded-xl p-6 ring-1 ring-white/[0.06]"
style={{ "--stagger-index": i } as React.CSSProperties}
>
<feature.icon className="mb-3 size-5 text-fd-primary" />
<h3 className="mb-1.5 font-display text-lg">{feature.title}</h3>
<p className="text-sm leading-relaxed text-fd-muted-foreground">
<feature.icon className="text-fd-primary mb-3 size-5" />
<h3 className="font-display mb-1.5 text-lg">{feature.title}</h3>
<p className="text-fd-muted-foreground text-sm leading-relaxed">
{feature.description}
</p>
</div>
@@ -176,7 +174,7 @@ export default function HomePage() {
{/* ── Quick Start ── */}
<section className="mx-auto w-full max-w-3xl px-6 pb-24">
<h2
className="animate-stagger-item mb-8 text-center font-display text-2xl md:text-3xl"
className="animate-stagger-item font-display mb-8 text-center text-2xl md:text-3xl"
style={{ "--stagger-index": 0 } as React.CSSProperties}
>
Deploy in minutes
@@ -186,13 +184,11 @@ export default function HomePage() {
{steps.map((s) => (
<div
key={s.step}
className="animate-stagger-item rounded-xl bg-fd-card/50 p-5 ring-1 ring-white/[0.06]"
style={
{ "--stagger-index": Number(s.step) } as React.CSSProperties
}
className="animate-stagger-item bg-fd-card/50 rounded-xl p-5 ring-1 ring-white/[0.06]"
style={{ "--stagger-index": Number(s.step) } as React.CSSProperties}
>
<div className="flex items-baseline gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-fd-primary text-xs font-semibold text-fd-primary-foreground">
<span className="bg-fd-primary text-fd-primary-foreground flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold">
{s.step}
</span>
<h3 className="font-display text-base">{s.title}</h3>
@@ -202,15 +198,15 @@ export default function HomePage() {
))}
</div>
<p className="mt-4 text-xs text-fd-muted-foreground flex items-center gap-1 mx-auto w-fit">
<InfoIcon className="size-3 inline-block" />
<p className="text-fd-muted-foreground mx-auto mt-4 flex w-fit items-center gap-1 text-xs">
<InfoIcon className="inline-block size-3" />
The first registered account becomes admin.
</p>
<div className="mt-6 text-center">
<Link
href="/docs"
className="text-sm text-fd-primary underline underline-offset-4 hover:text-fd-primary/80"
className="text-fd-primary hover:text-fd-primary/80 text-sm underline underline-offset-4"
>
Read the full setup guide &rarr;
</Link>
+62 -99
View File
@@ -8,95 +8,75 @@ export const metadata: Metadata = {
export default function PrivacyPolicyPage() {
return (
<article className="prose prose-sm prose-invert mx-auto max-w-3xl px-6 py-16 [&_a]:text-fd-primary [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-fd-primary/80 [&_h2]:font-display [&_h2]:text-2xl [&_h2]:mt-10 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:pb-2 [&_h3]:font-display [&_h3]:text-xl [&_h3]:mt-8 [&_h3]:mb-3 [&_p]:text-fd-muted-foreground [&_p]:text-[15px] [&_p]:leading-relaxed [&_li]:text-fd-muted-foreground [&_li]:text-[15px] [&_li]:leading-relaxed [&_ul]:space-y-1 [&_strong]:text-fd-foreground [&_strong]:font-medium">
<h1 className="font-display text-3xl tracking-tight mb-2">
Privacy Policy
</h1>
<p className="text-sm text-fd-muted-foreground/60 !mt-0 mb-8">
Last updated: March 16, 2026
</p>
<article className="prose prose-sm prose-invert [&_a]:text-fd-primary hover:[&_a]:text-fd-primary/80 [&_h2]:font-display [&_h3]:font-display [&_p]:text-fd-muted-foreground [&_li]:text-fd-muted-foreground [&_strong]:text-fd-foreground mx-auto max-w-3xl px-6 py-16 [&_a]:underline [&_a]:underline-offset-4 [&_h2]:mt-10 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:pb-2 [&_h2]:text-2xl [&_h3]:mt-8 [&_h3]:mb-3 [&_h3]:text-xl [&_li]:text-[15px] [&_li]:leading-relaxed [&_p]:text-[15px] [&_p]:leading-relaxed [&_strong]:font-medium [&_ul]:space-y-1">
<h1 className="font-display mb-2 text-3xl tracking-tight">Privacy Policy</h1>
<p className="text-fd-muted-foreground/60 !mt-0 mb-8 text-sm">Last updated: March 16, 2026</p>
<p>
Sofa is a self-hosted application. When you run Sofa, your data lives on
your own server and is under your control. This policy describes what
data Sofa stores, what it sends externally, and how you can control it.
Sofa is a self-hosted application. When you run Sofa, your data lives on your own server and
is under your control. This policy describes what data Sofa stores, what it sends
externally, and how you can control it.
</p>
<h2>Data Stored on Your Server</h2>
<p>
All of the following data is stored locally in a single SQLite database
file on the machine running Sofa.
All of the following data is stored locally in a single SQLite database file on the machine
running Sofa.
</p>
<h3>Account Information</h3>
<ul>
<li>Email address, display name, and hashed password</li>
<li>Session tokens, IP address, and user agent string of active sessions</li>
<li>
Session tokens, IP address, and user agent string of active sessions
</li>
<li>
If using OIDC/SSO: provider account IDs and OAuth tokens from your
identity provider
If using OIDC/SSO: provider account IDs and OAuth tokens from your identity provider
</li>
</ul>
<h3>Viewing Activity</h3>
<ul>
<li>
Watchlist status (e.g. &ldquo;watching&rdquo;,
&ldquo;completed&rdquo;) for movies and TV shows
Watchlist status (e.g. &ldquo;watching&rdquo;, &ldquo;completed&rdquo;) for movies and TV
shows
</li>
<li>Individual movie and episode watch timestamps</li>
<li>Personal ratings</li>
<li>
Source of each watch event (manual entry, or imported from Plex,
Jellyfin, or Emby)
</li>
<li>Source of each watch event (manual entry, or imported from Plex, Jellyfin, or Emby)</li>
</ul>
<h3>Media Metadata</h3>
<p>
Movie, TV show, season, episode, and cast/crew metadata is fetched from{" "}
<a
href="https://www.themoviedb.org/"
target="_blank"
rel="noopener noreferrer"
>
<a href="https://www.themoviedb.org/" target="_blank" rel="noopener noreferrer">
The Movie Database (TMDB)
</a>{" "}
and cached locally. This includes titles, descriptions, images, genres,
streaming availability, and related recommendations. No personal data is
included in this metadata.
and cached locally. This includes titles, descriptions, images, genres, streaming
availability, and related recommendations. No personal data is included in this metadata.
</p>
<h3>Backups</h3>
<p>
Manual and scheduled backups are full copies of the database stored on
your server. They contain all of the data described above. Backup
management (creation, download, deletion) requires admin authentication.
Manual and scheduled backups are full copies of the database stored on your server. They
contain all of the data described above. Backup management (creation, download, deletion)
requires admin authentication.
</p>
<h2>External Services</h2>
<p>
Sofa communicates with the following external services during normal
operation.
</p>
<p>Sofa communicates with the following external services during normal operation.</p>
<h3>The Movie Database (TMDB)</h3>
<p>
Your server makes API requests to TMDB to fetch and refresh movie and TV
metadata. These requests include your TMDB API key and the IDs or search
queries for titles being looked up. A single API key is used for the
entire instance &mdash; TMDB cannot identify individual users. No
personal data (watch history, ratings, etc.) is ever sent to TMDB.
Your server makes API requests to TMDB to fetch and refresh movie and TV metadata. These
requests include your TMDB API key and the IDs or search queries for titles being looked up.
A single API key is used for the entire instance &mdash; TMDB cannot identify individual
users. No personal data (watch history, ratings, etc.) is ever sent to TMDB.
</p>
<p>
When the local image cache is enabled (the default), poster and backdrop
images are downloaded from the TMDB CDN and served from your server.
When disabled, images are loaded directly from TMDB&rsquo;s CDN by the
client.
When the local image cache is enabled (the default), poster and backdrop images are
downloaded from the TMDB CDN and served from your server. When disabled, images are loaded
directly from TMDB&rsquo;s CDN by the client.
</p>
<h3>Sofa Public API</h3>
@@ -104,78 +84,67 @@ export default function PrivacyPolicyPage() {
<ul>
<li>
<strong>Update checks</strong> &mdash; a periodic request to{" "}
<code>public-api.sofa.watch</code> to check for new releases. Only a
user agent string is sent; no instance or user data is included. Can
be disabled in admin settings.
<code>public-api.sofa.watch</code> to check for new releases. Only a user agent string is
sent; no instance or user data is included. Can be disabled in admin settings.
</li>
<li>
<strong>Telemetry</strong> &mdash; an optional, anonymous report sent
at most once every 24 hours. It includes a random instance ID, the
Sofa version, CPU architecture, bucketed user and title counts (e.g.
&ldquo;2-5&rdquo;), and which optional features are enabled. No
personal data, watch history, or exact counts are included. Telemetry
is <strong>disabled by default</strong> and must be explicitly enabled
by an admin. See the{" "}
<Link href="/docs/telemetry">telemetry documentation</Link> for full
details.
<strong>Telemetry</strong> &mdash; an optional, anonymous report sent at most once every
24 hours. It includes a random instance ID, the Sofa version, CPU architecture, bucketed
user and title counts (e.g. &ldquo;2-5&rdquo;), and which optional features are enabled.
No personal data, watch history, or exact counts are included. Telemetry is{" "}
<strong>disabled by default</strong> and must be explicitly enabled by an admin. See the{" "}
<Link href="/docs/telemetry">telemetry documentation</Link> for full details.
</li>
</ul>
<h3>PostHog (Mobile App Only)</h3>
<p>
The native iOS and Android app includes optional, anonymous analytics
powered by PostHog. This tracks screen views and app lifecycle events
&mdash; no personal data, search queries, or watch history is collected.
Analytics is <strong>disabled by default</strong> and requires explicit
opt-in. On iOS, Apple&rsquo;s App Tracking Transparency prompt is shown
first. You can change your preference at any time in the app&rsquo;s
settings.
The native iOS and Android app includes optional, anonymous analytics powered by PostHog.
This tracks screen views and app lifecycle events &mdash; no personal data, search queries,
or watch history is collected. Analytics is <strong>disabled by default</strong> and
requires explicit opt-in. On iOS, Apple&rsquo;s App Tracking Transparency prompt is shown
first. You can change your preference at any time in the app&rsquo;s settings.
</p>
<h3>Media Server Integrations</h3>
<p>
If you connect Plex, Jellyfin, or Emby, those services send webhook
events <em>to</em> your Sofa server when you finish watching something.
This data is processed and stored locally. Sofa does not send data back
to your media servers.
If you connect Plex, Jellyfin, or Emby, those services send webhook events <em>to</em> your
Sofa server when you finish watching something. This data is processed and stored locally.
Sofa does not send data back to your media servers.
</p>
<h3>Sonarr & Radarr</h3>
<p>
If you use Sonarr or Radarr integration, those services pull your
watchlist from Sofa via authenticated API requests. Sofa does not push
data to them.
If you use Sonarr or Radarr integration, those services pull your watchlist from Sofa via
authenticated API requests. Sofa does not push data to them.
</p>
<h2>Cookies</h2>
<p>
Sofa uses a session cookie to keep you logged in. This cookie is
HTTP-only, same-site, and contains only a session token. No third-party
cookies are set by the web app.
Sofa uses a session cookie to keep you logged in. This cookie is HTTP-only, same-site, and
contains only a session token. No third-party cookies are set by the web app.
</p>
<h2>Data Sharing</h2>
<p>
Because Sofa is self-hosted, there is no central service that has access
to your data. The project maintainers have no access to your database,
your watch history, or your account information. The only data that
leaves your server is described in the{" "}
Because Sofa is self-hosted, there is no central service that has access to your data. The
project maintainers have no access to your database, your watch history, or your account
information. The only data that leaves your server is described in the{" "}
<a href="#external-services">External Services</a> section above.
</p>
<h2>Data Retention & Deletion</h2>
<p>
All data is stored in a single SQLite file on your server. You have full
control over it:
All data is stored in a single SQLite file on your server. You have full control over it:
</p>
<ul>
<li>
Admin users can delete other user accounts, which cascades to all
associated viewing history and ratings
Admin users can delete other user accounts, which cascades to all associated viewing
history and ratings
</li>
<li>
Scheduled backups are automatically pruned after a configurable
retention limit (default: 7)
Scheduled backups are automatically pruned after a configurable retention limit (default:
7)
</li>
<li>Sessions and verification tokens expire automatically</li>
<li>Deleting the database file removes all data entirely</li>
@@ -183,20 +152,15 @@ export default function PrivacyPolicyPage() {
<h2>Children</h2>
<p>
Sofa is not directed at children under 13. Since the application is
self-hosted, account creation is controlled entirely by the server
administrator.
Sofa is not directed at children under 13. Since the application is self-hosted, account
creation is controlled entirely by the server administrator.
</p>
<h2>Open Source</h2>
<p>
Sofa is open source under the MIT License. You can audit exactly what
data is collected and how it is handled by reviewing the{" "}
<a
href="https://github.com/jakejarvis/sofa"
target="_blank"
rel="noopener noreferrer"
>
Sofa is open source under the MIT License. You can audit exactly what data is collected and
how it is handled by reviewing the{" "}
<a href="https://github.com/jakejarvis/sofa" target="_blank" rel="noopener noreferrer">
source code
</a>
.
@@ -204,9 +168,8 @@ export default function PrivacyPolicyPage() {
<h2>Changes</h2>
<p>
This policy may be updated as new features are added. Changes will be
reflected in the &ldquo;Last updated&rdquo; date above and committed to
the repository.
This policy may be updated as new features are added. Changes will be reflected in the
&ldquo;Last updated&rdquo; date above and committed to the repository.
</p>
<h2>Contact</h2>
+1
View File
@@ -1,4 +1,5 @@
import { createFromSource } from "fumadocs-core/search/server";
import { source } from "@/lib/source";
export const { GET } = createFromSource(source, {
+4 -7
View File
@@ -9,6 +9,7 @@ import {
import { createRelativeLink } from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getMDXComponents } from "@/components/mdx";
import { gitConfig } from "@/lib/layout.shared";
import { getPageImage, source } from "@/lib/source";
@@ -23,10 +24,8 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">
{page.data.description}
</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b pb-6">
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
<div className="flex flex-row items-center gap-2 border-b pb-6">
<MarkdownCopyButton markdownUrl={`${page.url}.mdx`} />
<ViewOptionsPopover
markdownUrl={`${page.url}.mdx`}
@@ -49,9 +48,7 @@ export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(
props: PageProps<"/docs/[[...slug]]">,
): Promise<Metadata> {
export async function generateMetadata(props: PageProps<"/docs/[[...slug]]">): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
+1
View File
@@ -1,4 +1,5 @@
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { baseOptions } from "@/lib/layout.shared";
import { source } from "@/lib/source";
@@ -1,12 +1,10 @@
import { notFound } from "next/navigation";
import { getLLMText, source } from "@/lib/source";
export const revalidate = false;
export async function GET(
_req: Request,
{ params }: RouteContext<"/llms.mdx/docs/[[...slug]]">,
) {
export async function GET(_req: Request, { params }: RouteContext<"/llms.mdx/docs/[[...slug]]">) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
+1
View File
@@ -1,4 +1,5 @@
import { llms } from "fumadocs-core/source";
import { source } from "@/lib/source";
export const revalidate = false;
+3 -7
View File
@@ -1,5 +1,6 @@
import { ImageResponse } from "@takumi-rs/image-response";
import { notFound } from "next/navigation";
import { getPageImage, source } from "@/lib/source";
export const revalidate = false;
@@ -37,10 +38,7 @@ const PRIMARY = "#fba952";
// oklch(0.63 0.02 80) → muted foreground
const MUTED = "#90887c";
export async function GET(
_req: Request,
{ params }: RouteContext<"/og/docs/[...slug]">,
) {
export async function GET(_req: Request, { params }: RouteContext<"/og/docs/[...slug]">) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
@@ -110,9 +108,7 @@ export async function GET(
{title}
</div>
{description ? (
<div style={{ fontSize: "24px", lineHeight: 1.5, color: MUTED }}>
{description}
</div>
<div style={{ fontSize: "24px", lineHeight: 1.5, color: MUTED }}>{description}</div>
) : null}
</div>
+2
View File
@@ -1,5 +1,7 @@
import { createAPIPage } from "fumadocs-openapi/ui";
import { openapi } from "@/lib/openapi";
import client from "./api-page.client";
export const APIPage = createAPIPage(openapi, { client });
+1
View File
@@ -1,5 +1,6 @@
import defaultMdxComponents from "fumadocs-ui/mdx";
import type { MDXComponents } from "mdx/types";
import { APIPage } from "@/components/api-page";
export function getMDXComponents(components?: MDXComponents) {
+2 -5
View File
@@ -43,13 +43,10 @@ export function ScrollIndicator() {
return (
<div
ref={ref}
className="absolute bottom-9 flex flex-col items-center gap-1.5 text-fd-muted-foreground transition-opacity duration-500"
className="text-fd-muted-foreground absolute bottom-9 flex flex-col items-center gap-1.5 transition-opacity duration-500"
style={{ opacity: shown && !scrolled && fits ? 1 : 0 }}
>
<ArrowDown
className="size-7 animate-scroll-bounce stroke-2"
aria-hidden="true"
/>
<ArrowDown className="animate-scroll-bounce size-7 stroke-2" aria-hidden="true" />
</div>
);
}
+1
View File
@@ -1,4 +1,5 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import { SofaLogo } from "@/components/sofa-logo";
export const gitConfig = {
+2 -1
View File
@@ -1,8 +1,9 @@
import { docs } from "collections/server";
import { type InferPageType, loader } from "fumadocs-core/source";
import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons";
import { openapiPlugin } from "fumadocs-openapi/server";
import { docs } from "collections/server";
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: "/docs",
+1 -4
View File
@@ -1,10 +1,7 @@
import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation";
import { type NextRequest, NextResponse } from "next/server";
const { rewrite: rewriteLLM } = rewritePath(
"/docs{/*path}",
"/llms.mdx/docs{/*path}",
);
const { rewrite: rewriteLLM } = rewritePath("/docs{/*path}", "/llms.mdx/docs{/*path}");
export default function proxy(request: NextRequest) {
if (isMarkdownPreferred(request)) {