Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/fmoraga01/SpinAI/llms.txt

Use this file to discover all available pages before exploring further.

SpinAI includes a dedicated AI news feed at /noticias that keeps the team informed between meetings. News items are pulled from 10 hand-picked RSS feeds, stored in Supabase, and served to a paginated, filterable client-side page. The feed refreshes automatically every three hours through a GitHub Actions workflow — no manual curation required.

Curated sources

The feed aggregates content from ten sources defined in lib/newsSources.ts, selected for technical quality and reliability:

OpenAI

Official blog and product announcements from OpenAI.

Google DeepMind

Research updates and publications from DeepMind.

Hugging Face

Model releases, datasets, and open-source ML tooling.

MIT Technology Review

In-depth reporting on AI from MIT’s flagship technology publication.

The Verge

Consumer AI news and industry coverage.

Ars Technica

Technical deep-dives and AI policy analysis.

TechCrunch

Startup and funding news from the AI sector.

VentureBeat

Enterprise AI applications and business intelligence.

Wired

Long-form AI journalism and cultural commentary.

Import AI

Jack Clark’s weekly newsletter on AI research and policy, via Substack.

Refresh cadence

A GitHub Actions cron workflow runs every 3 hours and calls the internal API endpoint:
GET /api/cron/refresh-news
Each run fetches the RSS feed for every source and ingests up to 20 items per source. Items are deduplicated by URL before insertion — if an item’s URL already exists in the news_items table it is skipped, so re-running the cron never creates duplicates. For details on configuring the workflow and the required CRON_SECRET environment variable, see the GitHub Actions setup guide.

NewsItem data shape

Each ingested item is stored as a row in the news_items table and returned by loadNews() with the following shape:
export interface NewsItem {
  id: string;
  source: string;
  title: string;
  url: string;
  summary: string | null;
  imageUrl: string | null;
  publishedAt: string;
}
The summary field is populated from the RSS item’s <description> element when available. The imageUrl is extracted by checking, in order: RSS <enclosure> tags, <media:content>, <media:thumbnail>, and finally the first <img> tag found inside the item’s HTML body. If none of these are present imageUrl is stored as null and the news card renders without an image.

Browsing the feed

The /noticias page loads the first 20 items on mount and supports infinite pagination via a Cargar más noticias button that fetches the next page.

Source filter

Once items are loaded, a row of filter buttons appears above the feed — one per distinct source present in the current result set, plus an Todas (all) option. Clicking a source button filters the visible items client-side without a network request. The active filter is highlighted in blue. The filter is hidden when only one source is available.

Item display

Each news card shows:
  • A 96×96 px thumbnail (when imageUrl is not null), cropped to fill with object-fit: cover.
  • A source badge and a relative timestamp (e.g. “Hace 3h”, “Hace 2d”) derived from publishedAt. Hovering the timestamp shows the full locale-formatted date.
  • The title, clamped to two lines.
  • The summary, clamped to two lines, when present.
  • An ↗ external link arrow that opens the original article in a new tab.
Cards gain a blue glow (var(--shadow-glow)) and a #2C40FF44 border on hover.

Pagination

The loadNews(page) function in lib/news.ts fetches items from Supabase in pages of 20, ordered by published_at descending:
const PAGE_SIZE = 20;

export async function loadNews(
  page: number = 0
): Promise<{ items: NewsItem[]; hasMore: boolean }> {
  const from = page * PAGE_SIZE;
  const to = from + PAGE_SIZE - 1;

  const { data } = await db
    .from("news_items")
    .select("*")
    .order("published_at", { ascending: false })
    .range(from, to);

  return { items: data.map(rowToNewsItem), hasMore: data.length === PAGE_SIZE };
}
hasMore is true when a full page of 20 items was returned, signalling that another page may exist. The Cargar más button is hidden when hasMore is false or when a source filter is active (filtering is done entirely client-side over already-fetched data).
The news_items table is populated exclusively by the cron job. After a fresh deployment the table will be empty until the first cron run completes. The page handles this gracefully with a “Sin noticias nuevas” empty state. The first automatic refresh will occur within 3 hours, or you can trigger it manually by calling GET /api/cron/refresh-news with the correct CRON_SECRET header.

Build docs developers (and LLMs) love