fix: QA issues #28-35 — SEO titles, favicon, accessibility, security headers#38
fix: QA issues #28-35 — SEO titles, favicon, accessibility, security headers#38
Conversation
#30: Add favicon.ico (Next.js app router convention) #32: Change X-Frame-Options from DENY to SAMEORIGIN (match nginx, avoid duplicate) #34: Enable pinch-to-zoom (userScalable: true, maximumScale: 5) — WCAG 2.1 compliance #35: Add page-specific <title> tags via layout.tsx for movies, tvshows, music, books, search, find-torrents, trending, dht #31: Add reason message on login page when redirected from live-tv #28, #29: Browse API works without auth — category pages render content for unauthenticated users (verified). Search pages require a query parameter by design. #33: Trending page depends on TheTVDB API — separate investigation needed.
There was a problem hiding this comment.
Pull request overview
This PR addresses a set of QA-reported SEO/accessibility/header/polish issues in the Next.js App Router app by adding per-route metadata, fixing the /favicon.ico 404, adjusting viewport zoom settings, and improving the Live TV login redirect UX.
Changes:
- Added route-segment
layout.tsxfiles to provide unique page metadata (titles/descriptions) for several top-level routes. - Added
src/app/favicon.icoto satisfy Next.js App Router favicon convention and prevent 404s. - Updated security headers and viewport settings, and added a login banner for the
/live-tvauth redirect reason.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
next.config.ts |
Changes X-Frame-Options header value to address duplicate/conflicting headers. |
src/app/layout.tsx |
Updates viewport to allow user zoom (accessibility). |
src/app/favicon.ico |
Adds favicon asset so /favicon.ico no longer 404s. |
src/app/live-tv/page.tsx |
Adds reason=live-tv to login redirect. |
src/app/login/page.tsx |
Displays an explanatory banner when redirected from Live TV. |
src/app/movies/layout.tsx |
Adds segment metadata for Movies route. |
src/app/music/layout.tsx |
Adds segment metadata for Music route. |
src/app/tvshows/layout.tsx |
Adds segment metadata for TV Shows route. |
src/app/books/layout.tsx |
Adds segment metadata for Books route. |
src/app/search/layout.tsx |
Adds segment metadata for Search route. |
src/app/find-torrents/layout.tsx |
Adds segment metadata for Find Torrents route. |
src/app/dht/layout.tsx |
Adds segment metadata for DHT route. |
src/app/trending/layout.tsx |
Adds segment metadata for Trending route. |
src/app/movies/page.tsx |
Minor formatting-only change (blank line). |
src/app/music/page.tsx |
Minor formatting-only change (blank line). |
src/app/tvshows/page.tsx |
Minor formatting-only change (blank line). |
src/app/books/page.tsx |
Minor formatting-only change (blank line). |
src/app/search/page.tsx |
Minor formatting-only change (blank line). |
src/app/find-torrents/page.tsx |
Minor formatting-only change (blank line). |
src/app/dht/page.tsx |
Minor formatting-only change (blank line). |
src/app/trending/page.tsx |
Minor formatting-only change (blank line). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/app/find-torrents/layout.tsx
Outdated
| import type { Metadata } from 'next'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'Find Torrents — BitTorrented', |
There was a problem hiding this comment.
These segment titles include the site name ("— BitTorrented"), but the root metadata defines a title.template of %s | BitTorrented (src/app/layout.tsx). Next.js will apply that template to nested string titles, producing a duplicated site name (e.g., "Find Torrents — BitTorrented | BitTorrented"). Use a bare segment title (e.g., "Find Torrents") or set an absolute title to bypass the template.
| title: 'Find Torrents — BitTorrented', | |
| title: 'Find Torrents', |
src/app/dht/layout.tsx
Outdated
| import type { Metadata } from 'next'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'DHT Torrents — BitTorrented', |
There was a problem hiding this comment.
These segment titles include the site name ("— BitTorrented"), but the root metadata defines a title.template of %s | BitTorrented (src/app/layout.tsx). Next.js will apply that template to nested string titles, producing a duplicated site name (e.g., "DHT Torrents — BitTorrented | BitTorrented"). Use a bare segment title (e.g., "DHT Torrents") or set an absolute title to bypass the template.
| title: 'DHT Torrents — BitTorrented', | |
| title: 'DHT Torrents', |
| {/* Reason message for redirects */} | ||
| {typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('reason') === 'live-tv' && ( | ||
| <div className="mb-4 rounded-lg border border-accent-primary/30 bg-accent-primary/10 px-4 py-3 text-center text-sm text-text-primary"> | ||
| Sign in to access Live TV — add your IPTV playlists and stream live channels. | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
Because this is a client component page that is still SSR'd, gating the banner on typeof window !== 'undefined' means the server render will never include the banner, but the client render can include it for ?reason=live-tv. That can trigger hydration mismatch warnings and causes a layout shift. Prefer deriving reason from searchParams in a server wrapper (and pass it as a prop), or render the banner only after mount via state set in useEffect (so the first client render matches SSR).
src/app/movies/layout.tsx
Outdated
| import type { Metadata } from 'next'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'Movies — BitTorrented', |
There was a problem hiding this comment.
These segment titles include the site name ("— BitTorrented"), but the root metadata defines a title.template of %s | BitTorrented (src/app/layout.tsx). Next.js will apply that template to nested string titles, producing a duplicated site name (e.g., "Movies — BitTorrented | BitTorrented"). Use a bare segment title (e.g., "Movies") or set an absolute title to bypass the template.
| title: 'Movies — BitTorrented', | |
| title: 'Movies', |
src/app/tvshows/layout.tsx
Outdated
| import type { Metadata } from 'next'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'TV Shows — BitTorrented', |
There was a problem hiding this comment.
These segment titles include the site name ("— BitTorrented"), but the root metadata defines a title.template of %s | BitTorrented (src/app/layout.tsx). Next.js will apply that template to nested string titles, producing a duplicated site name (e.g., "TV Shows — BitTorrented | BitTorrented"). Use a bare segment title (e.g., "TV Shows") or set an absolute title to bypass the template.
| title: 'TV Shows — BitTorrented', | |
| title: 'TV Shows', |
src/app/search/layout.tsx
Outdated
| import type { Metadata } from 'next'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'Search — BitTorrented', |
There was a problem hiding this comment.
These segment titles include the site name ("— BitTorrented"), but the root metadata defines a title.template of %s | BitTorrented (src/app/layout.tsx). Next.js will apply that template to nested string titles, producing a duplicated site name (e.g., "Search — BitTorrented | BitTorrented"). Use a bare segment title (e.g., "Search") or set an absolute title to bypass the template.
| title: 'Search — BitTorrented', | |
| title: 'Search', |
| export const metadata: Metadata = { | ||
| title: 'Trending — BitTorrented', | ||
| description: 'Browse Trending on BitTorrented', | ||
| }; |
There was a problem hiding this comment.
These segment titles include the site name ("— BitTorrented"), but the root metadata defines a title.template of %s | BitTorrented (src/app/layout.tsx). Next.js will apply that template to nested string titles, producing a duplicated site name (e.g., "Trending — BitTorrented | BitTorrented"). Use a bare segment title (e.g., "Trending") or set an absolute title to bypass the template.
next.config.ts
Outdated
| }, | ||
| { | ||
| key: 'X-Frame-Options', | ||
| value: 'DENY', | ||
| value: 'SAMEORIGIN', | ||
| }, |
There was a problem hiding this comment.
This changes X-Frame-Options to SAMEORIGIN, but the repo docs show an example reverse-proxy header config setting X-Frame-Options DENY (docs/dht-crawler-prd.md). If production still sets DENY at the edge, this will reintroduce the original problem (duplicate/conflicting X-Frame-Options headers). Please confirm what the proxy actually sends and either align the value or remove the app-level X-Frame-Options header so there’s only a single source of truth.
src/app/music/layout.tsx
Outdated
| import type { Metadata } from 'next'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'Music — BitTorrented', |
There was a problem hiding this comment.
These segment titles include the site name ("— BitTorrented"), but the root metadata defines a title.template of %s | BitTorrented (src/app/layout.tsx). Next.js will apply that template to nested string titles, producing a duplicated site name (e.g., "Music — BitTorrented | BitTorrented"). Use a bare segment title (e.g., "Music") or set an absolute title to bypass the template.
| title: 'Music — BitTorrented', | |
| title: 'Music', |
src/app/books/layout.tsx
Outdated
| import type { Metadata } from 'next'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'Books — BitTorrented', |
There was a problem hiding this comment.
These segment titles include the site name ("— BitTorrented"), but the root metadata defines a title.template of %s | BitTorrented (src/app/layout.tsx). Next.js will apply that template to nested string titles, producing a duplicated site name (e.g., "Books — BitTorrented | BitTorrented"). Use a bare segment title (e.g., "Books") or set an absolute title to bypass the template.
| title: 'Books — BitTorrented', | |
| title: 'Books', |
…ptions 1-2, 4-7, 9-10: Remove '— BitTorrented' from layout titles (root template adds it) 3: Fix login reason banner hydration mismatch - use useEffect + state 8: Remove app-level X-Frame-Options entirely (nginx is single source of truth)
Summary
Addresses QA issues #28-35 reported by Gendolf.
Fixed
#30 — favicon.ico returns 404
src/app/favicon.ico(Next.js app router convention)#32 — Duplicate X-Frame-Options headers
DENYtoSAMEORIGINto match nginx config#34 — user-scalable=no blocks accessibility zoom
userScalable: false→true,maximumScale: 1→5#35 — All pages share identical <title> tag
layout.tsxwith unique metadata for: Movies, TV Shows, Music, Books, Search, Find Torrents, Trending, DHT Torrents#31 — /live-tv silently redirects to login
reason=live-tvquery param on redirectInvestigated (no code fix needed)
#28 — Search pages return blank content
/search,/find-torrents: blank by design until user enters a query/dht: renders content (verified)#29 — Category pages show zero content to unauthenticated users
/api/browse) works without auth (verified: returns 773 movies)Needs separate investigation
#33 — /trending page has no content
Testing
Closes #30, #31, #32, #34, #35