diff --git a/CLAUDE.md b/CLAUDE.md index 136d2c649..8b41b312b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -330,7 +330,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, and per-OS download landing pages (`/download/`, `/download/{windows,macos,linux}/`); 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), per-OS download landing pages (`/download/`, `/download/{windows,macos,linux}/`) and feature landing pages (`/features/`, registry in `src/lib/features.ts`); 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 500dc5065..a6629739e 100644 --- a/apps/website/README.md +++ b/apps/website/README.md @@ -82,3 +82,17 @@ Two conventions set them apart: `tools/testing/website-guides.test.mjs` (part of `pnpm nx test website`) checks each guide for the FAQPage schema, a link to the download hub and the presence of every referenced screenshot in the build output. + +## Feature Pages + +`/features/` plus one page per feature (`m3u-player`, `xtream-codes-player`, +`stalker-portal-player`, `epg`, `remote-control`) live in +`apps/website/src/pages/features/`. They target " player" style +searches, reuse the download-page sections, and each carries +`SoftwareApplication` (with `featureList`) / `FAQPage` / `BreadcrumbList` +structured data. The registry in `src/lib/features.ts` drives the hub, the +per-page switcher, the homepage feature cards and +`tools/testing/website-feature-pages.test.mjs`; adding a page means adding one +registry entry and one `.astro` file. Screenshots come only from the +mock-backed guide and release captures, never from the older homepage +screenshots that show real channel names. diff --git a/apps/website/project.json b/apps/website/project.json index 06820b077..8d39a04f5 100644 --- a/apps/website/project.json +++ b/apps/website/project.json @@ -18,7 +18,7 @@ "executor": "nx:run-commands", "dependsOn": ["build"], "options": { - "command": "node --test tools/testing/website-giscus-comments.test.mjs tools/testing/website-download-pages.test.mjs tools/testing/website-guides.test.mjs" + "command": "node --test tools/testing/website-giscus-comments.test.mjs tools/testing/website-download-pages.test.mjs tools/testing/website-guides.test.mjs tools/testing/website-feature-pages.test.mjs" } }, "serve": { diff --git a/apps/website/public/llms.txt b/apps/website/public/llms.txt index f98aad8ef..618888e06 100644 --- a/apps/website/public/llms.txt +++ b/apps/website/public/llms.txt @@ -14,6 +14,12 @@ - Guide, Xtream Codes setup: https://4gray.github.io/iptvnator/blog/xtream-codes-setup-guide/ - Guide, Stalker/Ministra portal setup: https://4gray.github.io/iptvnator/blog/stalker-portal-setup-guide/ - Guide, M3U playlist and EPG setup: https://4gray.github.io/iptvnator/blog/m3u-playlist-epg-setup-guide/ +- Features overview: https://4gray.github.io/iptvnator/features/ +- Feature, M3U playlist player: https://4gray.github.io/iptvnator/features/m3u-player/ +- Feature, Xtream Codes player: https://4gray.github.io/iptvnator/features/xtream-codes-player/ +- Feature, Stalker portal player: https://4gray.github.io/iptvnator/features/stalker-portal-player/ +- Feature, TV guide (EPG): https://4gray.github.io/iptvnator/features/epg/ +- Feature, phone remote control: https://4gray.github.io/iptvnator/features/remote-control/ - 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/FeatureGrid.astro b/apps/website/src/components/FeatureGrid.astro index c12eaa6ad..270bf2703 100644 --- a/apps/website/src/components/FeatureGrid.astro +++ b/apps/website/src/components/FeatureGrid.astro @@ -1,32 +1,39 @@ --- -const heroFeatures = [ +type FeatureCard = { icon: string; title: string; description: string; href?: string }; + +const heroFeatures: FeatureCard[] = [ { icon: '', title: 'M3U / M3U8 Playlists', description: 'Import playlists from files or URLs with full M3U and M3U8 support.', + href: '/iptvnator/features/m3u-player/', }, { icon: '', title: 'Xtream Codes API', description: 'Connect to Xtream Codes providers with live TV, VOD, and series.', + href: '/iptvnator/features/xtream-codes-player/', }, { icon: '', title: 'Electronic Program Guide', description: 'XMLTV-based EPG with current and upcoming program information.', + href: '/iptvnator/features/epg/', }, { icon: '', title: 'Cross-Platform', description: 'Runs on macOS, Windows, Linux as a desktop app or PWA in any browser.', + href: '/iptvnator/download/', }, ]; -const carouselFeatures = [ +const carouselFeatures: FeatureCard[] = [ { icon: '', title: 'Stalker Portals', description: 'Browse and stream content from Stalker/Ministra portal providers.', + href: '/iptvnator/features/stalker-portal-player/', }, { icon: '', @@ -57,6 +64,7 @@ const carouselFeatures = [ icon: '', title: 'Remote Control', description: 'Control the player from your phone with the built-in remote.', + href: '/iptvnator/features/remote-control/', }, { icon: '', @@ -87,7 +95,7 @@ const carouselFeatures = [ @@ -140,7 +148,7 @@ const carouselFeatures = [ { carouselFeatures.map((feature, i) => ( -
+
@@ -153,7 +161,7 @@ const carouselFeatures = [

{feature.description}

-
+
)) } diff --git a/apps/website/src/components/Header.astro b/apps/website/src/components/Header.astro index f144cf2c0..a15a29edf 100644 --- a/apps/website/src/components/Header.astro +++ b/apps/website/src/components/Header.astro @@ -1,6 +1,6 @@ --- const navLinks = [ - { label: 'Features', href: '/iptvnator/#features' }, + { label: 'Features', href: '/iptvnator/features/' }, { label: 'Screenshots', href: '/iptvnator/#screenshots' }, { label: 'Download', href: '/iptvnator/download/' }, { label: 'Blog', href: '/iptvnator/blog/' }, diff --git a/apps/website/src/components/feature/FeatureHero.astro b/apps/website/src/components/feature/FeatureHero.astro new file mode 100644 index 000000000..f56a923e7 --- /dev/null +++ b/apps/website/src/components/feature/FeatureHero.astro @@ -0,0 +1,101 @@ +--- +import type { FeatureEntry } from '../../lib/features'; +import { FEATURES_HUB_HREF } from '../../lib/features'; + +interface Screenshot { + src: string; + alt: string; + width: number; + height: number; +} + +interface Props { + feature: FeatureEntry; + titleLead: string; + titleAccent: string; + lede: string; + facts: string[]; + screenshot: Screenshot; +} + +const { feature, titleLead, titleAccent, lede, facts, screenshot } = Astro.props; +--- + +
+
+ +
+ + +
+
+
+ + + + {feature.eyebrow} +
+ +

+ {titleLead}{' '} + {titleAccent} +

+ +

{lede}

+ + + +
    + {facts.map((fact) => ( +
  • {fact}
  • + ))} +
+
+ +
+
+
+ {screenshot.alt} +
+
{screenshot.alt}
+
+
+
+
diff --git a/apps/website/src/components/feature/FeatureSwitcher.astro b/apps/website/src/components/feature/FeatureSwitcher.astro new file mode 100644 index 000000000..753937b64 --- /dev/null +++ b/apps/website/src/components/feature/FeatureSwitcher.astro @@ -0,0 +1,42 @@ +--- +import { FEATURES, FEATURES_HUB_HREF } from '../../lib/features'; +import type { FeatureSlug } from '../../lib/features'; + +interface Props { + current: FeatureSlug; +} + +const { current } = Astro.props; +const others = FEATURES.filter((feature) => feature.slug !== current); +--- + +
+
+ +
+
+
+ More features +

+ The rest of{' '} + IPTVnator +

+
+ All features +
+ +
+ {others.map((feature) => ( + + + + + {feature.label} + {feature.summary} + + ))} +
+
+
diff --git a/apps/website/src/lib/feature-schema.ts b/apps/website/src/lib/feature-schema.ts new file mode 100644 index 000000000..598b2f0d8 --- /dev/null +++ b/apps/website/src/lib/feature-schema.ts @@ -0,0 +1,57 @@ +import type { FaqEntry } from './download-schema'; +import { REPO_URL } from './downloads'; +import type { FeatureEntry } from './features'; +import { FEATURES_HUB_HREF } from './features'; +import { absoluteUrl } from './site'; + +interface FeaturePageSchemaInput { + feature: FeatureEntry; + pageUrl: string; + description: string; + /** Plain-language feature statements for schema.org `featureList`. */ + featureList: string[]; + faq: FaqEntry[]; +} + +/** SoftwareApplication (with featureList) + FAQPage + BreadcrumbList for one feature page. */ +export function buildFeaturePageSchema(input: FeaturePageSchemaInput): Array> { + const { feature, pageUrl, description, featureList, faq } = input; + + return [ + { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'IPTVnator', + applicationCategory: 'MultimediaApplication', + applicationSubCategory: 'IPTV player', + operatingSystem: 'Windows, macOS, Linux', + url: pageUrl, + downloadUrl: absoluteUrl('/download/'), + featureList, + isAccessibleForFree: true, + license: `${REPO_URL}/blob/master/LICENSE`, + offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, + author: { '@type': 'Person', name: '4gray', url: 'https://github.com/4gray' }, + sameAs: [REPO_URL, 'https://t.me/iptvnator'], + description, + }, + { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: faq.map((entry) => ({ + '@type': 'Question', + name: entry.q, + acceptedAnswer: { '@type': 'Answer', text: entry.a }, + })), + }, + { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'IPTVnator', item: absoluteUrl('/') }, + { '@type': 'ListItem', position: 2, name: 'Features', item: absoluteUrl(FEATURES_HUB_HREF.replace('/iptvnator', '')) }, + { '@type': 'ListItem', position: 3, name: feature.label, item: pageUrl }, + ], + }, + ]; +} diff --git a/apps/website/src/lib/features.ts b/apps/website/src/lib/features.ts new file mode 100644 index 000000000..ceccb60e8 --- /dev/null +++ b/apps/website/src/lib/features.ts @@ -0,0 +1,95 @@ +/** + * Registry of the feature landing pages under `/features/`. The hub, the + * per-page switcher, the homepage cards and the build test all read from + * here, so adding a page means adding one entry and one `.astro` file. + */ + +export type FeatureSlug = + | 'm3u-player' + | 'xtream-codes-player' + | 'stalker-portal-player' + | 'epg' + | 'remote-control'; + +export interface FeatureGuide { + href: string; + label: string; +} + +export interface FeatureEntry { + slug: FeatureSlug; + /** Short name used in breadcrumbs, cards and the switcher. */ + label: string; + /** Mono eyebrow above the page title. */ + eyebrow: string; + /** One sentence for cards. */ + summary: string; + /** Site-relative href including the GitHub Pages base. */ + href: string; + /** 24×24 stroke icon path (Heroicons outline style). */ + icon: string; + guide: FeatureGuide; +} + +export const FEATURES: readonly FeatureEntry[] = [ + { + slug: 'm3u-player', + label: 'M3U playlist player', + eyebrow: 'Feature · M3U playlists', + summary: + 'Load M3U and M3U8 playlists from a link, a file or text, and browse tens of thousands of channels with groups, favorites and search.', + href: '/iptvnator/features/m3u-player/', + 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', + guide: { href: '/iptvnator/blog/m3u-playlist-epg-setup-guide/', label: 'M3U and EPG setup guide' }, + }, + { + slug: 'xtream-codes-player', + label: 'Xtream Codes player', + eyebrow: 'Feature · Xtream Codes API', + summary: + 'Sign in with server URL, username and password for live TV, movies and series with posters, details, catch-up and offline downloads.', + href: '/iptvnator/features/xtream-codes-player/', + icon: 'M13 10V3L4 14h7v7l9-11h-7z', + guide: { href: '/iptvnator/blog/xtream-codes-setup-guide/', label: 'Xtream Codes setup guide' }, + }, + { + slug: 'stalker-portal-player', + label: 'Stalker portal player', + eyebrow: 'Feature · Stalker / Ministra portals', + summary: + 'Connect Stalker and Ministra portals by MAC address, the way a MAG box does, with live TV, radio, movies, series and an account page.', + href: '/iptvnator/features/stalker-portal-player/', + icon: 'M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.858 15.355-5.858 21.213 0', + guide: { href: '/iptvnator/blog/stalker-portal-setup-guide/', label: 'Stalker portal setup guide' }, + }, + { + slug: 'epg', + label: 'TV guide (EPG)', + eyebrow: 'Feature · Program guide', + summary: + 'XMLTV files and portal schedules become a program guide: now and next in every list, a timeline under the player, and catch-up for archived programs.', + href: '/iptvnator/features/epg/', + icon: 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z', + guide: { href: '/iptvnator/blog/m3u-playlist-epg-setup-guide/', label: 'M3U and EPG setup guide' }, + }, + { + slug: 'remote-control', + label: 'Phone remote control', + eyebrow: 'Feature · Remote control', + summary: + 'The desktop app serves a web remote to any phone on your network: channel up and down, direct numbers, volume, and a now-playing panel.', + href: '/iptvnator/features/remote-control/', + icon: 'M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z', + guide: { href: '/iptvnator/blog/m3u-playlist-epg-setup-guide/', label: 'M3U and EPG setup guide' }, + }, +]; + +export const FEATURES_HUB_HREF = '/iptvnator/features/'; + +export function featureBySlug(slug: FeatureSlug): FeatureEntry { + const entry = FEATURES.find((feature) => feature.slug === slug); + if (!entry) { + throw new Error(`Unknown feature slug: ${slug}`); + } + return entry; +} diff --git a/apps/website/src/pages/features/epg.astro b/apps/website/src/pages/features/epg.astro new file mode 100644 index 000000000..fbe62ae18 --- /dev/null +++ b/apps/website/src/pages/features/epg.astro @@ -0,0 +1,163 @@ +--- +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 SpecList from '../../components/download/SpecList.astro'; +import FeatureHero from '../../components/feature/FeatureHero.astro'; +import FeatureSwitcher from '../../components/feature/FeatureSwitcher.astro'; +import type { FaqEntry } from '../../lib/download-schema'; +import { buildFeaturePageSchema } from '../../lib/feature-schema'; +import { featureBySlug } from '../../lib/features'; +import { absoluteUrl } from '../../lib/site'; + +const feature = featureBySlug('epg'); +const pageUrl = absoluteUrl('/features/epg/'); +const title = 'TV Guide (EPG) for IPTV – XMLTV, Timeline and Catch-Up | IPTVnator'; +const description = + 'IPTVnator turns XMLTV files and portal schedules into a program guide: what is on now in every channel list, a timeline under the player, a grid across all channels, catch-up for archived programs, and manual mapping when a channel does not match. Free and open source for Windows, macOS and Linux.'; + +const guide = [ + { + title: 'Now and next in every list', + description: + 'Each channel row shows the current program with a progress bar, in the playlist views, the portal lists and the favorites and recent collections.', + }, + { + title: 'A timeline under the player', + description: + 'The schedule of the selected channel runs as a ribbon with day navigation, zoom levels, a jump to now, and a program search. A vertical single-day list is the alternative view.', + }, + { + title: 'A grid across channels', + description: + 'The multi-channel view lays the schedules of all channels side by side on one time axis, with channel filtering and program search.', + }, + { + title: 'Program details', + description: + 'Title, category, description, episode, language, rating and duration open in a dialog from any program block.', + }, + { + title: 'Catch-up', + description: + 'Where the provider keeps an archive, past programs start straight from the timeline, on M3U playlists with catch-up attributes and on Xtream and Stalker portals.', + }, + { + title: 'Manual channel mapping', + description: + 'A channel the guide cannot match gets a right-click action to pick its XMLTV entry by hand. Mappings are stored on your device and survive refreshes.', + }, +]; + +const sources = [ + { term: 'M3U playlists', value: 'XMLTV sources from Settings › EPG or declared by the playlist through url-tvg. Channels match by tvg-id, then tvg-name, then name.' }, + { term: 'Xtream Codes', value: 'The portal schedule is used first; your XMLTV sources fill missing channels or take precedence with one setting.' }, + { term: 'Stalker portals', value: 'A seven-day schedule is loaded from the portal once per session; single-channel requests fill any gaps.' }, + { term: 'Formats', value: 'XMLTV as .xml or compressed .xml.gz, parsed in a background worker and stored in a local database.' }, + { term: 'Sources', value: 'Any number of global sources plus per-playlist sources, each with a status chip, a refresh button and one refresh-all action.' }, + { term: 'Availability', value: 'The XMLTV guide is a desktop feature. Portal schedules from Xtream and Stalker also show in the browser version.' }, +]; + +const faq: FaqEntry[] = [ + { + q: 'Where does the program guide come from?', + a: 'From XMLTV files you add or your playlist declares, and from the schedules Xtream and Stalker portals publish. IPTVnator merges them per channel: portal data first, your XMLTV sources as a fallback or, with one setting, as the preferred source.', + }, + { + q: 'Why does a channel show no program?', + a: 'It is not matched to a guide entry. Matching uses tvg-id, then tvg-name, then the channel name. Check the ids in the playlist, or map the channel by hand with a right-click and "Map EPG channel".', + }, + { + q: 'Can I watch a program that already aired?', + a: 'Yes, when the provider keeps an archive. M3U playlists declare it with catchup attributes, Xtream and Stalker portals flag archived channels. Past programs then start from the timeline; without an archive the timeline is a schedule only.', + }, + { + q: 'How big can the XMLTV file be?', + a: 'Large guides are fine. Parsing runs in a background worker and the result is stored in a local database, so the app stays responsive and the data is there on the next start.', + }, + { + q: 'Does the guide work in the browser version?', + a: 'Portal schedules from Xtream and Stalker sources do. XMLTV downloads and the local guide database are desktop features.', + }, + { + q: 'Can I see several channels at once?', + a: 'Yes. The multi-channel view lays all schedules on one time axis with zoom, channel filtering and program search, in addition to the timeline of the selected channel.', + }, +]; + +const jsonLd = buildFeaturePageSchema({ + feature, + pageUrl, + description, + featureList: [ + 'XMLTV program guide from .xml and .xml.gz sources', + 'Current program and progress in every channel list', + 'Timeline and list views under the player', + 'Multi-channel schedule grid with search', + 'Catch-up playback of archived programs', + 'Manual EPG channel mapping', + 'Portal schedules from Xtream Codes and Stalker sources', + ], + faq, +}); +--- + + + + + + + + + +
+ +
+ Settings with the EPG section open, a source URL entered and its status chip +
+
+
+ + +
+ +
+
+ + + +
+ +
+
+ + +
diff --git a/apps/website/src/pages/features/index.astro b/apps/website/src/pages/features/index.astro new file mode 100644 index 000000000..eac0560ba --- /dev/null +++ b/apps/website/src/pages/features/index.astro @@ -0,0 +1,126 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { FEATURES } from '../../lib/features'; +import { absoluteUrl } from '../../lib/site'; +import { REPO_URL } from '../../lib/downloads'; + +const pageUrl = absoluteUrl('/features/'); +const title = 'IPTVnator Features – M3U, Xtream Codes, Stalker Portals, EPG and Remote'; +const description = + 'What IPTVnator does: M3U and M3U8 playlists, Xtream Codes and Stalker portals, an XMLTV program guide with catch-up, a phone remote, offline downloads, TMDB metadata and MPV or VLC playback. Free and open source for Windows, macOS and Linux.'; + +const more = [ + { title: 'Offline downloads', text: 'Movies, episodes and whole seasons from Xtream and Stalker sources, with resume on interrupted transfers.' }, + { title: 'Movie and series metadata', text: 'Optional TMDB enrichment with posters, cast, ratings, trailers, actor pages and a Discover view.' }, + { title: 'MPV, VLC and embedded MPV', text: 'Hand streams to MPV or VLC, or render mpv inside the window with the experimental embedded engine.' }, + { title: 'Global collections', text: 'Favorites, recent items and search that span every source you added.' }, + { title: 'Dashboard', text: 'Continue watching, favorites rails, recently added content and source cards with expiry chips.' }, + { title: 'Privacy first', text: 'No account, no analytics. Playlists and credentials stay on your device.' }, + { title: 'Nineteen languages', text: 'Community-contributed translations, light and dark themes.' }, + { title: 'Self-hosted browser version', text: 'A Docker image serves the web app and its backend for devices that cannot run the desktop app.' }, +]; + +const jsonLd = [ + { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'IPTVnator', + applicationCategory: 'MultimediaApplication', + applicationSubCategory: 'IPTV player', + operatingSystem: 'Windows, macOS, Linux', + url: pageUrl, + downloadUrl: absoluteUrl('/download/'), + featureList: FEATURES.map((feature) => feature.label), + isAccessibleForFree: true, + license: `${REPO_URL}/blob/master/LICENSE`, + offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, + author: { '@type': 'Person', name: '4gray', url: 'https://github.com/4gray' }, + description, + }, + { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'IPTVnator', item: absoluteUrl('/') }, + { '@type': 'ListItem', position: 2, name: 'Features', item: pageUrl }, + ], + }, +]; +--- + + +
+
+ +
+
+ Features +

+ Everything IPTVnator does +

+

+ One player for the three ways providers hand out access, with a program guide, a phone remote and the + comforts of a desktop app. Each page below goes into detail and links to the matching setup guide. +

+
+ + +
+
+ +
+
+
+
+ And more +

+ The rest of the desktop app +

+
+
+ {more.map((item) => ( +
+
{item.title}
+
{item.text}
+
+ ))} +
+
+
+
diff --git a/apps/website/src/pages/features/m3u-player.astro b/apps/website/src/pages/features/m3u-player.astro new file mode 100644 index 000000000..917a3d248 --- /dev/null +++ b/apps/website/src/pages/features/m3u-player.astro @@ -0,0 +1,177 @@ +--- +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 SpecList from '../../components/download/SpecList.astro'; +import FeatureHero from '../../components/feature/FeatureHero.astro'; +import FeatureSwitcher from '../../components/feature/FeatureSwitcher.astro'; +import type { FaqEntry } from '../../lib/download-schema'; +import { buildFeaturePageSchema } from '../../lib/feature-schema'; +import { featureBySlug } from '../../lib/features'; +import { absoluteUrl } from '../../lib/site'; + +const feature = featureBySlug('m3u-player'); +const pageUrl = absoluteUrl('/features/m3u-player/'); +const title = 'M3U Playlist Player for Windows, macOS and Linux – IPTVnator'; +const description = + 'Play M3U and M3U8 IPTV playlists on your desktop with IPTVnator: import from a URL, a file or text, browse channel groups and favorites, search tens of thousands of channels, attach an XMLTV guide with catch-up, and hand hard streams to MPV or VLC. Free and open source.'; + +const library = [ + { + title: 'Lists that stay fast', + description: + 'The channel list is virtualized, so a playlist with tens of thousands of entries scrolls and searches without lag. Search filters as you type.', + }, + { + title: 'Groups from the playlist', + description: + 'Every group-title becomes an expandable section. Hide the groups you never watch; the hidden set survives refreshes.', + }, + { + title: 'Favorites and history', + description: + 'Star channels into a favorites list you can reorder by drag and drop, and jump back to what you watched last from Recent.', + }, + { + title: 'Refresh and auto-update', + description: + 'Re-download a playlist on request, or let the desktop app refresh it on every start. Favorites, hidden groups and settings are kept.', + }, + { + title: 'Import any way you like', + description: + 'Paste a link, pick a file, drop it onto the window, open it from the file manager, pass it on the command line, or paste the raw text.', + }, + { + title: 'Global favorites and search', + description: + 'With several playlists, the dashboard combines favorites, recent items and search across all of them.', + }, +]; + +const declared = [ + { term: 'Logos and names', value: 'tvg-logo, tvg-name and the display name are used for the list; XMLTV icons fill in missing logos.' }, + { term: 'Program guide', value: 'tvg-id links a channel to the guide. url-tvg or x-tvg-url in the header imports the EPG source automatically.' }, + { term: 'Catch-up', value: 'catchup, catchup-source and catchup-days, or the older tvg-rec and timeshift, make archived programs playable from the timeline.' }, + { term: 'Radio', value: 'Entries with radio="true" open in a dedicated audio layout with artwork, transport controls and volume.' }, + { term: 'DASH with ClearKey', value: '#KODIPROP lines with inputstream.adaptive keys play in the built-in player through Shaka.' }, + { term: 'Movie files', value: 'Entries that look like a movie file open in a movie detail view with TMDB metadata when enrichment is enabled.' }, +]; + +const playback = [ + { + title: 'Three built-in engines', + description: + 'HTML5 with hls.js, Video.js and ArtPlayer cover HLS, MPEG-TS and DASH streams with one shared set of controls, subtitles, quality selection and picture-in-picture.', + }, + { + title: 'MPV and VLC handoff', + description: + 'Streams the browser engines cannot decode go to MPV or VLC on your machine while IPTVnator keeps the list, the guide and the remote control.', + }, + { + title: 'Embedded MPV', + description: + 'An experimental engine renders mpv inside the app window on Windows, macOS and Linux, including live-TV recording where supported.', + }, + { + title: 'Custom User-Agent', + description: + 'Providers that only answer known player signatures get the User-Agent you set in the playlist details, for both playlist download and playback.', + }, +]; + +const faq: FaqEntry[] = [ + { + q: 'Does IPTVnator play both M3U and M3U8 playlists?', + a: 'Yes. Both extensions are the same text format; M3U8 only marks the file as UTF-8. Playlists can come from a link, a local file or pasted text, and the same attributes are read from all of them.', + }, + { + q: 'How many channels can a playlist have?', + a: 'The list is built for very large playlists and stays responsive with tens of thousands of channels. Groups, favorites and search all work on the full list, not on a page of it.', + }, + { + q: 'Does the playlist update when my provider changes it?', + a: 'Refresh it on request from the playlist details, or enable Auto-update to re-download it every time the desktop app starts. Your favorites, hidden groups and settings are kept across refreshes.', + }, + { + q: 'Can I get a program guide for an M3U playlist?', + a: 'Yes, on the desktop app. Add an XMLTV source in Settings › EPG, or let the playlist declare it through url-tvg in its header. Channels are matched by tvg-id, and you can map the rest by hand.', + }, + { + q: 'What if a channel does not play?', + a: 'Open it in MPV or VLC via Settings › Playback, or enable the experimental embedded MPV engine. Providers that require a specific User-Agent get it from the playlist details.', + }, + { + q: 'Does IPTVnator provide playlists?', + a: 'No. IPTVnator is a player. You bring the playlist you already have from your provider, and nothing in it is sent anywhere except to the stream hosts it points at.', + }, +]; + +const jsonLd = buildFeaturePageSchema({ + feature, + pageUrl, + description, + featureList: [ + 'M3U and M3U8 playlist import from URL, file or text', + 'Virtualized channel list for tens of thousands of channels', + 'Channel groups, favorites and recent history', + 'XMLTV program guide with catch-up', + 'Radio entries in a dedicated audio layout', + 'DASH playback with ClearKey keys', + 'MPV and VLC external player handoff', + ], + faq, +}); +--- + + + + + + + + + + + + + + + + + +
+ +
+
+ + + +
+ +
+
+ + +
diff --git a/apps/website/src/pages/features/remote-control.astro b/apps/website/src/pages/features/remote-control.astro new file mode 100644 index 000000000..6ff3a57e6 --- /dev/null +++ b/apps/website/src/pages/features/remote-control.astro @@ -0,0 +1,151 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import FaqAccordion from '../../components/blog/FaqAccordion.astro'; +import StepRail from '../../components/blog/StepRail.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 SpecList from '../../components/download/SpecList.astro'; +import FeatureHero from '../../components/feature/FeatureHero.astro'; +import FeatureSwitcher from '../../components/feature/FeatureSwitcher.astro'; +import type { FaqEntry } from '../../lib/download-schema'; +import { buildFeaturePageSchema } from '../../lib/feature-schema'; +import { featureBySlug } from '../../lib/features'; +import { absoluteUrl } from '../../lib/site'; + +const feature = featureBySlug('remote-control'); +const pageUrl = absoluteUrl('/features/remote-control/'); +const title = 'Phone Remote Control for IPTVnator – Change Channels from the Couch'; +const description = + 'The IPTVnator desktop app serves a web remote to any phone or tablet on your network: channel up and down, direct channel numbers, volume for the built-in player and a now-playing panel with the current program. No app to install, no account, free and open source.'; + +const setup = [ + 'Open Settings › Remote control on the desktop app and enable the remote. The default port is 8765; change it if something else uses it.', + 'Scan the QR code shown in the settings with your phone, or type the displayed address into its browser. Phone and computer must be on the same network.', + 'Keep the page open. It shows what is playing and offers channel up and down, a number pad and, for the built-in player, volume.', +]; + +const remote = [ + { + title: 'Channel up and down', + description: 'Step through the list you are looking at: the whole playlist in the M3U player, the current category in a portal, the filtered collection in favorites and recent.', + }, + { + title: 'Direct channel numbers', + description: 'Type a number on the pad to jump to that position in the list, the way a set-top box remote does.', + }, + { + title: 'Now playing', + description: 'The remote shows the source, the channel name and number and the current program, refreshed every two seconds.', + }, + { + title: 'Volume', + description: 'Volume up, down and mute for the built-in players in the M3U player; external MPV or VLC keep their own volume.', + }, + { + title: 'Works with every source', + description: 'M3U playlists, Xtream live TV, Stalker live TV and radio, and the favorites, recent and global collections all publish their status to the remote.', + }, + { + title: 'Nothing to install', + description: 'The remote is a small web page served by the desktop app itself. Any phone or tablet browser works, and there is no account.', + }, +]; + +const details = [ + { term: 'Where it runs', value: 'Desktop app only. The app hosts the remote page and the small API it uses.' }, + { term: 'Address', value: 'Your computer\'s local IP address and the configured port, shown as text and as a QR code in the settings.' }, + { term: 'Default port', value: '8765, configurable in Settings › Remote control.' }, + { term: 'Network', value: 'Local network only. The remote has no authentication or encryption, so keep it off networks you do not trust.' }, + { term: 'Numbers', value: 'A number selects the channel at that position in the active list, not a channel number from the guide.' }, + { term: 'Updates', value: 'The remote polls the app every two seconds; a channel change on the desktop appears on the phone right after.' }, +]; + +const faq: FaqEntry[] = [ + { + q: 'Do I need to install an app on my phone?', + a: 'No. The remote is a web page served by the IPTVnator desktop app. Open the address or scan the QR code from the settings in any phone or tablet browser.', + }, + { + q: 'Does the remote work over the internet?', + a: 'No. It is meant for the local network, where your phone and computer share a Wi-Fi, and it has no authentication or encryption. Do not expose the port to the internet.', + }, + { + q: 'Which players support volume from the remote?', + a: 'The built-in web players in the M3U player. MPV, VLC and the embedded MPV engine keep their own volume, and portal views currently pass only channel commands.', + }, + { + q: 'What does a channel number mean on the remote?', + a: 'The position in the list you are currently looking at, counted from one, like the numbering in the sidebar. It is not the channel number a provider may print in its guide.', + }, + { + q: 'Does the remote work with the browser version of IPTVnator?', + a: 'No. Serving the remote page requires the desktop app, which hosts the small web server behind it.', + }, +]; + +const jsonLd = buildFeaturePageSchema({ + feature, + pageUrl, + description, + featureList: [ + 'Web remote served by the desktop app to phones on the local network', + 'Channel up, channel down and direct channel numbers', + 'Now-playing status with source, channel and current program', + 'Volume control for the built-in players', + 'QR code and address shown in the settings', + ], + faq, +}); +--- + + + + + +
+ +
+
+ + + + + + + + + + +
+ +
+
+ + + +
+ +
+
+ + +
diff --git a/apps/website/src/pages/features/stalker-portal-player.astro b/apps/website/src/pages/features/stalker-portal-player.astro new file mode 100644 index 000000000..db4fc15da --- /dev/null +++ b/apps/website/src/pages/features/stalker-portal-player.astro @@ -0,0 +1,192 @@ +--- +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 SpecList from '../../components/download/SpecList.astro'; +import FeatureHero from '../../components/feature/FeatureHero.astro'; +import FeatureSwitcher from '../../components/feature/FeatureSwitcher.astro'; +import type { FaqEntry } from '../../lib/download-schema'; +import { buildFeaturePageSchema } from '../../lib/feature-schema'; +import { featureBySlug } from '../../lib/features'; +import { absoluteUrl } from '../../lib/site'; + +const feature = featureBySlug('stalker-portal-player'); +const pageUrl = absoluteUrl('/features/stalker-portal-player/'); +const title = 'Stalker Portal Player for Windows, macOS and Linux – IPTVnator'; +const description = + 'IPTVnator is a free, open-source Stalker and Ministra portal player for the desktop: MAC address identity like a MAG box, automatic endpoint and portal-type detection, live TV with a program guide, radio, movies and series in every portal shape, and an account page with your expiry date.'; + +const capabilities = [ + { + title: 'Behaves like a MAG box', + description: + 'Handshake, profile and token refresh with the MAC address as identity, the same headers a set-top box sends, and a watchdog that keeps full Ministra portals happy.', + }, + { + title: 'Finds the right endpoint', + description: + 'Enter a bare host, /c, portal.php or server/load.php. IPTVnator probes the usual endpoints, classifies the portal by how it behaves, and repairs the stored connection if the provider migrates.', + }, + { + title: 'Live TV and radio', + description: + 'Categories, channels with the current program and a progress bar, the complete channel list cached for search, and a dedicated audio layout for radio stations.', + }, + { + title: 'Program guide and catch-up', + description: + 'The portal schedule is loaded for a week and shown in the list and in the timeline under the player. Archived channels play past programs from the timeline.', + }, + { + title: 'Movies and series, all three shapes', + description: + 'Regular series, movies with embedded episodes and Ministra lazily loaded seasons all render as seasons and episodes with resume positions and watched markers.', + }, + { + title: 'Account info', + description: + 'Status, login, tariff plan, expiry with a days-left counter and portal details, shown instantly from the import snapshot and refreshed from the portal.', + }, + { + title: 'Device identity, on your terms', + description: + 'Optional serial number, device IDs and signatures are sent only when you enter them. Device IDs can be derived the StbEmu way at import, and stored IDs are protected from accidental changes.', + }, + { + title: 'Fresh links, never stored', + description: + 'Temporary stream links are requested from the portal for every playback and never persisted, so favorites and history keep working when links expire.', + }, + { + title: 'Favorites, recent, search, downloads', + description: + 'Per-portal collections plus global favorites, recent items and search across all sources. The desktop app downloads movies and episodes for offline viewing.', + }, +]; + +const details = [ + { term: 'Portal URL', value: 'Any http or https address: bare host, /c, portal.php or server/load.php. The proven endpoint is stored with the source.' }, + { term: 'MAC address', value: 'Normalized to 00:1A:79:XX:XX:XX; dashes, dots and lowercase are accepted. Addresses outside the Infomir range get a hint, not a refusal.' }, + { term: 'Login and password', value: 'Only when the portal demands them; IPTVnator says so and retries once you fill them in.' }, + { term: 'Portal messages', value: 'Refusals are shown with the portal\'s own words: login required, login rejected, device conflict, blocked.' }, + { term: 'Program guide', value: 'Portal schedule first; XMLTV sources from the desktop settings as a fallback, plus manual channel mapping.' }, + { term: 'Browser version', value: 'Works in the self-hosted web version through its backend proxy; downloads are a desktop feature.' }, +]; + +const faq: FaqEntry[] = [ + { + q: 'What is a Stalker portal player?', + a: 'A player that speaks the Stalker or Ministra middleware protocol used by MAG set-top boxes. It authenticates with a MAC address instead of a username, fetches channel lists and movie catalogs on demand, and resolves a temporary link for every stream. IPTVnator does all of that on the desktop.', + }, + { + q: 'Which portal URL do I need?', + a: 'Whatever your provider gave you, as long as it starts with http or https. IPTVnator tries the usual endpoints behind that address and keeps the one that answers, so a bare host, /c, portal.php or server/load.php all work.', + }, + { + q: 'Do I need a real MAG box or a serial number?', + a: 'No. The MAC address the provider registered is enough for most portals. Serial number, device IDs and signatures are optional and only needed when the provider registered a specific box and told you the values.', + }, + { + q: 'Does IPTVnator support Ministra portals with a login?', + a: 'Yes. When a portal asks for a login and password on top of the MAC address, IPTVnator reports it, and the optional username and password fields in the Add playlist dialog complete the authentication.', + }, + { + q: 'Does the program guide and catch-up work with portals?', + a: 'Yes. The portal schedule is loaded for a week and shown in the channel list and the timeline under the player. Channels the portal marks as archived can be played from the timeline.', + }, + { + q: 'Does IPTVnator provide portal accounts?', + a: 'No. IPTVnator is a player. The portal URL and MAC address come from the provider you already have a contract with, and they are only sent to that portal.', + }, +]; + +const jsonLd = buildFeaturePageSchema({ + feature, + pageUrl, + description, + featureList: [ + 'Stalker and Ministra portal login by MAC address', + 'Automatic endpoint discovery and portal-type detection', + 'Live TV with portal program guide and catch-up', + 'Radio stations in a dedicated audio layout', + 'Movies and series in regular, embedded and lazy-season shapes', + 'Account status, tariff plan and expiry information', + 'Optional device identity fields with derivation from the MAC', + ], + faq, +}); +--- + + + + + + + + + +
+
+

+ The Add playlist dialog asks for a title, the portal URL and the MAC address. Everything else is optional and + stays empty unless your provider gave you values. When you click Add, IPTVnator probes the portal, authenticates + with your MAC address, saves the endpoint and mode that worked, and opens the source. +

+

+ Providers that migrate their portal do not break the source: on the first failure IPTVnator re-probes once and + repairs the stored connection. +

+
+
+ Add playlist dialog with the Stalker portal method, a portal URL and a MAC address +
+
+
+ + + + + + +
+ +
+
+ + + +
+ +
+
+ + +
diff --git a/apps/website/src/pages/features/xtream-codes-player.astro b/apps/website/src/pages/features/xtream-codes-player.astro new file mode 100644 index 000000000..7d461fb6e --- /dev/null +++ b/apps/website/src/pages/features/xtream-codes-player.astro @@ -0,0 +1,193 @@ +--- +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 SpecList from '../../components/download/SpecList.astro'; +import FeatureHero from '../../components/feature/FeatureHero.astro'; +import FeatureSwitcher from '../../components/feature/FeatureSwitcher.astro'; +import type { FaqEntry } from '../../lib/download-schema'; +import { buildFeaturePageSchema } from '../../lib/feature-schema'; +import { featureBySlug } from '../../lib/features'; +import { absoluteUrl } from '../../lib/site'; + +const feature = featureBySlug('xtream-codes-player'); +const pageUrl = absoluteUrl('/features/xtream-codes-player/'); +const title = 'Xtream Codes Player for Desktop – Live TV, Movies and Series | IPTVnator'; +const description = + 'IPTVnator is a free, open-source Xtream Codes player for Windows, macOS and Linux: live channels with the portal program guide and catch-up, movies and series with posters and details, offline downloads, a local catalog that opens instantly, and an account page with your expiry date.'; + +const unlocked = [ + { + title: 'Live TV with a guide', + description: + 'Categories, channels with the current program and a timeline under the player, fed by the portal schedule. Archived channels carry a catch-up badge and play from the timeline.', + }, + { + title: 'Movies and series', + description: + 'Poster grids per category and a detail page with description, cast, trailer and similar titles. Seasons are tabs, episodes remember where you stopped, and whole seasons can be marked watched.', + }, + { + title: 'A local catalog', + description: + 'The desktop app stores the categories and content in a local library, so a source opens instantly and only changed data is downloaded on the next sync.', + }, + { + title: 'Offline downloads', + description: + 'Save movies and episodes to disk from the detail page. The download manager resumes interrupted transfers and keeps a library of finished files.', + }, + { + title: 'Search, favorites, recent', + description: + 'Per-source search, favorites and history, plus a Recently added section for new content and global collections across all your sources.', + }, + { + title: 'Metadata and discovery', + description: + 'Optional TMDB enrichment adds artwork, ratings and trailers, links actors to their filmography, and opens a Discover page by year, genre and country.', + }, + { + title: 'Account info', + description: + 'Status, expiry date, active and maximum connections, trial flag and allowed formats in one dialog; the dashboard shows an expiry chip during the last week.', + }, + { + title: 'Refresh without losing data', + description: + 'Refresh a source from the provider while favorites, watch history, hidden categories and playback positions stay in place.', + }, + { + title: 'The same movie, several sources', + description: + 'With more than one Xtream source, a movie found in several of them shows a Sources chip, and playback can switch to another copy or fail over automatically.', + }, +]; + +const connection = [ + { term: 'What you enter', value: 'Server URL, username and password. A pasted get.php or player_api.php link fills the fields for you.' }, + { term: 'Connection test', value: 'Before adding, IPTVnator asks the portal for the account status and reports active, expired, inactive or unreachable.' }, + { term: 'Stream formats', value: 'Live streams use HLS when the portal allows it and fall back to MPEG-TS; a manual format setting exists under Settings › Playback.' }, + { term: 'Program guide', value: 'The portal schedule is used first. On the desktop app, your own XMLTV sources fill missing channels or take precedence, as you prefer.' }, + { term: 'User-Agent', value: 'A provider-compatible IPTV player signature by default; a custom one per playlist on the desktop app.' }, + { term: 'Browser version', value: 'Works in the self-hosted web version through its backend proxy; the local catalog and downloads are desktop features.' }, +]; + +const faq: FaqEntry[] = [ + { + q: 'What is an Xtream Codes player?', + a: 'A player that talks to the Xtream Codes API most IPTV panels expose: it signs in with a server URL, username and password and receives categories, channels, movies, series, the program guide and catch-up from the provider. IPTVnator is one, free and open source.', + }, + { + q: 'Does IPTVnator work with any Xtream Codes provider?', + a: 'It works with panels that implement the Xtream Codes API, including the common variants in account-info actions, catch-up URL shapes and stream formats. The connection test in the Add playlist dialog tells you immediately whether the portal answers.', + }, + { + q: 'Can I watch movies and series, not only live TV?', + a: 'Yes. Movies and series are first-class: poster grids, detail pages with descriptions and cast, seasons and episodes with resume positions, and on the desktop app a download button for offline viewing.', + }, + { + q: 'Does the program guide and catch-up work?', + a: 'Yes. The portal schedule is shown in the channel list and in a timeline under the player, and channels the provider archives can be played from the timeline. The timeline shows how many days of archive a channel has.', + }, + { + q: 'Can I add more than one Xtream account?', + a: 'Yes. Every account is its own source with separate favorites, history and cached catalog. The dashboard, global favorites, recent items and search combine them.', + }, + { + q: 'Does IPTVnator sell Xtream Codes subscriptions?', + a: 'No. IPTVnator is a player. The server URL, username and password come from the provider you already have a contract with, and they are only sent to that server.', + }, +]; + +const jsonLd = buildFeaturePageSchema({ + feature, + pageUrl, + description, + featureList: [ + 'Xtream Codes API login with connection test', + 'Live TV with portal program guide and catch-up', + 'Movies and series with posters, details and resume', + 'Local catalog cache for instant source switching', + 'Offline downloads of movies and episodes', + 'Account status and expiry information', + 'Optional TMDB metadata and Discover pages', + ], + faq, +}); +--- + + + + + + + + + +
+
+

+ A series opens with its seasons as tabs and the episodes of the selected season below, with stills and + descriptions from the provider or from TMDB when enrichment is on. IPTVnator remembers the position in every + episode, offers Resume and Play next episode, and shows an Up Next rail beside the player while an episode is + playing. +

+

+ Seasons and whole series can be marked watched in one action, and on the desktop app a season can be downloaded + episode by episode for offline viewing. +

+
+
+ Series details with season tabs and the episode list of a season +
+
+
+ + + + + + +
+ +
+
+ + + +
+ +
+
+ + +
diff --git a/tools/testing/website-feature-pages.test.mjs b/tools/testing/website-feature-pages.test.mjs new file mode 100644 index 000000000..6a27f20e2 --- /dev/null +++ b/tools/testing/website-feature-pages.test.mjs @@ -0,0 +1,69 @@ +import { readFile } from 'node:fs/promises'; +import { test } from 'node:test'; +import assert from 'node:assert/strict'; + +/** + * Structural checks for the feature landing pages: canonical URLs, the + * SoftwareApplication + FAQPage + BreadcrumbList structured data, links to + * the download hub and a setup guide, the hub's coverage, and sitemap entries. + */ + +const distRoot = new URL('../../dist/apps/website/', import.meta.url); +const SITE = 'https://4gray.github.io/iptvnator'; +const FEATURES = ['m3u-player', 'xtream-codes-player', 'stalker-portal-player', 'epg', 'remote-control']; + +const readDist = (relativePath) => readFile(new URL(relativePath, distRoot), 'utf8'); + +function extractJsonLd(html) { + const blocks = [...html.matchAll(/