Created/edited by GitHub Copilot; pending human review.
Give components your own data
Components normally load their text from Sefaria. If you already hold the response, you can pass it in as data. If you want your own server to stand between the page and Sefaria, you can use a custom loader through source. With data, the element makes no request at all. With a data source, it still loads, and navigates in the Reader, but from the place you choose.
Three elements accept data, because their content doesn't change. Valid data is authoritative and makes no requests:
- Text Segment (
sefaria-text-segment) - Bilingual Segment (
sefaria-bilingual-segment) - Source Card (
sefaria-source-card)
The Reader and the Connections Panel don't accept data, because they change as the reader navigates.
A response you already have can be one you saved, fetched on your server, received from an MCP tool, or exported. To choose a data source, see Control loading.
Both cases start from corrected API-shaped JSON. That is the JSON body Sefaria's API returns, in the shape the toolkit's corrected API description expects.
Supply data once
Check unknown JSON before you pass it on. validateExternalResponse from @sefaria/api-client returns { valid, issues }. Each issue has an instancePath that points to the place in the response where the problem is.
Then set data on the Text Segment, Bilingual Segment, or Source Card. data is a JavaScript property, not an HTML attribute, so set it from script. Import elements from the package root, which registers them.
The example sets data even when the check fails, so the card shows its own error instead of staying blank. You can skip the assignment instead.
import { validateExternalResponse } from "@sefaria/api-client";
import { SefariaSourceCard } from "@sefaria/web-components";
// A Micah 6:8 response you saved earlier, served from your own site.
const stored: unknown = await (await fetch("/data/micah-6-8.json")).json();
// Check it against the API description before you hand it over.
const check = validateExternalResponse(
{ method: "GET", path: "/api/v3/texts/{tref}", status: 200 },
stored,
);
for (const issue of check.issues) {
console.error(
`Stored text is invalid at ${issue.instancePath}: ${issue.message}`,
);
}
// With data set, the card renders it and doesn't ask Sefaria for anything.
const card = document.querySelector("sefaria-source-card");
if (card instanceof SefariaSourceCard) {
card.data = stored;
}
The Source Card expects the texts API response, because that is the request it would have made. The Components reference lists the shape each element expects.
When data is set, it wins over sref. In the example above, the only request the page makes is the one to your own /data/micah-6-8.json.
What happens with data that isn't usable:
- Invalid data. The element shows its error state and drops any content it showed before. It cancels any load in progress and doesn't fall back to loading
sref. - Valid but empty data. It is still authoritative. The element shows its empty state and doesn't load
sref. - Data set back to
undefined. A connected element with a non-blanksrefloads from it again, through its data source.
Supplied data also follows the element's translation-fallback setting. It defaults to none on Text Segment and Bilingual Segment, and to default on Source Card. If the data lacks the translation language you asked for, the setting decides what happens. With none, the element shows a missing-language state. With default, it shows a fallback translation that is already in the supplied data and notes the substitution. Neither setting makes a request for the missing language. The Components reference describes each element's states.
To see the failure in your own code as well, read Handle errors in your code.
Control loading Advanced
Choose a data source
Choose where the component gets its data: use a toolkit client, provide a custom loader, or disable loading. A custom loader supplies getText, getLinks, or both, using local data or your own service.
In code, the data source is the source property. A custom loader is { kind: "custom", loader: { getText, getLinks } }.
The three choices are:
- Toolkit client,
{ kind: "client", client }. Use a client you created withcreateSefariaClient, with that client's cache settings. - Custom loader,
{ kind: "custom", loader }. Use your own functions. - Loading disabled,
{ kind: "disabled" }. Don't load anything.
To set the shared data source instead, call configureSefariaDataSource(choice). Elements with their own source ignore it. You can call it again until the first element uses the shared data source. After that, it throws.
With a custom loader or loading disabled, the element never falls back to requesting Sefaria from the browser.
A custom loader is an object with either or both of two async functions, getText and getLinks. Its type is SefariaDataLoader. Each function takes a request and an AbortSignal, and returns { payload, status }. The component validates the payload. The type names are in Package imports and exports.
If an element needs a function your custom loader doesn't have, the load fails with an error. With loading disabled, any load the element attempts fails with "Sefaria data loading is disabled." That includes the text a Reader asks for. If a later load fails, an element that already showed text keeps it. Its status becomes error, and its error event fires. Handle errors in your code shows how to read both.
Serve local data with a custom loader
The Reader and the Connections Panel have no data property. To show data you already hold, give them a custom loader through source. The custom loader answers from your local data for the references it has. For any other reference, it throws a clear error or hands the request to another source.
The Reader asks for the text of its root reference. It may also ask for the surrounding section, and for links. Serve each reference you want it to show. Each response is checked like any other payload.
import { SefariaReader } from "@sefaria/web-components";
import type { SefariaDataLoader } from "@sefaria/web-components";
// A saved Micah 6:8 response. The Reader asks for the chapter too, so serve both refs.
const stored: unknown = await (await fetch("/data/micah-6-8.json")).json();
const localTexts = new Map<string, unknown>([
["Micah 6:8", stored],
["Micah 6", stored],
]);
const loader: SefariaDataLoader = {
getText: async ({ sref }) => {
if (!localTexts.has(sref)) {
throw new Error(`No local text for ${sref}.`);
}
return { payload: localTexts.get(sref), status: 200 };
},
getLinks: async ({ sref }) => {
if (!localTexts.has(sref)) {
throw new Error(`No local links for ${sref}.`);
}
return { payload: [], status: 200 };
},
};
const reader = document.querySelector("sefaria-reader");
if (reader instanceof SefariaReader) {
reader.source = { kind: "custom", loader };
reader.sref = "Micah 6:8";
}
This example serves only Micah 6:8. The Reader loads without any request to Sefaria. The only request is your own /data/micah-6-8.json. Navigating to another reference fails with the error the custom loader throws.
Example: a Reader inside AI chat
The MCP App example is an app shown inside an AI chat. It passes createMcpReaderSource(host), a custom loader, to the Reader:
/** Creates a Reader data loader backed only by host-proxied tools. */
export function createMcpReaderSource(
host: McpReaderToolHost,
): SefariaDataSource {
return {
kind: "custom",
loader: {
getText: async (request, signal) => {
requireSupportedTextRequest(request);
const effectiveRequest = { tref: request.sref };
const result = await host.callServerTool(
{
name: "get_text",
arguments: {
reference: request.sref,
version_language: "both",
},
},
{ signal },
);
return admitSourceResponse(result, effectiveRequest);
},
getLinks: async (request, signal) => {
const effectiveRequest = {
tref: request.sref,
withText: request.withText,
};
const result = await host.callServerTool(
{
name: "get_links_between_texts",
arguments: {
reference: request.sref,
with_text: request.withText ? "1" : "0",
},
},
{ signal },
);
return admitConnectionsResponse(result, effectiveRequest);
},
},
};
}
getText calls the MCP server tool get_text. getLinks calls get_links_between_texts. Both go through host.callServerTool and pass the signal along, so the host learns when work is cancelled. The code checks each tool result with admitSourceResponse or admitConnectionsResponse before returning it.
This custom loader supports only the Reader's default selectors, the primary edition plus the translation, and it throws for others. It provides both functions. This Reader loads and navigates through the host tools and makes no direct browser requests to Sefaria. See Reader inside AI chat.
Learn more: How the toolkit works · Client reference