Skip to content

Linked article ​

This page has not been reviewed by a human.

You write articles with hand-written links to Sefaria sources. Readers often want to glance at the passage without leaving your page. A plain link sends them away, and they may not come back.

This example solves that. A click on a citation opens a small dialog with the passage. The example fetches text only when a preview opens. It leaves your article text untouched.

This is a complete app (Vite and TypeScript) in examples/linked-article. It is shown running below. You can't edit it on this page. To read its source, see the example on GitHub.

Open in a new tab

How it works ​

An authored link is an ordinary link to sefaria.org. It also carries the reference in a data-sefaria-ref attribute, such as Micah 6:8.

html
<a
  href="https://www.sefaria.org/Micah.6.8"
  data-sefaria-ref="Micah 6:8"
  >Micah 6:8</a
>

The app enhances only a[data-sefaria-ref] links that have a non-blank reference and an href that is an https address on www.sefaria.org.

The app checks whether a link qualifies here:

ts
function isEligibleAnchor(anchor: HTMLAnchorElement): boolean {
  const tref = anchor.dataset.sefariaRef;
  if (tref === undefined || tref.trim().length === 0) {
    return false;
  }
  try {
    const url = new URL(anchor.href);
    return (
      url.protocol === "https:" &&
      url.hostname === "www.sefaria.org" &&
      url.username.length === 0 &&
      url.password.length === 0
    );
  } catch {
    return false;
  }
}

function shouldEnhanceActivation(
  event: MouseEvent,
  anchor: HTMLAnchorElement,
): boolean {
  return (
    !event.defaultPrevented &&
    event.button === 0 &&
    !event.altKey &&
    !event.ctrlKey &&
    !event.metaKey &&
    !event.shiftKey &&
    !anchor.hasAttribute("download") &&
    (anchor.target === "" || anchor.target === "_self")
  );
}

The preview ​

The app intercepts an ordinary click. An ordinary click means the primary button, with no Alt, Ctrl, Shift or Cmd key, no download attribute and no other target. The app then opens a native <dialog> that holds a <sefaria-source-card>.

ts
// Each opening owns its nodes so queued close/error events cannot close a newer preview.
const dialog = root.createElement("dialog");
dialog.id = DIALOG_ID;
dialog.className = "source-preview";
dialog.setAttribute("aria-label", "Sefaria source preview");
const closeButton = root.createElement("button");
closeButton.type = "button";
closeButton.textContent = "Close source preview";
closeButton.autofocus = true;
const card = root.createElement("sefaria-source-card") as SefariaSourceCard;
card.source = source;
card.setAttribute("sref", tref);
const events = new AbortController();
const preview = {
  card,
  anchor,
  close(): void {
    if (active !== preview) return;
    active = undefined;
    events.abort();
    card.setAttribute("sref", "");
    dialog.close();
    dialog.remove();
    if (preview.anchor.isConnected) preview.anchor.focus();
  },
};
active = preview;
closeButton.addEventListener("click", preview.close, {
  signal: events.signal,
});
dialog.addEventListener(
  "cancel",
  (event) => {
    event.preventDefault();
    preview.close();
  },
  { signal: events.signal },
);
dialog.addEventListener("close", preview.close, { signal: events.signal });
card.addEventListener(
  "sefaria-source-card-error",
  (event: Event) => {
    const { error, sref } = (
      event as CustomEvent<{
        readonly error: unknown;
        readonly sref: string;
      }>
    ).detail;
    if (active !== preview || card.sref !== sref) return;
    preview.close();
    status.textContent =
      error instanceof Error ? error.message : String(error);
    status.setAttribute("role", "alert");
  },
  { signal: events.signal },
);
dialog.append(closeButton, card);
root.body.append(dialog);
dialog.showModal();
closeButton.focus();

The card gets its sref only at that moment, so nothing loads before a click. The app creates its own client with the cache off and passes it to the card as its data source (the source property). The card makes one request, or two when a requested translation language is missing.

What readers get ​

  • Ctrl-, Cmd-, Shift- or middle-click, the context menu, target="_blank" and pages without JavaScript keep the normal link to Sefaria.
  • The dialog is modal, so you close it before using another article link. If another activation reaches the app while the dialog is open, it reuses the dialog and changes the reference.
  • Escape or the Close button closes the dialog. Focus returns to the link if it is still connected to the page.
  • If the request fails or the response is invalid, the dialog closes and a status area on the page shows the error (role="alert"). If Sefaria returns a documented HTTP error, such as a 404, the dialog stays open and the card shows the alert. To retry after that error, close the dialog and open it again.
  • The app adds only aria-controls to eligible links. destroy() removes its listeners and restores them.

What it doesn't do ​

It doesn't find citations in your text. You write the links.

For automatic detection, use Sefaria's Linker. It scans a page and links the citations it finds.

This example stores the authored reference in a data attribute. It differs from the Linker in two ways:

  • You write the links by hand. The Linker detects them.
  • The preview opens in a modal Source Card dialog. The Linker uses its own popups.

It doesn't submit your article or Linker tracking data. Opening a preview still sends a text request to Sefaria.

Run it locally ​

From the repository root, use Node.js 22.12 or later and the pnpm version pinned in package.json. Build the libraries first, because the example imports their built files:

sh
pnpm install
pnpm build
pnpm dev:linked-article

Next steps ​

Sefaria Frontend ToolkitCommunity-driven with Sefaria backing and support. GitHub Get support

AI-generated documentation.

About this documentation

Like the codebase, this documentation was scaffolded and written with GitHub Copilot. I don't have a background in technical writing, and wanted to get something out there.

Pages that have not undergone any human review are explicitly marked, but the others may also read funny or need work too. Sorry about that.

Please report any issues so they can be prioritized.

Thanks, Avi