Implementation Guide

A quick and easy guide to getting NeuraFeed data to show up on your website.

1. Knowing What You Get

When you call the API, you get back an object that has a mix of plain text and HTML in it. Knowing which field is which is pretty important so nothing breaks on your page.

  • Plain Text: Fields like title, summary, whyItMatters, and tags are just regular strings so you can use them directly in your components.
  • HTML Content: The article field comes back as pre-formatted HTML. You need to inject it into the DOM instead of printing it out as raw text.
  • Category: category is the section the article belongs to, one of technology, politics, economics, society, or world. Use it to group or filter. topic is different, that is the specific story. Older articles predate the field and omit it, so read it as article.category ?? "technology".
  • Sources: Prefer sourceDetails, which gives you id, number, title, url, and domain ready to use. The older sources array holds the same thing as strings shaped like [1] Site Name: https://... and only needs parsing when you are supporting articles from before the structured fields existed.
  • Structured Citations: citations links each cited passage back to its sourceIds, matching the data-citation-id attribute on the inline markers. Use it for custom footnotes, source popovers, or a non-HTML renderer. Note there are far fewer markers than sources, usually 5 to 7 against 10 to 20, because most attribution is written into the prose instead.
  • Cover Image: coverImage is a direct image URL (or null if unavailable). Always check for null before rendering. When you show it, link it to imageSourceUrl and label it with imageSource for attribution.
  • Rich Media: media is an array of optional attributed images, YouTube videos, audio, or direct videos. Use afterSection to place an item and show its sourceName/sourceUrl. embedMedia remains as a legacy YouTube shortcut.

2. Grabbing the Data

Call the API from your backend, a server component, or any other server-side code. Fetching it straight from browser JavaScript will not work: the endpoints send no Access-Control-Allow-Origin header, so the browser blocks a cross-origin request. If you need the data client-side, put a small proxy route on your own domain and fetch that instead. The second tab in the next section shows exactly that.

If you are on Next.js, an async Server Component is the simplest option: it fetches on the server and ships finished HTML, so CORS never enters the picture.

async function getLatestNews() {
  const response = await fetch('https://feed.neuraspheres.com/api/latest-news');
  const data = await response.json();
  return data.article;
}

3. Building the UI

Pick whichever tab matches your setup.

  • Next.js (server) fetches on the server, so there is no CORS problem at all. This is the shortest path and what we would reach for first. It also shows how to render media and fall back to the legacy sources strings on older articles.
  • React (via proxy) is for when the data genuinely has to arrive in the browser. It adds a one-line route on your own origin and points the client at that. Without the proxy the fetch is blocked.
  • Python renders server-side HTML and escapes every value it interpolates, which matters because only the article field is meant to be treated as markup.

All three inject article with an HTML renderer rather than printing it, only render sections that actually have content, and read sourceDetails in preference to parsing source strings.

// Server Component — runs on the server, so CORS never applies.
export default async function NewsViewer() {
  let article = null;

  try {
    const res = await fetch("https://feed.neuraspheres.com/api/latest-news", {
      // Articles publish twice a day; an hour of caching is plenty.
      next: { revalidate: 3600 },
    });
    if (res.ok) article = (await res.json()).article ?? null;
  } catch {
    // Network error — fall through to the "no news" message.
  }

  if (!article) return <div>No news found right now.</div>;

  // Prefer sourceDetails; older articles only have the legacy strings.
  const sources = article.sourceDetails?.length
    ? article.sourceDetails
    : (article.sources ?? []).map((raw, i) => {
        const m = raw.match(/^[(d+)]s+(.+?):s*(https?://S+)/);
        return m
          ? { id: `legacy-${i}`, number: Number(m[1]), title: m[2], url: m[3] }
          : { id: `legacy-${i}`, number: i + 1, title: raw, url: null };
      });

  return (
    <article>
      {/* category is the section; topic is the specific story */}
      <span className="badge">{article.category}</span>
      <h1>{article.title}</h1>
      <p>{article.summary}</p>

      {article.coverImage && (
        <figure>
          <img src={article.coverImage} alt={article.title} style={{ width: "100%" }} />
          {article.imageSource && (
            <figcaption>
              <a href={article.imageSourceUrl ?? article.coverImage} target="_blank" rel="noopener noreferrer">
                Source: {article.imageSource}
              </a>
            </figcaption>
          )}
        </figure>
      )}

      {/* Already HTML, so inject rather than print */}
      <div dangerouslySetInnerHTML={{ __html: article.article }} />

      {/* media replaces embedMedia; it may hold images, video or audio */}
      {article.media?.map((item) => (
        <figure key={item.id}>
          {item.type === "image" && <img src={item.url} alt={item.caption ?? ""} />}
          {item.type === "youtube" && (
            <iframe
              width="100%"
              height="400"
              src={`https://www.youtube-nocookie.com/embed/${item.youtubeId}`}
              title={item.title}
              allowFullScreen
            />
          )}
          {item.type === "audio" && <audio controls src={item.url} />}
          {item.type === "video" && <video controls src={item.url} />}
          <figcaption>
            {item.caption}
            {item.sourceUrl && <><a href={item.sourceUrl}>{item.sourceName}</a></>}
          </figcaption>
        </figure>
      ))}

      {sources.length > 0 && (
        <section>
          <h3>Sources</h3>
          <ol>
            {sources.map((src) => (
              <li key={src.id} id={`ref-${src.number}`}>
                {src.url ? <a href={src.url}>{src.title}</a> : src.title}
              </li>
            ))}
          </ol>
        </section>
      )}
    </article>
  );
}

4. Making It Look Good

The article body comes with its own HTML tags like h2, p, and sup already in it. You will probably want to write some CSS to style those so they match the rest of your site.

.article-container h2 {
  font-size: 24px;
  margin-top: 32px;
  font-weight: bold;
}

.article-container p {
  line-height: 1.6;
  margin-bottom: 16px;
}

/* Inline citation markers. They carry data-source-ids, so you can also
   use them as hooks for a popover instead of just styling them. */
.article-container sup {
  color: #2563eb;
  font-size: 12px;
  vertical-align: super;
  cursor: pointer;
}

/* The category badge from the examples above. */
.badge {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 999px;
  background: #eef2ff;
  color: #4338ca;
  font-size: 12px;
  text-transform: capitalize;
}

/* Attributed media from the media[] array. Always keep the caption:
   it carries the credit back to the original publisher. */
.article-container figure {
  margin: 24px 0;
}

.article-container figure img,
.article-container figure video {
  width: 100%;
  border-radius: 8px;
}

.article-container figcaption {
  font-size: 13px;
  opacity: 0.7;
  margin-top: 6px;
}

Wrapping Up

Every example handles article coming back null, which happens if nothing has published yet or the request failed. New articles land twice a day, around 00:17 and 12:17 UTC, so caching for an hour is plenty.

Want to page through the whole archive rather than just the latest? The cursor-paginated /api/articles endpoint is covered in the API Reference, along with the full field table. If you would rather your AI agent pull the news itself, there is an MCP server too.