A modern, production-grade demo app built with Next.js 15 (App Router), React 19, TypeScript, and Tailwind CSS.
It fetches and displays paginated Hacker News stories, with author details, comments, dark mode, robust testing, and strict code quality enforcement.
Try it out: https://nextjs15-demo-alpha.vercel.app/
- Next.js 15 App Router with React Server Components (RSC) and Client Components
- TypeScript throughout for type safety
- Tailwind CSS for styling and dark mode (all styling via utility classes in
className
) - API proxying: All HN API requests go through your Next.js backend
- Pagination: Browse stories with accessible, robust pagination controls
- Link Previews: Rich previews for story URLs (image, title, description, logo) using a pure JS Cheerio-based scraper
- Client-side image handling: All external images use a custom
ClientImage
(Next.js<Image />
with custom loader, no domain restrictions) - Author details: View karma and account creation date for each story's author
- Comments system: Full comment threads with lazy-loading nested comments and safe HTML rendering
- Safe HTML rendering: All user-generated content is sanitized with DOMPurify to prevent XSS attacks
- Accessibility: Keyboard navigation, ARIA labels, and semantic HTML
- Testing: Jest, React Testing Library, and code coverage
- Prettier, ESLint, Husky, lint-staged: Enforced code style and pre-commit checks
- Scroll restoration: When returning from story detail to the list, your scroll position is restored
git clone git@github.com:mickael-h/nextjs15-demo.git
cd nextjs15-demo
npm install
Create a .env.local
file in the project root with the following:
# The base URL for the official Hacker News API (must end with a slash)
HN_API_URL=https://hacker-news.firebaseio.com/
# The public site URL for client-side fetches (no trailing slash)
NEXT_PUBLIC_SITE_URL=http://localhost:3000
HN_API_URL
is used by server-side API routes to fetch HN data.NEXT_PUBLIC_SITE_URL
is used by client-side code and hooks to call your own API endpoints.
npm run dev
Visit http://localhost:3000.
- Run all tests:
npm test
- Check code coverage:
npm test -- --coverage
- Lint and format code:
npm run lint npm run format
- Check type validity:
npm run typecheck
- Husky and lint-staged are set up to run Prettier and ESLint on staged files before every commit.
- The
.husky/pre-commit
file contains the commands to run (nohusky.sh
):npx lint-staged npm run lint npm run typecheck npm test
- If you add new hooks, just add your commands directly to the relevant
.husky/*
file.
- The
.prettierignore
file ensures Prettier does not format:.next
(build output)node_modules
(dependencies)- All config files matching
*.config.*
(e.g.,next.config.js
,tailwind.config.js
) - All files matching
*.rc
(e.g.,.eslintrc
,.prettierrc
)
src/app/
— Next.js App Router, API routes, pages, and layoutsrc/components/
— UI components (StoryList, StoryCard, StoryDetail, LinkPreview, ClientImage, Comment, CommentsList, etc.)src/hooks/
— Custom React hooks (useAuthor, useComments, useNestedComments)src/lib/
— Pure backend logic (for HN API fetchers, link preview scraping, easily unit tested)
- All requests to the Hacker News API are proxied through
/api/hn/top20
,/api/hn/user/[username]
,/api/hn/comments/[storyId]
,/api/hn/comments/nested
, and/api/preview
routes. - This enables error handling and lets you add business logic to requests (like sorting, pagination, or scraping).
- StoryList: Handles list/detail state, renders
StoryCard
andStoryDetail
, preserves scroll position when toggling views. - StoryCard: Clickable, accessible card for each story.
- StoryDetail: Shows full story info, author details, and comments. Fetches author data via
useAuthor
and comments viauseComments
. - Comment: Individual comment component with support for nested replies and safe HTML rendering.
- CommentsList: Manages the list of comments with loading states and error handling.
- LinkPreview: Shows a rich preview for story URLs, using a Cheerio-based pure JS scraper and client-side image handling.
- ClientImage: Renders all external images using Next.js
<Image />
with a custom loader andunoptimized
, bypassing domain restrictions. - useAuthor: Custom hook for fetching and caching author info, using the public API route.
- useComments: Custom hook for fetching story comments with SWR caching.
- useNestedComments: Custom hook for lazy-loading nested comments on-demand.
- Lazy Loading: Comments are only fetched when the user clicks "show X replies" to improve performance
- Nested Comments: Unlimited depth, loaded on-demand as users expand threads
- Safe HTML Rendering: All comment content is sanitized with DOMPurify to prevent XSS attacks
- Skeleton Loading: Beautiful skeleton loaders while comments are being fetched
- Error Handling: Graceful error display if comments fail to load
- SWR Integration: Efficient caching and data fetching with SWR
- Collapsible Threads: All nested comments start collapsed for better UX
- The app supports paginated stories, with accessible, robust pagination controls.
- Pagination state is reflected in the URL for shareability and navigation.
- Story details show a rich link preview (image, title, description, logo) using a pure JS Cheerio-based scraper (no native dependencies).
- All preview logic is in
src/lib/preview.ts
and proxied via/api/preview
. - All images and logos are resolved to absolute URLs, so previews work even if the source site uses relative paths.
- All images are rendered client-side with
ClientImage
(Next.js<Image />
with custom loader andunoptimized
), so there are no domain restrictions and no<img>
tags.
- SWR is used for client-side data fetching with automatic caching.
- Automatic refetching on window focus is disabled for a stable UX.
- Comments are cached for 1 minute to avoid unnecessary refetches.
- DOMPurify: All user-generated HTML content (comments, story text) is sanitized to prevent XSS attacks
- Safe HTML Rendering: Uses
dangerouslySetInnerHTML
only with sanitized content - Input Validation: All API inputs are validated and sanitized
- All business logic is extracted to pure functions/modules for easy unit testing.
- Hooks are tested with real fetch mocking, not just stubs.
- UI is tested with React Testing Library for realistic user flows.
- Comments system is thoroughly tested with edge cases and error scenarios.
- Prettier and ESLint enforce consistent style and catch errors.
- Husky and lint-staged ensure only well-formatted, linted code is committed.
- TypeScript is used everywhere for safety and maintainability.
- DOMPurify ensures safe HTML rendering throughout the application.
- Fork the repo and create your branch.
- Make your changes (with tests!).
- Ensure all tests, lint, and type checks pass.
- Submit a pull request.
Questions or feedback?
Open an issue or start a discussion!
Ideas for improvement
- Integrate the rest of the API.
- Add integration tests for components.
- A few Playwright end-to-end tests.
- Add Storybook and SonarCloud.