News System Implementation Guide
Technical implementation details for integrating the Ring Platform News System
ποΈ Architecture Overviewβ
The Ring Platform News System is built with a modern, scalable architecture that integrates seamlessly with the notification system and provides robust content management capabilities.
System Componentsβ
Data Flowβ
- Content Creation: Admin/Moderator creates news article
- Validation: Content validation and moderation
- Storage: Article saved to Firestore with metadata
- Notification Trigger: Automatic notification to subscribers
- Delivery: Multi-channel notification delivery
- Engagement: User interactions (likes, comments, shares)
- Analytics: Engagement data collection and analysis
π Database Schemaβ
News Articles Collectionβ
// Firestore: /news/{articleId}
interface NewsArticle {
id: string;
title: string;
slug: string;
content: string;
excerpt: string;
// Author information
authorId: string;
authorName: string;
authorAvatar?: string;
// Classification
categoryId: string;
tags: string[];
// Publishing
status: 'draft' | 'published' | 'archived' | 'scheduled';
publishedAt?: Timestamp;
scheduledFor?: Timestamp;
// Engagement metrics
views: number;
likes: number;
comments: number;
shares: number;
// Media
featuredImage?: string;
galleryImages?: string[];
// Metadata
createdAt: Timestamp;
updatedAt: Timestamp;
locale: 'en' | 'uk';
// SEO
metaTitle?: string;
metaDescription?: string;
keywords?: string[];
}
News Categories Collectionβ
// Firestore: /newsCategories/{categoryId}
interface NewsCategory {
id: string;
name: string;
nameUk: string; // Ukrainian translation
description: string;
descriptionUk: string;
slug: string;
// Visual
color: string;
icon: string;
coverImage?: string;
// Hierarchy
parentId?: string;
children?: string[];
// Settings
isActive: boolean;
sortOrder: number;
// Metadata
createdAt: Timestamp;
updatedAt: Timestamp;
// Statistics
articleCount: number;
subscriberCount: number;
}
News Engagement Collectionβ
// Firestore: /newsEngagement/{engagementId}
interface NewsEngagement {
id: string;
newsId: string;
userId: string;
type: 'like' | 'view' | 'share' | 'bookmark';
// Metadata
createdAt: Timestamp;
ipAddress?: string;
userAgent?: string;
// Share-specific data
shareChannel?: 'twitter' | 'linkedin' | 'facebook' | 'internal' | 'copy';
referrerUrl?: string;
}
π§ API Implementationβ
Core News Endpointsβ
GET /api/news-listβ
Fetch paginated news articles with filtering and sorting options.
// Handler implementation
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const limit = parseInt(searchParams.get('limit') || '20');
const category = searchParams.get('category');
const author = searchParams.get('author');
const startAfter = searchParams.get('startAfter');
const locale = searchParams.get('locale') || 'en';
try {
let query = db.collection('news')
.where('status', '==', 'published')
.where('locale', '==', locale)
.orderBy('publishedAt', 'desc')
.limit(limit);
if (category) {
query = query.where('categoryId', '==', category);
}
if (author) {
query = query.where('authorId', '==', author);
}
if (startAfter) {
const startAfterDoc = await db.collection('news').doc(startAfter).get();
query = query.startAfter(startAfterDoc);
}
const snapshot = await query.get();
const articles = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
publishedAt: doc.data().publishedAt?.toDate(),
createdAt: doc.data().createdAt?.toDate(),
updatedAt: doc.data().updatedAt?.toDate()
}));
return NextResponse.json({
articles,
hasMore: snapshot.docs.length === limit,
lastVisible: snapshot.docs[snapshot.docs.length - 1]?.id
});
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch news articles' },
{ status: 500 }
);
}
}
GET /api/news-by-id/[id]β
Fetch specific article with engagement data.
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const { id } = params;
try {
// Get article
const articleDoc = await db.collection('news').doc(id).get();
if (!articleDoc.exists) {
return NextResponse.json(
{ error: 'Article not found' },
{ status: 404 }
);
}
const article = {
id: articleDoc.id,
...articleDoc.data(),
publishedAt: articleDoc.data()?.publishedAt?.toDate(),
createdAt: articleDoc.data()?.createdAt?.toDate(),
updatedAt: articleDoc.data()?.updatedAt?.toDate()
};
// Increment view count
await articleDoc.ref.update({
views: FieldValue.increment(1)
});
// Get category information
const categoryDoc = await db.collection('newsCategories')
.doc(article.categoryId).get();
const category = categoryDoc.exists ? {
id: categoryDoc.id,
...categoryDoc.data()
} : null;
return NextResponse.json({
article,
category
});
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch article' },
{ status: 500 }
);
}
}
POST /api/news-likesβ
Handle article like/unlike actions with notification integration.
export async function POST(request: Request) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { newsId, action } = await request.json();
if (!newsId || !['like', 'unlike'].includes(action)) {
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
}
try {
const userId = session.user.id;
const engagementId = `${userId}_${newsId}_like`;
const engagementRef = db.collection('newsEngagement').doc(engagementId);
const newsRef = db.collection('news').doc(newsId);
if (action === 'like') {
// Check if already liked
const existingLike = await engagementRef.get();
if (existingLike.exists) {
return NextResponse.json({ error: 'Already liked' }, { status: 400 });
}
// Create like engagement
await engagementRef.set({
id: engagementId,
newsId,
userId,
type: 'like',
createdAt: FieldValue.serverTimestamp()
});
// Increment like count
await newsRef.update({
likes: FieldValue.increment(1)
});
// Get article for notification
const articleDoc = await newsRef.get();
const article = articleDoc.data();
// Trigger notification to author (if not self-like)
if (article?.authorId !== userId) {
await createNotification({
userId: article.authorId,
type: NotificationType.NEWS_LIKED,
title: 'Article Liked! π',
body: `Someone liked your article "${article.title}"`,
data: {
newsId,
likerUserId: userId,
articleTitle: article.title
},
actionUrl: `/news/${newsId}`,
priority: NotificationPriority.LOW
});
}
} else {
// Remove like
await engagementRef.delete();
await newsRef.update({
likes: FieldValue.increment(-1)
});
}
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to process like action' },
{ status: 500 }
);
}
}
News Categories APIβ
// GET /api/news-categories
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const locale = searchParams.get('locale') || 'en';
try {
const snapshot = await db.collection('newsCategories')
.where('isActive', '==', true)
.orderBy('sortOrder', 'asc')
.get();
const categories = snapshot.docs.map(doc => {
const data = doc.data();
return {
id: doc.id,
name: locale === 'uk' ? data.nameUk || data.name : data.name,
description: locale === 'uk' ? data.descriptionUk || data.description : data.description,
slug: data.slug,
color: data.color,
icon: data.icon,
articleCount: data.articleCount || 0,
subscriberCount: data.subscriberCount || 0
};
});
return NextResponse.json({ categories });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch categories' },
{ status: 500 }
);
}
}
π Notification Integrationβ
News Notification Triggersβ
The news system integrates with the notification service to automatically notify users of relevant content:
// News notification triggers
export async function notifyNewsPublished(
articleId: string,
articleTitle: string,
authorId: string,
categoryId: string
) {
try {
// Get category subscribers
const subscribers = await getSubscribersByCategory(categoryId);
// Filter out the author
const targetUsers = subscribers.filter(userId => userId !== authorId);
if (targetUsers.length > 0) {
await createNotification({
userIds: targetUsers,
type: NotificationType.NEWS_PUBLISHED,
title: 'New Article Published π°',
body: `Check out: "${articleTitle}"`,
data: {
newsId: articleId,
categoryId,
authorId,
articleTitle
},
actionUrl: `/news/${articleId}`,
priority: NotificationPriority.NORMAL,
channels: [
NotificationChannel.IN_APP,
NotificationChannel.EMAIL
]
});
}
} catch (error) {
console.error('Failed to send news publication notification:', error);
}
}
export async function notifyNewsEngagementMilestone(
articleId: string,
articleTitle: string,
authorId: string,
milestone: number,
type: 'likes' | 'comments' | 'views'
) {
const milestoneText = {
likes: `${milestone} likes`,
comments: `${milestone} comments`,
views: `${milestone} views`
};
await createNotification({
userId: authorId,
type: NotificationType.NEWS_MILESTONE,
title: `π Article Milestone Reached!`,
body: `Your article "${articleTitle}" has reached ${milestoneText[type]}!`,
data: {
newsId: articleId,
milestone,
milestoneType: type,
articleTitle
},
actionUrl: `/news/${articleId}`,
priority: NotificationPriority.NORMAL
});
}
Notification Preferences Integrationβ
// News-specific notification preferences
interface NewsNotificationPreferences {
enabled: boolean;
categories: {
[categoryId: string]: {
enabled: boolean;
frequency: 'immediate' | 'daily' | 'weekly';
channels: NotificationChannel[];
};
};
engagementNotifications: {
likes: boolean;
comments: boolean;
milestones: boolean;
};
digestFrequency: 'daily' | 'weekly' | 'monthly' | 'disabled';
}
π± Frontend Integrationβ
React Hook for News Dataβ
// hooks/use-news.ts
export function useNews(options: {
category?: string;
limit?: number;
locale?: string;
}) {
const [articles, setArticles] = useState<NewsArticle[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [lastVisible, setLastVisible] = useState<string>();
const fetchNews = useCallback(async (reset = false) => {
setLoading(true);
try {
const params = new URLSearchParams({
limit: String(options.limit || 20),
...(options.category && { category: options.category }),
...(options.locale && { locale: options.locale }),
...(lastVisible && !reset && { startAfter: lastVisible })
});
const response = await fetch(`/api/news-list?${params}`);
const data = await response.json();
if (reset) {
setArticles(data.articles);
} else {
setArticles(prev => [...prev, ...data.articles]);
}
setHasMore(data.hasMore);
setLastVisible(data.lastVisible);
} catch (error) {
console.error('Failed to fetch news:', error);
} finally {
setLoading(false);
}
}, [options.category, options.limit, options.locale, lastVisible]);
const likeArticle = useCallback(async (newsId: string) => {
try {
await fetch('/api/news-likes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newsId, action: 'like' })
});
// Optimistically update local state
setArticles(prev => prev.map(article =>
article.id === newsId
? { ...article, likes: article.likes + 1 }
: article
));
} catch (error) {
console.error('Failed to like article:', error);
}
}, []);
return {
articles,
loading,
hasMore,
fetchNews,
likeArticle,
refreshNews: () => fetchNews(true),
loadMore: () => fetchNews(false)
};
}
News Feed Componentβ
// components/news/news-feed.tsx
export function NewsFeed({ category }: { category?: string }) {
const { articles, loading, hasMore, loadMore, likeArticle } = useNews({
category,
limit: 10
});
return (
<div className="news-feed space-y-6">
{articles.map((article) => (
<NewsCard
key={article.id}
article={article}
onLike={likeArticle}
/>
))}
{hasMore && (
<LoadMoreButton
onClick={loadMore}
loading={loading}
/>
)}
</div>
);
}
π Performance Optimizationsβ
Caching Strategyβ
// Implement caching for frequently accessed data
const CACHE_DURATION = {
ARTICLES: 300, // 5 minutes
CATEGORIES: 3600, // 1 hour
USER_ENGAGEMENT: 60 // 1 minute
};
// Redis cache implementation
export async function getCachedArticles(cacheKey: string) {
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
return null;
}
export async function setCachedArticles(
cacheKey: string,
data: any,
ttl: number
) {
await redis.setex(cacheKey, ttl, JSON.stringify(data));
}
Database Indexingβ
// Firestore indexes for optimal query performance
const FIRESTORE_INDEXES = [
// News articles
{
collection: 'news',
fields: [
{ field: 'status', order: 'ASCENDING' },
{ field: 'locale', order: 'ASCENDING' },
{ field: 'publishedAt', order: 'DESCENDING' }
]
},
{
collection: 'news',
fields: [
{ field: 'categoryId', order: 'ASCENDING' },
{ field: 'status', order: 'ASCENDING' },
{ field: 'publishedAt', order: 'DESCENDING' }
]
},
// News engagement
{
collection: 'newsEngagement',
fields: [
{ field: 'newsId', order: 'ASCENDING' },
{ field: 'type', order: 'ASCENDING' },
{ field: 'createdAt', order: 'DESCENDING' }
]
}
];
π Security & Validationβ
Input Validationβ
// Validation schemas for news operations
const CreateNewsSchema = z.object({
title: z.string().min(10).max(200),
content: z.string().min(100),
excerpt: z.string().max(300),
categoryId: z.string().uuid(),
tags: z.array(z.string()).max(10),
featuredImage: z.string().url().optional(),
status: z.enum(['draft', 'published', 'scheduled']),
scheduledFor: z.date().optional(),
locale: z.enum(['en', 'uk'])
});
const LikeNewsSchema = z.object({
newsId: z.string().uuid(),
action: z.enum(['like', 'unlike'])
});
Authorization Rulesβ
// Role-based access control for news operations
export function canCreateNews(userRole: UserRole): boolean {
return [UserRole.ADMIN, UserRole.MODERATOR].includes(userRole);
}
export function canEditNews(
userRole: UserRole,
authorId: string,
userId: string
): boolean {
return userRole === UserRole.ADMIN ||
(userRole === UserRole.MODERATOR && authorId === userId);
}
export function canDeleteNews(
userRole: UserRole,
authorId: string,
userId: string
): boolean {
return userRole === UserRole.ADMIN || authorId === userId;
}
Implementation Status: β Production Ready | Performance: β Optimized | Security: β Secured
This implementation guide provides a complete foundation for building and extending the Ring Platform News System with robust notification integration and scalable architecture.