# Subject link

> A link to any subject type, titled and routed by subject_titles, with plain text for a record the caller can't read.

- Block: [subjects](https://bettersupabase.com/blocks/subjects.md)
- Kind: component
- Hook: `useSubject` from `better-supabase/blocks/subjects/react`
- Page: https://bettersupabase.com/recipes/subject-link

## Install the block

```bash
npx better-supabase add subjects
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add button skeleton
```

From `apps/examples/nextjs/src/features/subjects/components/subject-link.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/subjects/components/subject-link.tsx):

```tsx
"use client";

import type { SubjectRef } from "better-supabase/blocks/subjects";

import { useSubject } from "better-supabase/blocks/subjects/react";
import { RotateCwIcon } from "lucide-react";
import { useExtracted } from "next-intl";

import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Link } from "@/i18n/navigation";

/**
 * A link to any subject in `config.subjects`, titled and routed by
 * `subject_titles`. The links rendered in one pass share one round trip, and a
 * subject the caller can't read shows as plain text.
 */
export function SubjectLink({ subject }: { subject: SubjectRef }) {
  const t = useExtracted("subjects");
  const { subject: resolved, error, refresh } = useSubject(subject);

  if (error) {
    return (
      <span className="flex items-center gap-1 text-sm text-muted-foreground">
        {t("The title didn't load")}
        <Button
          size="icon-xs"
          variant="ghost"
          aria-label={t("Retry")}
          onClick={() => {
            void refresh();
          }}
        >
          <RotateCwIcon />
        </Button>
      </span>
    );
  }

  if (resolved === undefined) {
    return <Skeleton className="h-4 w-32" />;
  }

  if (resolved === null) {
    return (
      <span className="truncate text-sm text-muted-foreground">
        {t("A record you can't open")}
      </span>
    );
  }

  const title = resolved.title ?? t("Untitled");

  return resolved.url === undefined ? (
    <span className="truncate text-sm font-medium">{title}</span>
  ) : (
    <Link
      href={resolved.url}
      className="truncate text-sm font-medium underline-offset-4 transition-colors hover:underline"
    >
      {title}
    </Link>
  );
}
```

## Prompt

Paste this into a coding agent to build the recipe in your app.

### Web

```text
Build "Subject link" in a React web app that uses shadcn/ui components. A link to any subject type, titled and routed by subject_titles, with plain text for a record the caller can't read.

It uses the better-supabase `subjects` block (Subjects). It calls the hook `useSubject` from `better-supabase/blocks/subjects/react`.

Steps:

1. If the app doesn't have the block yet, add the SQL with `npx better-supabase add subjects` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add button skeleton`.
3. The recipe needs no glue: the hook calls the block from the client.
4. Write the component. Call `useSubject` from `better-supabase/blocks/subjects/react`, starting from the recipe's snippet (`apps/examples/nextjs/src/features/subjects/components/subject-link.tsx`). Adapt it to the app's existing components, styling and data loading instead of copying it as is.
5. Check the result against the recipe and the block's docs: it typechecks, it calls the same hook, and it handles the loading and empty states the snippet handles.

Read the full code first:

- Recipe: https://bettersupabase.com/recipes/subject-link.md
- Block docs: https://bettersupabase.com/docs/blocks/subjects
```
