npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ritik-singh/news-sdk

v0.1.2

Published

Embeddable news module SDK for OrgFlow platforms — articles, categories, tracking, polls, comments, subscriptions

Readme

@ritik-singh/news-sdk

A production-ready, embeddable news module for React/Next.js platforms. Drop in a single component and get a complete news experience — articles, categories, search, comments, reactions, polls, push notifications, bookmarks, subscriptions, analytics, and performance monitoring — with zero custom code.

Platforms just provide an API key, a base path, and optional theme overrides. The SDK handles everything else.


Table of Contents


Installation

npm install @ritik-singh/news-sdk
# or
pnpm add @ritik-singh/news-sdk
# or
yarn add @ritik-singh/news-sdk

Peer dependencies (must be installed in your project):

npm install react react-dom next

| Peer Dependency | Version | Required | |-----------------|----------|----------| | react | >=18.0.0 | Yes | | react-dom | >=18.0.0 | Yes | | next | >=14.0.0 | Yes |


Quick Start

Next.js App Router

Create a catch-all route at app/news/[[...slug]]/page.jsx:

import { NewsApp } from '@ritik-singh/news-sdk'
import '@ritik-singh/news-sdk/styles.css'

export default function NewsPage() {
  return (
    <NewsApp
      apiKey="pk_live_xxx"
      basePath="/news"
      router="app"
    />
  )
}

Next.js Pages Router

Create a catch-all route at pages/news/[[...slug]].jsx:

import { NewsApp } from '@ritik-singh/news-sdk'
import '@ritik-singh/news-sdk/styles.css'

export default function NewsPage() {
  return (
    <NewsApp
      apiKey="pk_live_xxx"
      basePath="/news"
      router="pages"
    />
  )
}

Custom React App

Use NewsProvider with your own routing and layout:

'use client'
import { NewsProvider, useArticles, useNewsContext } from '@ritik-singh/news-sdk'
import '@ritik-singh/news-sdk/styles.css'

function MyNewsFeed() {
  const { articles, loading, loadMore } = useArticles({ limit: 12 })
  const { colors } = useNewsContext()

  if (loading) return <p>Loading...</p>

  return (
    <div style={{ background: colors.bg, color: colors.text }}>
      {articles.map((a) => (
        <article key={a.article_id}>
          <h2>{a.title}</h2>
          <p>{a.excerpt}</p>
        </article>
      ))}
      <button onClick={loadMore}>Load More</button>
    </div>
  )
}

export default function Page() {
  return (
    <NewsProvider apiKey="pk_live_xxx">
      <MyNewsFeed />
    </NewsProvider>
  )
}

Push Notifications Setup

The SDK includes a service worker for browser push notifications.

1. Copy the service worker to your public directory

npx @ritik-singh/news-sdk init

This copies push-sw.js to public/news-push-sw.js.

2. How it works

  1. User clicks the bell icon or subscribes via the push CTA card
  2. SDK requests browser notification permission
  3. Registers the service worker and subscribes via the Web Push API (VAPID)
  4. Sends the subscription endpoint + keys to your backend
  5. Backend sends push payloads → service worker shows native notifications
  6. Clicking a notification focuses the existing tab or opens a new one

Push payload format

The backend should send JSON payloads with this shape:

{
  "title": "Breaking News",
  "body": "Shipping rates surge 30% overnight",
  "icon": "https://cdn.example.com/icon.png",
  "image": "https://cdn.example.com/cover.jpg",
  "url": "https://yoursite.com/news/article/shipping-surge",
  "badge": "/badge.png",
  "tag": "news-notification"
}

3. Customization

Control push notification UI via theme:

createTheme({
  header: { showPushNotifications: true },   // Bell icon in header
  feed: {
    showPushCTA: true,                       // Sidebar CTA card
    showPushToast: true,                     // Bottom toast prompt
    pushToastDelay: 45000,                   // Toast delay (45s)
  },
  push: {
    serviceWorkerPath: '/news-push-sw.js',   // SW file path
    promptTitle: 'Stay updated',             // Custom prompt title
    promptDesc: 'Get breaking news alerts',  // Custom prompt text
  },
})

Server-Side Metadata (SEO)

Generate Next.js Metadata objects server-side for optimal SEO:

// app/news/[[...slug]]/page.jsx
import { generateMetadata as genMeta } from '@ritik-singh/news-sdk/server'

export async function generateMetadata({ params }) {
  const slug = (await params).slug
  return genMeta({
    slug: Array.isArray(slug) ? slug : slug ? [slug] : [],
    apiKey: 'pk_live_xxx',
    siteTitle: 'My News',
    titleTemplate: '%s | My News',
    feedTitle: 'Latest News',
    feedDescription: 'Read the latest articles',
    defaultOgImage: 'https://yoursite.com/og.png',
  })
}

This fetches article data server-side and returns structured metadata with Open Graph + Twitter Card tags, so search engines and social media previews work correctly.


Theme Customization

Use createTheme() to build a theme object. Any option you don't specify falls back to the default.

import { createTheme, NewsApp } from '@ritik-singh/news-sdk'

const theme = createTheme({
  colors: { accent: '#e11d48' },
  fonts: { serif: 'Georgia, serif' },
  layout: { maxWidth: '1200px', cardStyle: 'shadow' },
  header: { logoText: 'My News' },
  feed: { articlesPerPage: 12 },
  article: { showComments: false },
})

<NewsApp apiKey="pk_live_xxx" theme={theme} />

You can also pass a raw object directly to <NewsApp theme={...}> — the SDK will call createTheme() internally if the object isn't already a resolved theme.

Additional exports:

import { DEFAULT_THEME, resolveColors } from '@ritik-singh/news-sdk'

DEFAULT_THEME                       // Full default theme object (read-only reference)
resolveColors(theme, 'dark')        // Returns merged color object for the given mode
resolveColors(theme, 'light')       // Returns light mode colors

resolveColors() merges theme.colors with theme.colorsDark when mode is "dark". This is what components use internally to get the active colors.

Colors

Light mode colors (all strings — any valid CSS color):

| Key | Default | Description | |---------------|-------------|---------------------------------| | bg | #ffffff | Page background | | surface | #f6f6f3 | Secondary backgrounds | | surface2 | #eeeeea | Tertiary / hover backgrounds | | card | #ffffff | Card backgrounds | | text | #16161a | Primary text | | muted | #6b6b73 | Secondary text | | faint | #9a9aa2 | Disabled / hint text | | border | #e6e6e1 | Default borders | | border2 | #d9d9d3 | Hover / active borders | | accent | #1f4fd1 | Primary brand color | | accentSoft | #eaefff | Accent light background | | accentText | #1f4fd1 | Accent on-text color | | red | #d22f2a | Breaking news, errors, danger | | redSoft | #fdecec | Red light background | | green | #1f7a4d | Success, positive | | amber | #b07a14 | Premium badges, warnings |

Dark Mode Colors

Overrides applied when the user toggles dark mode:

| Key | Default | |---------------|-------------| | bg | #0d0e11 | | surface | #16171b | | surface2 | #1d1f24 | | card | #15161a | | text | #f1f1ef | | muted | #a0a0a8 | | faint | #74747c | | border | #272930 | | border2 | #33353d | | accent | #5b86ff | | accentSoft | #1a2440 | | accentText | #86a4ff | | red | #ff5b54 | | redSoft | #33191a | | green | #3fae73 | | amber | #d6a23a |

Override dark mode colors with the colorsDark key:

createTheme({
  colors: { accent: '#e11d48' },           // Light mode
  colorsDark: { accent: '#ff6b8a' },       // Dark mode
})

Fonts

| Key | Default | Description | |-------------------|----------------------------------------------|---------------------------| | serif | 'Newsreader', Georgia, serif | Titles, article body | | sans | 'Libre Franklin', system-ui, -apple-system, sans-serif | UI elements, buttons | | mono | ui-monospace, 'Cascadia Code', monospace | Code blocks, labels | | loadGoogleFonts | true | Auto-load from Google Fonts |

createTheme({
  fonts: {
    serif: "'Playfair Display', Georgia, serif",
    sans: "'Inter', system-ui, sans-serif",
    loadGoogleFonts: true,  // Loads Playfair Display + Inter from Google
  },
})

Typography

All values are CSS font-size strings:

| Key | Default | Description | |------------------|------------|-----------------------------------| | heroTitle | 34px | Hero card title | | articleTitle | 42px | Article detail page title | | articleSubtitle| 21px | Article subtitle | | sectionHeading | 27px | h2 headings inside article body | | cardTitle | 20px | Article card title | | bodyText | 19px | Article paragraph text | | bodyLineHeight | 1.7 | Article paragraph line height | | caption | 13px | Meta, timestamps, labels | | badge | 11px | Badge text | | categoryLabel | 10.5px | Category name on cards | | navItem | 13.5px | Header nav buttons | | buttonText | 13.5px | Button labels | | blockquote | 25px | Blockquote text |

Spacing

| Key | Default | Description | |----------------|----------|-----------------------------------| | pagePadding | 20px | Left/right page padding | | gap | 20px | Default grid gap | | gapSm | 14px | Small gap (hero side cards) | | gapLg | 34px | Large gap (main + sidebar) | | sectionGap | 22px | Between feed sections | | cardPadding | 13px | Card internal padding |

Border Radius

| Key | Default | Description | |--------|----------|-----------------------------| | xs | 4px | Tiny elements | | sm | 6px | Badges, small chips | | md | 8px | Logo, nav buttons | | lg | 10px | Inputs, buttons | | xl | 12px | Cards, thumbnails | | xxl | 14px | Hero card, cover image | | card | 16px | Modals, feature cards | | full | 9999px | Pills, avatars |

Shadows

| Key | Default | |------|--------------------------------------------------------------------------------| | sm | 0 1px 2px rgba(20, 20, 30, 0.06) | | md | 0 1px 2px rgba(20, 20, 30, 0.06), 0 8px 24px rgba(20, 20, 30, 0.06) | | lg | 0 12px 40px rgba(20, 20, 30, 0.16) |

Layout

| Key | Default | Description | |----------------------|---------------|----------------------------------------| | maxWidth | 1320px | Outer container max-width | | contentWidth | 760px | Article body max-width | | sidebarWidth | 320px | Sidebar width in feed | | headerHeight | 64px | Sticky header height | | tickerHeight | 36px | Breaking news bar height | | heroColumns | 1.7fr 1fr | Hero section grid columns | | articleGridColumns | 2 | Article card grid columns | | relatedColumns | 3 | Related articles grid columns | | sidebarPosition | right | Sidebar position: right or left | | cardStyle | border | Card style: shadow, border, flat |

Header

| Key | Default | Description | |-------------------------|---------------|-------------------------------------| | show | true | Master toggle — hide entire header + ticker | | showSearch | true | Show search icon button | | showBookmarks | true | Show bookmarks icon | | showThemeToggle | true | Show dark/light mode toggle | | showSubscribe | true | Show subscribe button | | showTicker | true | Show breaking news ticker bar | | showCategoryNav | true | Show category navigation bar | | showPushNotifications | true | Show push notification bell icon | | subscribeBtnText | "Subscribe" | Subscribe button label | | logoText | null | Brand name text (null = hidden) | | logoIcon | null | Brand icon URL or single letter | | logoColor | "accent" | Logo square background color |

Feed Page

| Key | Default | Description | |---------------------|------------------|----------------------------------------| | showHero | true | Show hero section | | showCategoryChips | true | Show filter chip buttons | | showSortControls | true | Show sort buttons | | showSidebar | true | Show sidebar | | showTrending | true | Sidebar: trending articles list | | showTrendingTags | true | Sidebar: tag cloud | | showNewsletter | true | Sidebar: newsletter CTA card | | showCollections | true | Sidebar: collections list | | showLoadMore | true | Show "Load more" button | | showPushCTA | true | Sidebar: push notification CTA card | | showPushToast | true | Bottom-right push notification toast | | pushToastDelay | 45000 | Toast appear delay in ms (45 seconds) | | articlesPerPage | 8 | Articles per page | | defaultSort | "published_at" | Default sort: published_at, views, trending | | cardImageAspect | "16 / 10" | Card cover image aspect ratio | | showCardExcerpt | true | Show excerpt on cards | | showCardAuthor | true | Show author on cards | | showCardViews | true | Show view count on cards | | showCardBookmark | true | Show bookmark icon on cards | | trendingCount | 5 | Number of trending items in sidebar |

Article Detail Page

| Key | Default | Description | |--------------------|------------------|-----------------------------------------| | showProgressBar | true | Reading progress bar at top | | showLiveReaders | true | "X reading now" live badge | | showBadges | true | Breaking/Premium/Editor's Pick/Category badges | | showTitle | true | Article title (h1) | | showSubtitle | true | Article subtitle/excerpt below title | | showByline | true | Author byline section | | showCover | true | Cover image | | showTags | true | Tags row after article body | | showReactions | true | Reaction emoji grid | | showActionBar | true | Like/comment/share action bar | | showAuthorCard | true | Author bio card | | showPushPrompt | true | Push notification opt-in prompt | | showShareRow | true | Share buttons below body | | showComments | true | Comments section | | showRelated | true | Related articles grid | | showFontControls | true | A-/A+ font size buttons in byline | | fontScaleMin | 0.85 | Minimum font scale | | fontScaleMax | 1.3 | Maximum font scale | | coverAspect | "16 / 9" | Cover image aspect ratio | | commentSort | "created_at" | Default comment sort: created_at, like_count | | relatedAlgorithm | "hybrid" | Recommendation algorithm | | relatedCount | 3 | Number of related articles |

Sharing

createTheme({
  sharing: {
    channels: [
      'twitter', 'facebook', 'linkedin', 'whatsapp',
      'telegram', 'reddit', 'email', 'pinterest',
      'sms', 'embed', 'print', 'copy_link',
    ],
    inlineCount: 8,      // Buttons shown inline in article
    modalColumns: 4,     // Grid columns in share modal
  },
})

Reactions

Configure which reaction emojis are available:

createTheme({
  reactions: {
    enabled: [
      'like', 'dislike', 'love', 'fire', 'wow',
      'informative', 'celebrate', 'clap', 'think',
      'laugh', 'sad', 'angry',
    ],
  },
})

Subscriptions

createTheme({
  subscribe: {
    types: [
      'platform_newsletter', 'breaking_news', 'category',
      'weekly_digest', 'author',
    ],
    frequencies: ['instant', 'daily', 'weekly', 'monthly'],
    defaultFrequency: 'daily',
    newsletterTitle: 'The Morning Wire',
    newsletterDesc: 'Markets, freight & commodities — delivered every morning.',
  },
})

Push Notifications

createTheme({
  push: {
    serviceWorkerPath: '/news-push-sw.js',  // Service worker file path
    promptTitle: null,                       // Custom prompt title
    promptDesc: null,                        // Custom prompt description
  },
})

Performance Monitoring

createTheme({
  performance: {
    enabled: true,        // Master toggle for all performance tracking
    reportErrors: true,   // Report JS errors to backend
  },
})

When enabled, automatically collects:

  • Core Web Vitals: LCP, FCP, CLS, INP, TTFB
  • Page load metrics: page_load_time_ms, dom_interactive_ms, dom_complete_ms
  • Resource metrics: page_size_bytes, resource_count, image_size_bytes, js_size_bytes, css_size_bytes
  • JS errors: window.onerror + unhandledrejection (throttled to 1 report per 30 seconds)

Reports once per page load via POST /performance/report. SSR-safe.

Animations

| Key | Default | Description | |--------------------|-------------|---------------------------------------| | enabled | true | Master toggle for all animations | | duration | 150ms | Default transition duration | | durationSlow | 250ms | Slow transitions | | cardEntrance | fadeUp | Card appear animation | | modalEntrance | slideIn | Modal appear animation | | pollBarDuration | 600ms | Poll result bar fill animation | | tickerSpeed | 38s | Breaking news ticker scroll speed | | toastDuration | 2600 | Toast auto-dismiss time (ms) |

SEO / Meta

| Key | Default | Description | |------------------|--------------------------|---------------------------------------| | siteTitle | "" | Site title (e.g. "My News") | | titleTemplate | "%s" | Title template (e.g. "%s | My News") | | feedTitle | "News" | Feed page title | | feedDescription| "" | Feed page meta description | | ogImage | "" | Default Open Graph image (fallback) | | twitterCard | "summary_large_image" | Twitter card type | | canonicalBase | "" | Canonical URL base | | dynamicTitle | true | Auto-set document.title on navigation |


Components

NewsApp

Complete standalone news application. Renders all pages, routes, header, and engagement features.

<NewsApp
  apiKey="pk_live_xxx"          // Required — Platform API key
  basePath="/news"              // URL base path (default: "/news")
  router="app"                  // "app" (App Router) or "pages" (Pages Router), default: "app"
  theme={theme}                 // Theme overrides (optional)
  ssr={false}                   // Server-side meta (default: false)
  baseUrl="https://..."         // Optional — default: https://your-api.example.com/platform/news
  locale="en"                   // Locale code (default: "en")
/>

NewsProvider

Root context provider. Use this when building custom layouts with hooks.

<NewsProvider
  apiKey="pk_live_xxx"                          // Required
  theme={theme}                                 // Theme object or overrides
  baseUrl="https://..."                         // Optional — defaults to https://your-api.example.com/platform/news
  locale="en"                                   // Locale (default: "en")
  basePath="/news"                              // URL base path (default: "/news")
  ssr={false}                                   // Server-side meta (default: false)
  route={{ page: "feed", param: null }}         // Current route
  navHelpers={navHelpers}                       // Navigation functions
  urlSearchQuery=""                             // Search query from URL params
>
  {children}
</NewsProvider>

HeaderBar

Sticky header with logo, category navigation, search, bookmarks, theme toggle, push bell, and subscribe button. Reads all config from context — no required props.

BreakingTicker

Horizontal scrolling ticker showing breaking/trending/latest news. Falls back gracefully: breaking → trending → latest. No required props — reads from context.

<BreakingTicker onItemClick={(slug) => {}} />  // Optional click handler

FeedPage

Full homepage with hero section, category filter chips, sort controls, article grid, sidebar, and pagination. No required props — reads everything from context.

HeroSection

Featured article highlight at the top of the feed. No required props.

ArticleCard

Single article card for grid display.

<ArticleCard
  article={articleObject}    // Required — article data
  compact={false}            // Compact mode for hero side cards (default: false)
/>

ArticleGrid

Responsive grid of article cards.

<ArticleGrid
  articles={[]}              // Article array
  pagination={null}          // Pagination object from useArticles
  loading={false}            // Loading state
  onLoadMore={() => {}}      // Load more handler
  excludeId={null}           // Article ID to exclude (e.g., hero article)
/>

CategoryChips

Horizontal category filter buttons. No props — reads selectedCategory and setSelectedCategory from context.

SortControls

Sort toggle buttons (Latest, Most viewed, Trending). No props — reads sortBy and setSortBy from context.

Sidebar

Sidebar with trending articles, tag cloud, newsletter CTA, collections, and push CTA. All sections toggleable via theme. No required props.

TrendingList

Top articles by trending score. Used inside Sidebar. No props — reads config from context.

TrendingTags

Tag cloud for filtering articles by tag. Used inside Sidebar. No props — reads config from context.

NewsletterCTA

Newsletter subscription call-to-action card. Shows title/description from theme.subscribe.newsletterTitle and theme.subscribe.newsletterDesc. No props.

CollectionsList

Featured collections cards. Used inside Sidebar. No props — reads config from context.

PushCTA

Push notification opt-in card for the sidebar. Shows prompt to enable browser push notifications. Reads config from theme.push.promptTitle and theme.push.promptDesc. No props.

ArticleDetailPage

Full article page with all engagement features: reading progress, cover image, byline with font controls, article body with interactive polls, tags, reactions, action bar, push prompt, share buttons, comments with nested replies, and related stories. No props — reads article slug from route.param in context.

SectionRenderer

Renders article body content sections (text, images, blockquotes, polls, embeds, etc.). Used internally by ArticleDetailPage but also available for custom article layouts.

<SectionRenderer
  sections={article.sections}    // Section[] from article API (default: [])
  polls={article.polls}          // Poll[] from article API (default: [])
  fontScale={1}                  // Font scale factor (default: 1)
  linkMap={linkMap}              // Link tracking map from useLinkTracking (default: {})
  onLinkClick={(linkId) => {}}   // Click handler for tracked links
/>

PollWidget

Interactive poll with vote tracking, change-vote support, and animated result bars.

<PollWidget
  pollId={123}                   // Required — poll ID
  pollData={pollObject}          // Optional — pre-loaded poll data
/>

SearchOverlay

Full-screen search modal with live results and analytics tracking. No props — reads searchOpen and setSearchOpen from context.

SubscribeModal

Email subscription form supporting multiple subscription types (newsletter, breaking news, category, digest, author). No props — reads subscribeOpen and setSubscribeOpen from context.

ShareModal

Social sharing buttons in a modal grid.

<ShareModal
  open={true}                    // Whether modal is open
  onClose={() => {}}             // Close handler
  onShare={(channel) => {}}      // Share handler from useShare
  onCopyLink={() => {}}          // Copy link handler from useShare
  articleUrl="https://..."       // Article URL for sharing
/>

PreferencesPage

Full subscription management page. Email input, subscription list with frequency dropdowns, unsubscribe/resubscribe actions, and push notification status. Accessible at /news/preferences.


Hooks

All hooks must be used inside <NewsProvider> or <NewsApp>. They handle loading states, error recovery, abort signals, and caching automatically.

useNewsContext

Access the full SDK context.

const {
  apiKey,                 // Platform API key
  client,                 // API client instance
  theme,                  // Resolved theme object
  colors,                 // Current mode color map
  themeMode,              // "light" | "dark"
  toggleTheme,            // () => void
  selectedCategory,       // number | null
  setSelectedCategory,    // (id) => void
  sortBy,                 // string
  setSortBy,              // (sort) => void
  searchQuery,            // string
  setSearchQuery,         // (query) => void
  selectedTag,            // number | null
  setSelectedTag,         // (id) => void
  searchOpen,             // boolean
  setSearchOpen,          // (open) => void
  subscribeOpen,          // boolean
  setSubscribeOpen,       // (open) => void
  locale,                 // string
  basePath,               // string
  ssr,                    // boolean
  route,                  // { page, param }
  navigate,               // (url) => void
  navigateToFeed,         // () => void
  navigateToArticle,      // (slug) => void
  navigateToCategory,     // (slug) => void
  navigateToTag,          // (slug) => void
  navigateToCollection,   // (slug) => void
  navigateToBookmarks,    // () => void
  navigateToPreferences,  // () => void
  navigateToSearch,       // (query) => void
} = useNewsContext()

useArticles

Fetch published articles with filtering, sorting, and pagination.

const {
  articles,       // Article[]
  pagination,     // { hasNextPage, page, limit, total }
  loading,        // boolean
  error,          // string | null
  loadMore,       // () => void — append next page
  refetch,        // () => void — reset to page 1
} = useArticles({
  is_featured: true,        // Only featured articles
  is_breaking: false,       // Only breaking news
  is_editors_pick: false,   // Only editor's picks
  article_type: 'standard', // Article type filter
  author_id: 5,             // Filter by author
  limit: 8,                 // Articles per page (default: theme.feed.articlesPerPage)
  date_from: '2024-01-01',  // Start date (YYYY-MM-DD)
  date_to: '2024-12-31',    // End date (YYYY-MM-DD)
})

Auto-refetches (debounced 300ms) when category, sort, search query, or tag changes in context.

useArticle

Fetch a single article by slug with full details.

const {
  article,    // Full article object with sections, polls, authors, tags, related
  loading,    // boolean
  error,      // string | null
} = useArticle('my-article-slug')

useCategories

Fetch categories with automatic module-level caching (5-minute TTL). Multiple components share one API call.

const {
  categories,   // Category[]
  loading,      // boolean
  error,        // string | null
  refetch,      // () => Promise — clears cache and re-fetches
} = useCategories({
  sort_by: 'display_order',   // 'display_order' | 'category_name' | 'article_count'
  limit: 50,                   // default: 50
  featured: true,              // Only featured categories (omit for all categories)
})

useBreakingNews

Fetch breaking news for the ticker with intelligent fallback (breaking → trending → latest). Cached per app lifecycle.

const {
  items,      // { id, title, slug, image }[]
  loading,    // boolean
  source,     // "breaking" | "trending" | "latest" | null
} = useBreakingNews({ limit: 10 })

useTags

Fetch trending tags for the sidebar tag cloud.

const {
  trending,   // { tag_id, tag_name, article_count }[]
  loading,    // boolean
  error,      // string | null
} = useTags({ limit: 10 })

useAuthors

Fetch all authors.

const {
  authors,    // Author[]
  loading,    // boolean
  error,      // string | null
} = useAuthors({ limit: 50, sort_by: 'display_name' })

useAuthor

Fetch a single author with their recent articles.

const {
  author,          // Author object | null
  authorArticles,  // Article[] (last 5 published)
  loading,         // boolean
  error,           // string | null
} = useAuthor(authorId)

useCollections

Fetch featured collections for sidebar.

const {
  collections,   // Collection[]
  loading,       // boolean
  error,         // string | null
} = useCollections({
  limit: 4,                    // default: 4
  featured: true,              // default: true (set false to include non-featured)
})

useCollection

Fetch collection detail with paginated articles.

const {
  collection,   // Collection object | null
  articles,     // Article[]
  pagination,   // { hasNextPage, page }
  loading,      // boolean
  error,        // string | null
  sort,         // string (default: "display_order")
  setSort,      // (sort) => void — also resets pagination to page 1
  loadMore,     // () => void — append next page of articles
} = useCollection('collection-slug')

useTracking

Accurate visitor tracking for article detail pages. Handles page views, heartbeats, engaged time, scroll depth, and page exit.

const {
  viewId,        // string | null — tracking view ID
  sessionUuid,   // string | null
  liveReaders,   // number — real-time reader count
  scrollPercent,  // number — current scroll percentage
} = useTracking(articleId)

Behavior:

  • Sends page_view on mount + immediate first heartbeat (registers live reader instantly)
  • Heartbeat every 30 seconds with scroll depth + engaged time
  • Engaged time automatically pauses when browser tab is hidden (document.visibilityState)
  • page_exit on unmount/beforeunload with fetch keepalive: true for reliable delivery; falls back to navigator.sendBeacon if fetch fails
  • Sends exit_type: "navigate_away" and engaged_time_seconds with the exit event
  • Live reader count refreshes every 60 seconds (reads data.count from API)
  • Fresh session per page load (no localStorage persistence for session_uuid)

usePoll

Interactive poll with vote tracking and change-vote support.

const {
  poll,             // Full poll object with options + vote counts
  hasVoted,         // boolean
  selectedOptions,  // number[] — selected option IDs
  showResults,      // boolean — show result bars
  vote,             // (optionIds: number[]) => Promise<boolean>
  loading,          // boolean
} = usePoll(pollId, initialPollData)

Behavior:

  • Checks vote status from API on mount (via visitor_uuid)
  • Saves to localStorage only after confirmed API success
  • Self-healing counters: backend uses COUNT(*) from actual vote rows
  • Supports vote change (delete + re-insert)

useComments

Full comment system with nested replies.

const {
  comments,         // Comment[] — pre-nested with replies
  totalComments,    // number
  loading,          // boolean
  sort,             // string
  setSort,          // (sortBy, order?) => void — also resets to page 1
  addComment,       // (text, authorName, parentCommentId?) => Promise<object|null>
  editComment,      // (commentId, text) => Promise<boolean>
  deleteComment,    // (commentId) => Promise<boolean>
  reactToComment,   // (commentId, "like"|"dislike") => Promise<boolean>
  removeCommentReaction, // (commentId) => Promise<boolean>
  reportComment,    // (commentId, reason, description?) => Promise<boolean>
  loadMore,         // () => void
  hasMore,          // boolean
} = useComments(articleId)

useBookmarks

Manage article bookmarks with optimistic updates and full rollback on error.

const {
  bookmarks,      // Bookmark[] with article data
  isBookmarked,   // (articleId) => boolean
  toggle,         // (articleId) => Promise — optimistic toggle
  count,          // number
  loading,        // boolean
} = useBookmarks()

useReactions

Article reactions with 12 emoji types and optimistic toggle.

const {
  reactions,       // { like: 5, love: 3, fire: 2, ... }
  myReaction,      // string | null — current visitor's reaction
  totalReactions,  // number
  loading,         // boolean
  react,           // (type) => Promise<boolean> — toggle: same=remove, different=switch
  removeReaction,  // () => Promise<boolean>
} = useReactions(articleId)

Available reaction types: like, dislike, love, fire, wow, informative, celebrate, clap, think, laugh, sad, angry

useShare

Social sharing with automatic backend tracking.

const {
  share,             // (channel) => void
  copyLink,          // () => Promise<boolean>
  shareModalOpen,    // boolean
  setShareModalOpen, // (open) => void
} = useShare(articleId, articleUrl, articleTitle)

Channels: twitter, facebook, linkedin, whatsapp, telegram, reddit, email, pinterest, sms, embed, print, copy_link

useSubscription

Subscribe users to newsletters and alerts.

const {
  subscribe,     // ({ email, subscriptionType, frequency?, entityType?, entityId? }) => Promise<boolean>
  loading,       // boolean
  success,       // boolean
  error,         // string | null
  reset,         // () => void
  isSubscribed,  // (email, type) => boolean
  lastEmail,     // string — last subscribed email
} = useSubscription()

Subscription types: platform_newsletter, breaking_news, category, weekly_digest, author

Exported helper functions (standalone, no hook needed):

import { isAlreadySubscribed, hasAnySubscription, getLastSubscribedEmail } from '@ritik-singh/news-sdk'

isAlreadySubscribed('[email protected]', 'platform_newsletter')  // boolean
hasAnySubscription('[email protected]')                          // boolean
getLastSubscribedEmail()                                        // string (from localStorage)

useSubscriptionManager

Manage existing subscriptions: list, update frequency, unsubscribe, resubscribe.

const {
  subscriptions,       // Subscription[]
  loading,             // boolean
  error,               // string | null
  fetchSubscriptions,  // (email) => Promise
  updateFrequency,     // (subscriptionId, frequency) => Promise<boolean>
  unsubscribe,         // (subscriptionId) => Promise<boolean> — deactivates + cleans localStorage
  resubscribe,         // (email, type, entityType?, entityId?) => Promise<boolean>
  TYPE_ICONS,          // Icon map (see below)
  TYPE_LABELS,         // Label map (see below)
} = useSubscriptionManager()

TYPE_ICONS — Emoji icons for each subscription type:

| Key | Icon | |------------------------|------| | platform_newsletter | 📰 | | breaking_news | ⚡ | | category | 📂 | | author | ✍️ | | tag | 🏷️ | | weekly_digest | 📅 | | daily_digest | 📋 |

TYPE_LABELS — Display labels for each subscription type:

| Key | Label | |------------------------|----------------| | platform_newsletter | Newsletter | | breaking_news | Breaking News | | category | Category | | author | Author | | tag | Tag | | weekly_digest | Weekly Digest | | daily_digest | Daily Digest |

usePushNotifications

Manage browser push notification subscription lifecycle.

const {
  permission,    // "default" | "denied" | "granted" — current Notification.permission
  isSubscribed,  // boolean — actively subscribed to push
  loading,       // boolean — subscribe/unsubscribe in progress
  supported,     // boolean — true if browser has ServiceWorker + PushManager + Notification APIs
  subscribe,     // () => Promise<boolean> — full subscribe flow (permission → SW → VAPID → backend)
  unsubscribe,   // () => Promise<boolean> — unsubscribe from browser + notify backend
} = usePushNotifications()

Subscribe flow:

  1. Check browser support (ServiceWorker + PushManager + Notification)
  2. Request Notification.requestPermission() — returns false if denied
  3. Register service worker from theme.push.serviceWorkerPath
  4. Check if browser already has a push subscription (dedup)
  5. If not, fetch VAPID public key from backend (cached 1 hour)
  6. Subscribe via PushManager.subscribe() with VAPID key
  7. Extract endpoint, auth_key, p256dh_key from subscription
  8. Send to backend via POST /push-subscriptions
  9. Persist subscription state to localStorage

Exported helper functions (standalone, no hook needed):

import { isToastDismissed, dismissToast } from '@ritik-singh/news-sdk'

isToastDismissed()   // boolean — true if push toast was dismissed within last 7 days
dismissToast()       // void — marks push toast as dismissed (stores timestamp)

useLinkTracking

Track clicks on article links.

const {
  trackLinkClick,  // (linkId) => Promise
  linkMap,         // { [originalUrl]: { link_id, tracking_url, ... } }
} = useLinkTracking(articleId, article.links)

useRecommendations

Fetch AI-powered article recommendations with event tracking.

const {
  recommendations,  // Article[] — falls back to related_articles if empty
  loading,          // boolean
  trackDisplayed,   // (recommendationIds: number[]) => void
  trackClicked,     // (recommendationId, dwellTimeSeconds?) => void
} = useRecommendations(articleId, fallbackRelated)

useSearchAnalytics

Track search queries and result clicks for analytics.

const {
  trackSearch,       // (query, resultsCount?) => Promise<string|null> — returns queryId
  trackResultClick,  // (articleId, position?) => Promise
} = useSearchAnalytics()

usePerformance

Auto-collects and reports Core Web Vitals + page metrics + JS errors. No return value — runs automatically. Gated by theme.performance.enabled.

// Automatically mounted inside NewsApp — no manual usage needed
usePerformance()

usePageMeta

Dynamically set document.title and meta tags. Restores previous values on unmount.

usePageMeta({
  title: 'Article Title',
  description: 'Article excerpt...',
  ogTitle: 'Article Title',
  ogDescription: 'Article excerpt...',
  ogImage: 'https://...',
  ogType: 'article',
  ogUrl: 'https://...',
  twitterCard: 'summary_large_image',
  canonicalUrl: 'https://...',
  keywords: 'news, shipping',
  author: 'John Doe',
  publishedTime: '2024-06-15T10:00:00Z',
})

useVisitor

Manage visitor identity for all engagement features (comments, reactions, polls, tracking).

const {
  visitorUuid,   // string | null
  loading,       // boolean
} = useVisitor()

Behavior:

  • Generates a UUID (or reuses previously stored one) and persists it to localStorage immediately
  • Registers via POST /visitors/identify — marks as "registered" in localStorage only after success
  • If registration fails, the UUID is kept but not marked registered, so next mount retries with the same UUID
  • Returns null if registration fails (hooks that depend on visitorUuid gracefully skip)

API Client

The SDK includes a built-in API client. You can access it via context or create one directly.

Via context

const { client } = useNewsContext()

const data = await client.get('/articles', { limit: 10, is_featured: true })
const result = await client.post('/track', { event: 'page_view', article_id: 123 })

Direct creation

import { createApiClient } from '@ritik-singh/news-sdk'

const client = createApiClient({
  apiKey: 'pk_live_xxx',         // Required
  baseUrl: 'https://...',        // Optional — default: "https://your-api.example.com/platform/news"
  timeout: 15000,                // Optional — default: 15000 (ms)
})

// Methods
client.get(path, query?, options?)
client.post(path, body?, options?)
client.put(path, body?, options?)
client.delete(path, body?, options?)
client.getBaseUrl()    // Returns the configured base URL
client.getApiKey()     // Returns the configured API key

Request options

{
  query: { limit: 10 },           // URL query parameters
  signal: abortController.signal,  // AbortSignal for cancellation
  headers: { 'X-Custom': 'val' }, // Extra headers (merged before X-API-Key)
  keepalive: true,                 // For reliable page-exit tracking (disables timeout signal)
}

All requests include X-API-Key header automatically (appended last to prevent override). Expects responses in { success: true, data, message } format.

Error handling

When a request fails, the client throws an Error object with extra properties:

try {
  await client.get('/articles')
} catch (err) {
  err.message   // "Request failed: 404" or server message
  err.status    // HTTP status code (e.g., 404, 500)
  err.code      // Error code: "REQUEST_FAILED", "INVALID_RESPONSE", or server error code
  err.data      // Full JSON response body (when available)
}

Routing

The SDK uses a simple path-based routing system.

Routes

| URL Pattern | Route Object | |------------------------------|----------------------------------------| | /news | { page: "feed", param: null } | | /news/article/my-slug | { page: "article", param: "my-slug" }| | /news/category/tech | { page: "category", param: "tech" } | | /news/tag/shipping | { page: "tag", param: "shipping" } | | /news/collection/weekly | { page: "collection", param: "weekly" } | | /news/bookmarks | { page: "bookmarks", param: null } | | /news/preferences | { page: "preferences", param: null } | | /news/search?q=rice | { page: "search", param: null } |

Direct utilities

import { parseRoute, createNavigationHelpers } from '@ritik-singh/news-sdk'

const route = parseRoute('/news/article/my-slug', '/news')
// → { page: "article", param: "my-slug" }

const helpers = createNavigationHelpers('/news', router.push)
helpers.navigateToFeed()                    // → /news
helpers.navigateToArticle('my-slug')        // → /news/article/my-slug
helpers.navigateToCategory('tech')          // → /news/category/tech
helpers.navigateToTag('shipping')           // → /news/tag/shipping
helpers.navigateToCollection('weekly')      // → /news/collection/weekly
helpers.navigateToBookmarks()               // → /news/bookmarks
helpers.navigateToPreferences()             // → /news/preferences
helpers.navigateToSearch('shipping rates')  // → /news/search?q=shipping%20rates

CSS Variables

The SDK sets CSS custom properties on the .orgflow-news wrapper element. Override them in your own CSS to customize colors without JavaScript:

.orgflow-news {
  /* Colors */
  --on-bg: #ffffff;
  --on-surface: #f6f6f3;
  --on-surface2: #eeeeea;
  --on-card: #ffffff;
  --on-text: #16161a;
  --on-muted: #6b6b73;
  --on-faint: #9a9aa2;
  --on-border: #e6e6e1;
  --on-border2: #d9d9d3;
  --on-accent: #1f4fd1;
  --on-accent-soft: #eaefff;
  --on-accent-text: #1f4fd1;
  --on-red: #d22f2a;
  --on-red-soft: #fdecec;
  --on-green: #1f7a4d;
  --on-amber: #b07a14;

  /* Fonts */
  --on-font-serif: 'Newsreader', Georgia, serif;
  --on-font-sans: 'Libre Franklin', system-ui, sans-serif;
  --on-font-mono: ui-monospace, 'Cascadia Code', monospace;
}

Data attributes

<div class="orgflow-news" data-theme="light" data-card-style="border">

Target dark mode overrides:

.orgflow-news[data-theme="dark"] {
  --on-bg: #0d0e11;
  --on-text: #f1f1ef;
}

CSS animations

The stylesheet defines these keyframe animations:

| Animation | Description | |-----------------|--------------------------------| | on-ticker | Horizontal scroll for ticker | | on-live-pulse | Pulsing dot for live readers | | on-fade-up | Card entrance fade + slide up | | on-slide-in | Modal entrance slide in |


Storage

Storage API

The SDK exports localStorage utility functions. All keys are automatically prefixed with orgflow_news_. SSR-safe — returns defaults when window is unavailable.

import { getItem, setItem, removeItem, getJSON, setJSON } from '@ritik-singh/news-sdk'

getItem('theme')                    // Returns string | null (reads orgflow_news_theme)
setItem('theme', 'dark')            // Writes orgflow_news_theme = "dark"
removeItem('theme')                 // Deletes orgflow_news_theme

getJSON('bookmarked_ids')           // Returns parsed JSON | null
setJSON('bookmarked_ids', [1, 2])   // Writes JSON.stringify([1, 2])

Storage Keys

All localStorage keys are prefixed with orgflow_news_:

| Key | Description | |----------------------------------|---------------------------------------| | orgflow_news_theme | Current theme mode (light/dark) | | orgflow_news_visitor_uuid | Visitor UUID for engagement features | | orgflow_news_visitor_registered| Registration confirmed flag | | orgflow_news_bookmarked_ids | JSON array of bookmarked article IDs | | orgflow_news_poll_votes | JSON map of poll votes { pollId: [optionIds] } | | orgflow_news_subscribed_list | JSON array of subscription records | | orgflow_news_push_subscribed | Push subscription active flag | | orgflow_news_push_device_token | Push subscription endpoint | | orgflow_news_push_toast_dismissed | Timestamp when push toast was dismissed |


Build & Development

Scripts

npm run build           # Production build (tsup)
npm run dev             # Watch mode
npm run lint            # ESLint
npm run clean           # Remove dist/
npm run storybook       # Launch Storybook on port 6006
npm run build-storybook # Build static Storybook

Build output

The build produces these bundles:

| Output | Format | Description | |------------------------------|------------|-------------------------------------| | dist/index.js / .cjs | ESM + CJS | All components + hooks + core | | dist/hooks.js / .cjs | ESM + CJS | Hooks only (tree-shakeable) | | dist/components.js / .cjs| ESM + CJS | Components only (tree-shakeable) | | dist/server.js / .cjs | ESM + CJS | Server utilities (no "use client") | | dist/push-sw.js | ESM | Push notification service worker | | dist/styles.css | CSS | Theme CSS with variables + keyframes| | dist/*.d.ts | TypeScript | Type declarations |

Client bundles include "use client" banner for Next.js App Router compatibility. Server bundle does not. Service worker bundle is minified with no banner.


Package Exports

// Main entry — all components, hooks, utilities
import {
  // Components
  NewsApp, NewsProvider, HeaderBar, BreakingTicker, SearchOverlay,
  SubscribeModal, FeedPage, HeroSection, ArticleCard, ArticleGrid,
  CategoryChips, SortControls, Sidebar, TrendingList, TrendingTags,
  NewsletterCTA, CollectionsList, PushCTA, ArticleDetailPage,
  SectionRenderer, PollWidget, ShareModal, PreferencesPage,

  // Hooks
  useNewsContext, useArticles, useArticle, useCategories, useBreakingNews,
  useTags, useAuthors, useAuthor, useCollections, useCollection,
  useTracking, usePoll, useComments, useBookmarks, useReactions,
  useShare, useSubscription, useSubscriptionManager, usePushNotifications,
  useLinkTracking, useRecommendations, useSearchAnalytics, usePerformance,
  usePageMeta, useVisitor,

  // Core utilities
  createApiClient, createTheme, DEFAULT_THEME, resolveColors,
  parseRoute, createNavigationHelpers,

  // Storage utilities
  getItem, setItem, removeItem, getJSON, setJSON,

  // Standalone helpers
  isAlreadySubscribed, hasAnySubscription, getLastSubscribedEmail,
  isToastDismissed, dismissToast,
} from '@ritik-singh/news-sdk'

// Stylesheet — must be imported separately
import '@ritik-singh/news-sdk/styles.css'

// Hooks only (tree-shakeable)
import { useArticles, useCategories, usePoll } from '@ritik-singh/news-sdk/hooks'

// Components only (tree-shakeable)
import { ArticleCard, HeaderBar, PollWidget } from '@ritik-singh/news-sdk/components'

// Server utilities (for Next.js generateMetadata)
import { generateMetadata } from '@ritik-singh/news-sdk/server'

// Push service worker (for manual copy)
// Located at: @ritik-singh/news-sdk/push-sw

Browser Support

  • Chrome 80+
  • Firefox 78+
  • Safari 14+
  • Edge 80+

Push notifications require HTTPS and a browser that supports the Push API (all modern browsers except Safari on iOS < 16.4).


Configuration

The SDK requires only an apiKey prop. The baseUrl is optional and defaults to https://your-api.example.com/platform/news.

| Prop | Required | Default | Description | |------------|----------|--------------------------------------------|---------------------| | apiKey | Yes | — | Platform API key | | baseUrl | No | https://your-api.example.com/platform/news | API base URL | | basePath | No | /news | URL base path |


Complete Example

// app/news/[[...slug]]/page.jsx
import { NewsApp, createTheme } from '@ritik-singh/news-sdk'
import { generateMetadata as genMeta } from '@ritik-singh/news-sdk/server'
import '@ritik-singh/news-sdk/styles.css'

// Server-side SEO metadata
export async function generateMetadata({ params }) {
  const slug = (await params).slug
  return genMeta({
    slug: Array.isArray(slug) ? slug : slug ? [slug] : [],
    apiKey: 'pk_live_xxx',
    siteTitle: 'IREF News',
    titleTemplate: '%s | IREF News',
    feedTitle: 'Latest News',
    feedDescription: 'Read the latest shipping and commodities news',
    defaultOgImage: 'https://iref.net/og-news.png',
  })
}

// Custom theme
const theme = createTheme({
  colors: {
    accent: '#e11d48',
  },
  colorsDark: {
    accent: '#ff6b8a',
  },
  fonts: {
    serif: "'Playfair Display', Georgia, serif",
    sans: "'Inter', system-ui, sans-serif",
  },
  layout: {
    maxWidth: '1280px',
    cardStyle: 'shadow',
    articleGridColumns: 3,
  },
  header: {
    logoText: 'IREF News',
    logoColor: '#e11d48',
    showPushNotifications: true,
  },
  feed: {
    articlesPerPage: 12,
    showPushCTA: true,
    showPushToast: true,
    pushToastDelay: 30000,
  },
  article: {
    showComments: true,
    showReactions: true,
    showFontControls: true,
    relatedCount: 4,
  },
  subscribe: {
    newsletterTitle: 'IREF Daily Digest',
    newsletterDesc: 'Shipping, freight & commodities — every morning.',
  },
  meta: {
    siteTitle: 'IREF News',
    titleTemplate: '%s | IREF News',
    dynamicTitle: true,
  },
})

export default function NewsPage() {
  return (
    <NewsApp
      apiKey="pk_live_xxx"
      basePath="/news"
      router="app"
      theme={theme}
    />
  )
}

License

MIT