diff --git a/CLAUDE.md b/CLAUDE.md index 808011503..5b745a627 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -366,7 +366,7 @@ This is an Nx monorepo with the following structure: - **apps/electron-backend-e2e** - Playwright E2E tests against the Electron app - **apps/stalker-mock-server** - Mock Stalker/Ministra portal for dev and E2E - **apps/xtream-mock-server** - Mock Xtream Codes API for dev and E2E -- **apps/website** - Astro + Tailwind landing page, blog (guides carry `faq:` frontmatter → FAQPage JSON-LD and open with `src/components/blog/ContentDisclaimer.astro`, whose `offline` variant is mandatory for posts about downloads or recordings; tags are a closed vocabulary in `src/lib/blog-tags.ts` enforced by the collection schema, each with a `/blog/tag//` hub), per-OS download landing pages (`/download/`, `/download/{windows,macos,linux}/`) plus the Docker page (`/download/docker/`) feature landing pages (`/features/`, registry in `src/lib/features.ts`) and comparison pages (`/compare/`, registry in `src/lib/comparisons.ts`, comparing IPTVnator's own options rather than other products); direct asset links are resolved at build time from the GitHub Releases API with a `package.json` fallback (`src/lib/downloads.ts`, see `apps/website/README.md`) +- **apps/website** - Astro + Tailwind landing page, blog (guides carry `faq:` frontmatter → FAQPage JSON-LD and open with `src/components/blog/ContentDisclaimer.astro`, whose `offline` variant is mandatory for posts about downloads or recordings; tags are a closed vocabulary in `src/lib/blog-tags.ts` enforced by the collection schema, each with a `/blog/tag//` hub), per-OS download landing pages (`/download/`, `/download/{windows,macos,linux}/`) plus the Docker page (`/download/docker/`) feature landing pages (`/features/`, registry in `src/lib/features.ts`) and comparison pages (`/compare/`, registry in `src/lib/comparisons.ts`; most compare IPTVnator's own options, and a page that names other software must carry a dated `ThirdPartyNote` — see "Pages that name other software" in `apps/website/README.md`); direct asset links are resolved at build time from the GitHub Releases API with a `package.json` fallback (`src/lib/downloads.ts`, see `apps/website/README.md`) - **libs/** - Shared libraries: - **epg/data-access** - EPG services, runtime bridge, program normalization - **m3u-state** - NgRx state management for M3U playlists diff --git a/apps/website/README.md b/apps/website/README.md index 1470427fd..e3be3474f 100644 --- a/apps/website/README.md +++ b/apps/website/README.md @@ -166,10 +166,10 @@ screenshots that show real channel names. ## Comparison Pages `/compare/` plus one page per decision the app asks users to make -(`m3u-vs-xtream-vs-stalker`, `playback-engines`, `desktop-vs-browser`) live in -`apps/website/src/pages/compare/`. They compare IPTVnator's own options against -each other, never other products, so every claim is checkable against this -repository; the registry is `src/lib/comparisons.ts`. +(`m3u-vs-xtream-vs-stalker`, `playback-engines`, `desktop-vs-browser`, +`iptvnator-vs-vlc`) live in `apps/website/src/pages/compare/`; the registry is +`src/lib/comparisons.ts`. Most of them compare IPTVnator's own options against +each other, so every claim is checkable against this repository. Each page opens with a one-paragraph verdict (`CompareHero`), carries at least one `ComparisonTable` (cells are `true`, `false` or a qualifying string) and @@ -178,6 +178,22 @@ emits `WebPage` / `FAQPage` / `BreadcrumbList` JSON-LD from these pages are guidance rather than a product listing, and `tools/testing/website-compare-pages.test.mjs` asserts that. -Naming a competitor on these pages is a product decision, not a technical one. -Phase 3 of `.plans/2026-09-03-marketing-landing-pages.md` covers that and is -still open. +### Pages that name other software + +Naming another project is a product decision the maintainer makes, not a +technical one. Where it has been made, the page follows stricter rules, and +`website-compare-pages.test.mjs` enforces the first two through its +`NAMES_THIRD_PARTY_SOFTWARE` set: + +- a dated `ThirdPartyNote` (`src/components/compare/ThirdPartyNote.astro`) + stating that the other project is independent and endorses nothing, +- claims carrying the month they were checked, because another project can add + a feature the day after the page is written, +- only stable, publicly documented platform and feature facts — never a claim + that something is missing unless it was actually checked, +- no third-party logos, brand styling, download links or affiliate links. + +Prefer describing what IPTVnator does and letting the difference speak. The +comparison that works best is the one where the other project is a partner +rather than a rival: `iptvnator-vs-vlc` ends on the external-player +integration, because that is the honest answer. diff --git a/apps/website/public/llms.txt b/apps/website/public/llms.txt index f623bd8fe..95dc2c465 100644 --- a/apps/website/public/llms.txt +++ b/apps/website/public/llms.txt @@ -33,6 +33,7 @@ - Compare, M3U vs Xtream Codes vs Stalker portal: https://4gray.github.io/iptvnator/compare/m3u-vs-xtream-vs-stalker/ - Compare, playback engines (built-in vs MPV/VLC vs embedded MPV): https://4gray.github.io/iptvnator/compare/playback-engines/ - Compare, desktop app vs self-hosted browser version: https://4gray.github.io/iptvnator/compare/desktop-vs-browser/ +- Compare, IPTVnator vs VLC (when a general media player is enough): https://4gray.github.io/iptvnator/compare/iptvnator-vs-vlc/ - Release Notes (v0.18): https://4gray.github.io/iptvnator/blog/v0-18-release-notes/ - Troubleshooting (macOS app damaged fix): https://4gray.github.io/iptvnator/blog/macos-arm-app-damaged-fix/ - Source Code: https://github.com/4gray/iptvnator diff --git a/apps/website/src/components/compare/ThirdPartyNote.astro b/apps/website/src/components/compare/ThirdPartyNote.astro new file mode 100644 index 000000000..3a860da9f --- /dev/null +++ b/apps/website/src/components/compare/ThirdPartyNote.astro @@ -0,0 +1,29 @@ +--- +/** + * Shown on every comparison page that names software other than IPTVnator. + * Two jobs: state that there is no affiliation, and date the claims, because + * another project can add a feature the day after this page is written. + */ +interface Props { + /** Names as their own projects spell them. */ + names: string[]; + /** Month and year the claims on the page were checked. */ + checked: string; +} + +const { names, checked } = Astro.props; +const many = names.length > 1; +const list = many ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names[0]; +const copy = many + ? { verb: 'are independent projects', pronoun: 'them', possessive: 'Their names are', doing: 'they do' } + : { verb: 'is an independent project', pronoun: 'it', possessive: 'Its name is', doing: 'it does' }; +--- + +
+

+ Not affiliated.{' '} + {list} {copy.verb} with no connection to IPTVnator, and nothing here is endorsed by{' '} + {copy.pronoun}. {copy.possessive} used only to describe what {copy.doing}. Claims on this + page were checked in {checked}; software changes, so verify anything that decides your setup. +

+
diff --git a/apps/website/src/lib/comparisons.ts b/apps/website/src/lib/comparisons.ts index b2ceb092d..a71c9d9d1 100644 --- a/apps/website/src/lib/comparisons.ts +++ b/apps/website/src/lib/comparisons.ts @@ -1,14 +1,20 @@ /** - * Registry of the comparison pages under `/compare/`. These pages compare - * IPTVnator's own options against each other — connection types, playback - * engines, editions — rather than naming other products, so every claim on - * them can be checked against this repository. + * Registry of the comparison pages under `/compare/`. + * + * Most of them compare IPTVnator's own options against each other — connection + * types, playback engines, editions — so every claim can be checked against + * this repository. A page that names other software follows stricter rules: + * only platform and feature facts that are stable and publicly documented, a + * dated `ThirdPartyNote`, no logos or brand styling, no download links to the + * other project, and no claim that a feature is missing unless it was checked. + * Prefer describing what IPTVnator does and letting the difference speak. */ export type ComparisonSlug = | 'm3u-vs-xtream-vs-stalker' | 'playback-engines' - | 'desktop-vs-browser'; + | 'desktop-vs-browser' + | 'iptvnator-vs-vlc'; export interface ComparisonEntry { slug: ComparisonSlug; @@ -52,6 +58,15 @@ export const COMPARISONS: readonly ComparisonEntry[] = [ href: '/iptvnator/compare/desktop-vs-browser/', icon: 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z', }, + { + slug: 'iptvnator-vs-vlc', + label: 'IPTVnator vs VLC', + eyebrow: 'Compare · Other players', + question: + 'VLC opens an M3U playlist too. When is that enough, and what does a player built around IPTV add on top?', + href: '/iptvnator/compare/iptvnator-vs-vlc/', + icon: 'M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5', + }, ]; export const COMPARE_HUB_HREF = '/iptvnator/compare/'; diff --git a/apps/website/src/pages/compare/iptvnator-vs-vlc.astro b/apps/website/src/pages/compare/iptvnator-vs-vlc.astro new file mode 100644 index 000000000..2294b7d83 --- /dev/null +++ b/apps/website/src/pages/compare/iptvnator-vs-vlc.astro @@ -0,0 +1,175 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import FaqAccordion from '../../components/blog/FaqAccordion.astro'; +import DownloadSection from '../../components/download/DownloadSection.astro'; +import FeatureList from '../../components/download/FeatureList.astro'; +import OfficialSourcesNote from '../../components/download/OfficialSourcesNote.astro'; +import RelatedPosts from '../../components/download/RelatedPosts.astro'; +import CompareHero from '../../components/compare/CompareHero.astro'; +import ComparisonTable from '../../components/compare/ComparisonTable.astro'; +import CompareSwitcher from '../../components/compare/CompareSwitcher.astro'; +import ThirdPartyNote from '../../components/compare/ThirdPartyNote.astro'; +import type { FaqEntry } from '../../lib/download-schema'; +import { buildComparisonPageSchema } from '../../lib/comparison-schema'; +import { comparisonBySlug } from '../../lib/comparisons'; +import { absoluteUrl } from '../../lib/site'; + +const comparison = comparisonBySlug('iptvnator-vs-vlc'); +const pageUrl = absoluteUrl('/compare/iptvnator-vs-vlc/'); +const title = 'IPTVnator vs VLC – When a Media Player Is Enough for IPTV'; +const description = + 'VLC opens an M3U playlist and plays almost anything inside it. IPTVnator adds what a playlist file cannot carry: a searchable catalog, the programme guide, provider accounts, favorites and catch-up — and it can hand the stream to VLC when a codec needs it.'; + +const columns = ['VLC', 'IPTVnator']; + +const sourceRows = [ + { label: 'Opens an M3U or M3U8 playlist', cells: [true, true] }, + { label: 'Opens a single stream URL', cells: [true, true] }, + { label: 'Xtream Codes account (server, username, password)', cells: [false, true], note: 'Brings live TV, movies and series as one catalog instead of a flat list.' }, + { label: 'Stalker or Ministra portal (MAC address)', cells: [false, true] }, + { label: 'XMLTV programme guide', cells: [false, true] }, + { label: 'Several sources side by side', cells: ['One playlist at a time', true] }, +]; + +const libraryRows = [ + { label: 'Channel list with groups and search', cells: ['Flat playlist', true] }, + { label: 'Movies and series with posters and descriptions', cells: [false, true] }, + { label: 'Favorites across sources, recently watched', cells: ['General playlist and media library', true] }, + { label: 'Programme now and next, timeline, full guide', cells: [false, true] }, + { label: 'Catch-up from the guide', cells: [false, true], note: 'Where the provider offers an archive.' }, + { label: 'Resume where you stopped', cells: ['Per file in its own history', 'Per stream, across sources'] }, + { label: 'Offline downloads', cells: [false, 'Desktop app'] }, + { label: 'Phone remote control', cells: [false, 'Desktop app'] }, +]; + +const practicalRows = [ + { label: 'Plays unusual codecs and containers', cells: [true, 'Built-in players cover the common ones; MPV, VLC or the bundled mpv engine handle the rest'] }, + { label: 'Sends the provider’s User-Agent and Referer', cells: ['Set by hand per stream', 'Stored with the playlist and sent automatically'] }, + { label: 'Where it runs', cells: ['Windows, macOS, Linux, Android, iOS', 'Windows, macOS, Linux; browser version you host yourself'] }, + { label: 'Price', cells: ['Free, open source', 'Free, open source'] }, + { label: 'Account needed', cells: [false, false] }, + { label: 'Supplies channels or playlists', cells: [false, false], note: 'Neither is a content source. You bring a subscription you already have.' }, +]; + +const together = [ + { + title: 'IPTVnator can play the stream in VLC', + description: + 'Choose VLC under Settings › Playback and IPTVnator launches your own copy for playback. The catalog, the guide, favorites and the phone remote stay in IPTVnator; only the video moves to a VLC window.', + }, + { + title: 'The handover carries what VLC cannot guess', + description: + 'IPTVnator passes the playlist’s User-Agent, Referer and any custom headers with the stream, so a provider that checks them still answers. It also passes your resume position and the title for the window.', + }, + { + title: 'Progress comes back', + description: + 'IPTVnator asks VLC where it is through VLC’s own remote-control interface, so a movie you stopped in VLC resumes from the same place in the library.', + }, + { + title: 'A codec problem stops being a dead end', + description: + 'A stream that stutters or refuses in a browser engine usually plays immediately in VLC. Switching one stream does not change anything else about your setup.', + }, +]; + +const faq: FaqEntry[] = [ + { + q: 'Can VLC play an IPTV M3U playlist?', + a: 'Yes. Open the .m3u file or paste its URL into Media › Open Network Stream and VLC lists the entries in its playlist, then plays them. That is genuinely enough for a handful of channels.', + }, + { + q: 'So why use a dedicated IPTV player at all?', + a: 'Because a playlist file carries only names and stream addresses. It has no programme guide, no movie or series catalog, no account status and no catch-up. With a few hundred or a few thousand entries, a flat list also stops being something you can browse. IPTVnator is built around those things; VLC is built around playing a file.', + }, + { + q: 'Does VLC support Xtream Codes logins or Stalker portals?', + a: 'No. Those are provider APIs, not stream files: something has to log in, ask for categories, and build a catalog out of the answers. VLC opens files and URLs. If your provider gave you a server address, a username and a password, IPTVnator can use them directly.', + }, + { + q: 'Can VLC show the programme guide?', + a: 'Not for IPTV. XMLTV guide data is a separate file that has to be downloaded, parsed and matched to channels; VLC has no such feature. IPTVnator does this in the background and shows what is on now, a timeline and a full guide.', + }, + { + q: 'Which one plays more formats?', + a: 'VLC, almost certainly — decoding is what it is for. IPTVnator does not compete there: it lets you send any stream to VLC or MPV on your machine, or to the mpv engine that ships with the desktop app, so the coverage becomes theirs.', + }, + { + q: 'Do I have to choose one?', + a: 'No, and most people should not. Keeping VLC installed gives IPTVnator somewhere to send a stubborn stream. Neither installation affects the other.', + }, + { + q: 'What about watching on a phone?', + a: 'VLC has mobile apps and IPTVnator does not. The browser version of IPTVnator, which you host yourself, opens on a phone browser, and the desktop app can be controlled from a phone with its web remote — but for playback on the phone itself, a mobile player is the right tool.', + }, + { + q: 'Is either one a source of channels?', + a: 'No. Both are players. Neither provides, hosts or sells playlists, channels or subscriptions; you bring a source you already have the right to use.', + }, +]; + +const jsonLd = buildComparisonPageSchema({ comparison, pageUrl, description, faq }); +--- + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + +
+ +
+
+ + +
diff --git a/tools/testing/website-compare-pages.test.mjs b/tools/testing/website-compare-pages.test.mjs index 8937c8a01..13c2a1f1b 100644 --- a/tools/testing/website-compare-pages.test.mjs +++ b/tools/testing/website-compare-pages.test.mjs @@ -10,7 +10,14 @@ import assert from 'node:assert/strict'; const distRoot = new URL('../../dist/apps/website/', import.meta.url); const SITE = 'https://4gray.github.io/iptvnator'; -const COMPARISONS = ['m3u-vs-xtream-vs-stalker', 'playback-engines', 'desktop-vs-browser']; +const COMPARISONS = [ + 'm3u-vs-xtream-vs-stalker', + 'playback-engines', + 'desktop-vs-browser', + 'iptvnator-vs-vlc', +]; +/** Pages that name other software must say they are not affiliated with it, and date their claims. */ +const NAMES_THIRD_PARTY_SOFTWARE = new Set(['iptvnator-vs-vlc']); const readDist = (relativePath) => readFile(new URL(relativePath, distRoot), 'utf8'); @@ -40,6 +47,15 @@ for (const slug of COMPARISONS) { !schema.some((entry) => entry['@type'] === 'SoftwareApplication'), 'A comparison page is guidance, not a product listing.' ); + + if (NAMES_THIRD_PARTY_SOFTWARE.has(slug)) { + assert.match(html, /Not affiliated\./, 'A page naming other software must carry the disclaimer.'); + assert.match( + html, + /Claims on this page were checked in \w+ \d{4}/, + 'A page naming other software must date its claims.' + ); + } }); test(`comparison page ${slug}: links to the hub and the other comparisons`, async () => {