@ritik-singh/news-sdk
v0.1.2
Published
Embeddable news module SDK for OrgFlow platforms — articles, categories, tracking, polls, comments, subscriptions
Maintainers
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
- Quick Start
- Push Notifications Setup
- Server-Side Metadata (SEO)
- Theme Customization
- Components
- 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
- API Client
- Routing
- CSS Variables
- Storage
- Build & Development
- Package Exports
- Browser Support
- Configuration
- Complete Example
- License
Installation
npm install @ritik-singh/news-sdk
# or
pnpm add @ritik-singh/news-sdk
# or
yarn add @ritik-singh/news-sdkPeer 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 initThis copies push-sw.js to public/news-push-sw.js.
2. How it works
- User clicks the bell icon or subscribes via the push CTA card
- SDK requests browser notification permission
- Registers the service worker and subscribes via the Web Push API (VAPID)
- Sends the subscription endpoint + keys to your backend
- Backend sends push payloads → service worker shows native notifications
- 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 colorsresolveColors() 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 handlerFeedPage
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_viewon 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_exiton unmount/beforeunload withfetch keepalive: truefor reliable delivery; falls back tonavigator.sendBeaconif fetch fails- Sends
exit_type: "navigate_away"andengaged_time_secondswith the exit event - Live reader count refreshes every 60 seconds (reads
data.countfrom 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:
- Check browser support (ServiceWorker + PushManager + Notification)
- Request
Notification.requestPermission()— returns false if denied - Register service worker from
theme.push.serviceWorkerPath - Check if browser already has a push subscription (dedup)
- If not, fetch VAPID public key from backend (cached 1 hour)
- Subscribe via
PushManager.subscribe()with VAPID key - Extract
endpoint,auth_key,p256dh_keyfrom subscription - Send to backend via
POST /push-subscriptions - 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
nullif registration fails (hooks that depend onvisitorUuidgracefully 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 keyRequest 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%20ratesCSS 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 StorybookBuild 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-swBrowser 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
