An embeddable, responsive news feed component with Instagram-style video playback, swipe gestures, and real-time comments. Built with React 19, TypeScript, and Vite.
- π± Instagram-style UI - Vertical scrolling feed with full-screen video viewer
- π₯ Multi-format media - Supports YouTube videos, MP4 files, and images
- π Touch-optimized - Swipe gestures for navigation (mobile-first)
- π¬ Auto-play - Videos play automatically when visible, pause when scrolled away
- π¬ Real-time comments - Fetch and post comments via Discourse integration
- π¨ Themeable - Respects parent page color schemes via CSS custom properties
- βΏ Accessibility-first - Full ARIA support, keyboard navigation, screen reader tested
- π RSS-powered - Parses standard RSS feeds with media enclosures
cd news-widget
npm install
npm run dev # Start dev server at http://localhost:5173
npm run build # Build for production
npm run preview # Preview production buildnpm test # Run Playwright E2E tests
npm run test:ui # Open Playwright UI
npm run test:headed # Run tests in headed modeInstall the widget as an NPM dependency:
npm install @mieweb/news-widget
# or
yarn add @mieweb/news-widget
# or
pnpm add @mieweb/news-widgetimport { NewsWidget } from '@mieweb/news-widget';
import '@mieweb/news-widget/style.css';
function App() {
return (
<div className="my-app">
<h1>Latest News</h1>
{/* Show landing page with all feeds */}
<NewsWidget />
{/* Or render a specific feed directly */}
<NewsWidget feedId="features" />
</div>
);
}import { renderNewsWidget } from '@mieweb/news-widget';
import '@mieweb/news-widget/style.css';
// Show landing page with all feeds
renderNewsWidget(document.getElementById('news-feed'));
// Or render a specific registered feed directly (no landing page)
renderNewsWidget(document.getElementById('news-feed'), { feedId: 'features' });import { useFeed, Feed, FeedCard } from '@mieweb/news-widget';
import '@mieweb/news-widget/style.css';
import type { Post } from '@mieweb/news-widget';
function CustomFeed() {
const { posts, loading, error } = useFeed('https://example.com/feed.rss');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{posts.map((post: Post) => (
<FeedCard key={post.id} post={post} />
))}
</div>
);
}Build the widget yourself for full control:
npm run buildThis creates optimized files in the dist/ folder:
dist/index.html- Main HTML entry pointdist/assets/*.js- JavaScript bundlesdist/assets/*.css- Stylesheets
Copy the dist/ folder to your web server and embed with an iframe:
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<style>
/* Make iframe responsive and full-height */
.news-widget-container {
width: 100%;
height: 600px; /* Or use 100vh for full viewport */
border: none;
overflow: hidden;
}
</style>
</head>
<body>
<h1>Latest News</h1>
<!-- Embed the news widget -->
<iframe
src="/dist/index.html"
class="news-widget-container"
title="News Feed Widget"
sandbox="allow-scripts allow-same-origin allow-popups"
></iframe>
</body>
</html>To show a specific feed without the landing page, use the IIFE build with feedId:
<!-- widget.html -->
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="news-widget.iife.js"></script>
<script>
var params = new URLSearchParams(window.location.search);
var feedId = params.get('feedId');
NewsWidget.renderNewsWidget(
document.getElementById('root'),
feedId ? { feedId: feedId } : {}
);
</script>
</body>
</html>Then embed with a query parameter to select the feed:
<!-- Enterprise Health feeds (pre-registered) -->
<iframe src="widget.html?feedId=features" title="Features Feed"></iframe>
<iframe src="widget.html?feedId=testing" title="Testing Feed"></iframe>
<iframe src="widget.html?feedId=public" title="Public Feed"></iframe>Use registerFeed() to add a custom feed at runtime before rendering:
<!-- custom-widget.html -->
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="news-widget.iife.js"></script>
<script>
var params = new URLSearchParams(window.location.search);
var feedUrl = params.get('feed');
var feedName = params.get('name') || 'News';
if (feedUrl) {
NewsWidget.registerFeed({
id: 'custom',
name: feedName,
url: feedUrl,
description: '',
emoji: 'π°',
capabilities: { supportsLikes: true, supportsComments: true }
});
}
NewsWidget.renderNewsWidget(
document.getElementById('root'),
feedUrl ? { feedId: 'custom' } : {}
);
</script>
</body>
</html><iframe src="custom-widget.html?feed=https://example.com/feed.rss&name=My+Feed"></iframe>For tighter integration, include the built assets directly:
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<!-- Include widget styles -->
<link rel="stylesheet" href="/dist/assets/index-[hash].css">
</head>
<body>
<!-- Widget mounts here -->
<div id="root"></div>
<!-- Include widget JavaScript -->
<script type="module" src="/dist/assets/index-[hash].js"></script>
</body>
</html>Note: Replace
[hash]with the actual hash from your build output.
Use a CDN for quick prototyping without installation:
<link rel="stylesheet" href="https://unpkg.com/@mieweb/news-widget/style.css">
<script type="module">
import { renderNewsWidget } from 'https://unpkg.com/@mieweb/news-widget';
renderNewsWidget(document.getElementById('news-feed'));
</script>The widget uses the @mieweb/ui design system with Tailwind CSS 4 and CSS custom properties for theming. The default brand is BlueHive.
Theming is handled via @mieweb/ui brand CSS files imported in src/index.css:
@import '@mieweb/ui/brands/bluehive.css' layer(theme);
@import 'tailwindcss';All component colors use var(--mieweb-*) CSS custom properties, which are mapped from the brand's Tailwind color tokens in the @theme block.
Override the CSS custom properties on your parent page:
<style>
:root {
/* Primary brand colors */
--mieweb-primary-500: #0066cc;
--mieweb-primary-600: #0055aa;
/* Semantic tokens */
--mieweb-background: #ffffff;
--mieweb-foreground: #333333;
--mieweb-card: #f9f9f9;
--mieweb-border: #e0e0e0;
--mieweb-muted-foreground: #666666;
}
</style>Dark mode is activated via data-theme="dark" attribute on a parent element:
<div data-theme="dark">
<!-- Widget renders in dark mode -->
</div>Or override CSS variables for dark mode:
<style>
@media (prefers-color-scheme: dark) {
:root {
--mieweb-background: #0a0a0a;
--mieweb-foreground: #fafafa;
--mieweb-card: #1a1a1a;
--mieweb-border: #2a2a2a;
--mieweb-muted-foreground: #a0a0a0;
}
}
</style>| Property | Default (Light) | Purpose |
|---|---|---|
--mieweb-background |
#fafafa |
Main background color |
--mieweb-foreground |
#0a0a0a |
Primary text color |
--mieweb-card |
#ffffff |
Card/container background |
--mieweb-border |
#e5e7eb |
Border and divider color |
--mieweb-muted-foreground |
#737373 |
Secondary/muted text |
--mieweb-primary-500 |
#3b82f6 |
Primary accent (links, buttons) |
--mieweb-destructive-500 |
#ef4444 |
Error/destructive actions |
--mieweb-success-500 |
#22c55e |
Success indicators |
--mieweb-ring |
#3b82f6 |
Focus ring color |
See src/index.css for the full list of color scale variables (--mieweb-primary-50 through --mieweb-primary-950, etc.).
Match your brand colors:
<style>
:root {
--mieweb-background: var(--your-site-bg, #f5f5f5);
--mieweb-primary-500: var(--your-brand-primary, #ff6b35);
--mieweb-foreground: var(--your-site-text, #2d3748);
}
</style>Feeds can be configured statically in src/data/feedRegistry.ts, or registered at runtime.
Add feeds to the FEED_SECTIONS array in feedRegistry.ts:
{
id: 'my-feed',
name: 'My News Feed',
description: 'Latest updates',
url: 'https://example.com/feed.rss',
emoji: 'π°',
capabilities: { supportsLikes: true, supportsComments: true },
}Use registerFeed() to add or override feeds before rendering β useful for iframe embedding or dynamic configuration:
import { registerFeed, renderNewsWidget } from '@mieweb/news-widget';
registerFeed({
id: 'custom',
name: 'Custom Feed',
description: 'Dynamically registered',
url: 'https://example.com/feed.rss',
emoji: 'π°',
capabilities: { supportsLikes: true, supportsComments: true },
});
renderNewsWidget(document.getElementById('root'), { feedId: 'custom' });| Feed ID | Name | Source |
|---|---|---|
features |
Features | Enterprise Health product announcements |
testing |
Test | Enterprise Health testing discussions |
public |
Public | Enterprise Health public community |
test-server |
Test Server | Local development test server |
sample |
Sample Feed | Built-in demo content |
For development, configure CORS proxies in vite.config.ts:
server: {
proxy: {
'/api/rss': {
target: 'https://your-discourse-instance.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/rss/, ''),
},
},
}The widget is mobile-first with touch gestures:
- Swipe up/down - Navigate between posts
- Tap video - Toggle play/pause
- Tap muted icon - Unmute audio
- Tap comment icon - Open comment panel
- Tap outside - Close comment panel
/* Mobile: default styles */
/* Tablet: 768px+ */
/* Desktop: 1024px+ */Built with WCAG 2.1 AA compliance:
- β Full keyboard navigation (Tab, Enter, Escape)
- β ARIA labels on all interactive elements
- β Screen reader tested (VoiceOver, NVDA)
- β Focus indicators on all controls
- β Semantic HTML structure
- β Color contrast meets AA standards
| Key | Action |
|---|---|
Tab |
Navigate between elements |
Enter / Space |
Activate buttons/links |
Escape |
Close comment panel or fullscreen viewer |
Arrow Up/Down |
Scroll feed (when focused) |
Playwright E2E tests verify:
- Video playback and autoplay
- Comment posting and syncing
- Like/unlike functionality
- Swipe gesture navigation
- Fullscreen viewer interactions
- Accessibility (ARIA roles, keyboard nav)
npm test # Run all tests
npm run test:ui # Interactive test UI
npm run test:headed # See tests in browsernews-widget/
βββ src/
β βββ components/ # React components (using @mieweb/ui)
β β βββ Feed.tsx # Main feed container
β β βββ FeedCard.tsx # Individual post card (Card, CardHeader, CardActions)
β β βββ FullscreenViewer.tsx # Fullscreen video viewer (dialog)
β β βββ CommentsPanel.tsx # Comment sidebar (Input, Button)
β β βββ Avatar.tsx # User avatar (wraps @mieweb/ui Avatar)
β β βββ ClickTooltip.tsx # Tooltip trigger (wraps @mieweb/ui Tooltip)
β β βββ LandingPage.tsx # Feed selection landing page (Card, Tooltip)
β βββ hooks/ # Custom React hooks
β β βββ useFeed.ts # RSS feed fetching/parsing
β β βββ useComments.ts # Comment state management
β β βββ useVisibility.ts # IntersectionObserver for autoplay
β β βββ useRouter.ts # URL routing
β β βββ useDiscourseAuth.ts # Authentication
β βββ types/ # TypeScript interfaces
β βββ data/ # Feed configuration
βββ test-server/ # Development test server
When embedding via iframe, use appropriate sandbox attributes:
<iframe
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
src="/dist/index.html">
</iframe>allow-scripts- Required for JavaScript executionallow-same-origin- Required for API calls to parent domainallow-popups- For external linksallow-forms- For comment submission
The IIFE build injects CSS at runtime by creating a <style> element via JavaScript. This requires the style-src 'unsafe-inline' directive (or a matching nonce/hash) in the page's Content Security Policy. If your site uses a strict CSP that disallows inline styles, the IIFE bundle's CSS will be blocked.
For environments with strict CSP, use the ES or UMD build with the separate dist/news-widget.css stylesheet instead.
[Add your license here]
See project copilot-instructions.md for code quality guidelines.
- React 19 - Latest React with concurrent features
- TypeScript 5.9 - Type-safe development
- Vite 7 - Fast build tool and dev server
- @mieweb/ui - MIE design system components (Button, Card, Avatar, Tooltip, Input, Alert, etc.)
- Tailwind CSS 4 - Utility-first CSS framework (via
@tailwindcss/viteplugin) - lucide-react - SVG icon library (consistent with @mieweb/ui)
- react-player v3 - Multi-format video playback
- react-swipeable - Touch gesture handling
- Playwright - E2E testing
npm run dev # Start dev server (http://localhost:5173)
npm run build # Production build β dist/ (for standalone app)
npm run build:lib # Library build β dist/ (for NPM package)
npm run preview # Preview production build
npm run lint # Run ESLint
npm test # Run Playwright tests
npm run test:ui # Open Playwright UI for debuggingTo build the library version for NPM distribution:
npm run build:libThis generates:
dist/news-widget.js- ES moduledist/news-widget.umd.cjs- UMD module (browser globals)dist/news-widget.iife.js- Standalone IIFE bundle (all CSS inlined)dist/news-widget.css- Compiled styles (for ES/UMD consumers)dist/index.d.ts- TypeScript declarations
The IIFE build injects all CSS (Tailwind, component styles, @mieweb/ui) into the page at runtime via a <style> tag, so no separate stylesheet is needed β just a single <script> tag.
Automated Publishing (Recommended)
Create a GitHub Release and the package is automatically published via GitHub Actions:
graph LR
updateVersion[Update Version] --> commitPush[Commit & Push]
commitPush --> createRelease[Create GitHub Release]
createRelease --> githubActions{GitHub Actions}
githubActions --> runTests[Run Tests]
githubActions --> runLinter[Run Linter]
githubActions --> buildLibrary[Build Library]
runTests --> checkPass{All Pass?}
runLinter --> checkPass
buildLibrary --> checkPass
checkPass -->|Yes| publishNPM[Publish to NPM]
checkPass -->|No| failed[β Failed]
publishNPM --> published[β
Published]
Steps:
- Update version in package.json (
npm version patch/minor/major) - Commit and push the version bump
- Create a GitHub Release with tag
vX.Y.Z - GitHub Actions workflow automatically runs and publishes to NPM
Manual Publishing
# Test the package locally first
npm run build:lib
npm pack
# Publish to NPM (requires auth)
npm login
npm publish --access publicResources:
- π§ EXAMPLES.md - Usage examples
This project follows strict quality guidelines:
- DRY principle - No code duplication
- KISS principle - Simplest solution that works
- Accessibility-first - ARIA labels, keyboard navigation
- Test-driven - E2E tests for all features
- Type-safe - Full TypeScript coverage
See .github/copilot-instructions.md for complete guidelines.
The project uses flat config ESLint 9 with TypeScript support. To enable stricter type-aware rules:
// eslint.config.js
import tseslint from 'typescript-eslint';
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
tseslint.configs.recommendedTypeChecked,
// or tseslint.configs.strictTypeChecked for stricter rules
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
])