mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
Add /download/ with Windows, macOS and Linux landing pages: OS-specific install steps, requirements, feature list, FAQ, related posts and a platform switcher, plus SoftwareApplication / FAQPage / BreadcrumbList JSON-LD on every page. Direct asset links resolve the latest published release from the GitHub Releases API at build time (asset names, sizes, publish date) and fall back to the root package.json version when the API is unreachable; WEBSITE_SKIP_RELEASE_FETCH=1 forces the fallback. The deploy workflow passes GITHUB_TOKEN to the build. The homepage download cards and the header Download link now point at the new pages, the homepage schema reads the resolved version instead of a hard-coded 0.20.0, and the locale count is corrected to 19. tools/testing/website-download-pages.test.mjs checks titles, canonicals, direct asset links, structured data, cross-links and sitemap entries of the built output; nx test website runs it beside the Giscus test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
125 lines
4.9 KiB
JavaScript
125 lines
4.9 KiB
JavaScript
import { readFile } from 'node:fs/promises';
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
/**
|
|
* Structural checks for the per-OS download pages in the built website.
|
|
* The build resolves the latest release from the GitHub API and falls back to
|
|
* package.json, so assertions accept any semver version but insist on direct
|
|
* asset links, canonical URLs, structured data and internal linking.
|
|
*/
|
|
|
|
const distRoot = new URL('../../dist/apps/website/', import.meta.url);
|
|
const SITE = 'https://4gray.github.io/iptvnator';
|
|
|
|
const readDist = (relativePath) => readFile(new URL(relativePath, distRoot), 'utf8');
|
|
|
|
const assetLink = (suffix) =>
|
|
new RegExp(
|
|
`https://github\\.com/4gray/iptvnator/releases/download/v\\d+\\.\\d+\\.\\d+/iptvnator-\\d+\\.\\d+\\.\\d+${suffix}`
|
|
);
|
|
|
|
const PAGES = {
|
|
windows: {
|
|
path: 'download/windows/index.html',
|
|
label: 'Windows',
|
|
assets: ['-windows-x64-setup\\.exe'],
|
|
operatingSystem: /Windows 10/,
|
|
},
|
|
macos: {
|
|
path: 'download/macos/index.html',
|
|
label: 'macOS',
|
|
assets: ['-mac-arm64\\.dmg', '-mac-x64\\.dmg'],
|
|
operatingSystem: /macOS/,
|
|
},
|
|
linux: {
|
|
path: 'download/linux/index.html',
|
|
label: 'Linux',
|
|
assets: [
|
|
'-linux-x86_64\\.AppImage',
|
|
'-linux-amd64\\.deb',
|
|
'-linux-x86_64\\.rpm',
|
|
'-linux-x64\\.pacman',
|
|
'-linux-x86_64\\.flatpak',
|
|
'-linux-arm64\\.AppImage',
|
|
],
|
|
operatingSystem: /Linux/,
|
|
},
|
|
};
|
|
|
|
function extractJsonLd(html) {
|
|
const blocks = [...html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)];
|
|
assert.ok(blocks.length > 0, 'Expected at least one JSON-LD script block.');
|
|
return blocks.flatMap((match) => JSON.parse(match[1]));
|
|
}
|
|
|
|
for (const [platform, page] of Object.entries(PAGES)) {
|
|
test(`${platform} download page: title, canonical and direct asset links`, async () => {
|
|
const html = await readDist(page.path);
|
|
|
|
assert.match(html, new RegExp(`<title>[^<]*IPTVnator for ${page.label}[^<]*</title>`));
|
|
assert.match(html, new RegExp(`<link rel="canonical" href="${SITE}/download/${platform}/"`));
|
|
assert.match(html, /<meta name="robots" content="index, follow/);
|
|
|
|
for (const suffix of page.assets) {
|
|
assert.match(html, assetLink(suffix), `Expected a direct link to the ${suffix} asset.`);
|
|
}
|
|
});
|
|
|
|
test(`${platform} download page: structured data`, async () => {
|
|
const html = await readDist(page.path);
|
|
const schema = extractJsonLd(html);
|
|
|
|
const app = schema.find((entry) => entry['@type'] === 'SoftwareApplication');
|
|
assert.ok(app, 'Expected a SoftwareApplication entry.');
|
|
assert.match(String(app.operatingSystem), page.operatingSystem);
|
|
assert.match(String(app.softwareVersion), /^\d+\.\d+\.\d+$/);
|
|
assert.match(String(app.downloadUrl), /^https:\/\/github\.com\/4gray\/iptvnator\/releases\//);
|
|
assert.equal(app.isAccessibleForFree, true);
|
|
|
|
const faq = schema.find((entry) => entry['@type'] === 'FAQPage');
|
|
assert.ok(faq, 'Expected a FAQPage entry.');
|
|
assert.ok(faq.mainEntity.length >= 5, 'Expected at least five FAQ questions.');
|
|
|
|
const breadcrumbs = schema.find((entry) => entry['@type'] === 'BreadcrumbList');
|
|
assert.ok(breadcrumbs, 'Expected a BreadcrumbList entry.');
|
|
assert.equal(breadcrumbs.itemListElement.at(-1).item, `${SITE}/download/${platform}/`);
|
|
});
|
|
|
|
test(`${platform} download page: links to the other platforms and the hub`, async () => {
|
|
const html = await readDist(page.path);
|
|
for (const other of Object.keys(PAGES).filter((candidate) => candidate !== platform)) {
|
|
assert.match(html, new RegExp(`href="/iptvnator/download/${other}/"`));
|
|
}
|
|
assert.match(html, /href="\/iptvnator\/download\/"/);
|
|
assert.match(html, /href="\/iptvnator\/blog\/beware-unofficial-iptvnator-websites\/"/);
|
|
});
|
|
}
|
|
|
|
test('download hub links to every platform page', async () => {
|
|
const html = await readDist('download/index.html');
|
|
assert.match(html, new RegExp(`<link rel="canonical" href="${SITE}/download/"`));
|
|
for (const platform of Object.keys(PAGES)) {
|
|
assert.match(html, new RegExp(`href="/iptvnator/download/${platform}/"`));
|
|
}
|
|
assert.match(html, /brew install --cask iptvnator/);
|
|
assert.match(html, /sudo snap install iptvnator/);
|
|
});
|
|
|
|
test('homepage download cards point at the platform pages', async () => {
|
|
const html = await readDist('index.html');
|
|
for (const platform of Object.keys(PAGES)) {
|
|
assert.match(html, new RegExp(`href="/iptvnator/download/${platform}/"`));
|
|
}
|
|
const schema = extractJsonLd(html);
|
|
const app = schema.find((entry) => entry['@type'] === 'SoftwareApplication');
|
|
assert.match(String(app.softwareVersion), /^\d+\.\d+\.\d+$/, 'Homepage schema should carry the resolved version.');
|
|
});
|
|
|
|
test('sitemap lists the download pages', async () => {
|
|
const sitemap = await readDist('sitemap-0.xml');
|
|
for (const path of ['download/', 'download/windows/', 'download/macos/', 'download/linux/']) {
|
|
assert.match(sitemap, new RegExp(`<loc>${SITE}/${path}</loc>`));
|
|
}
|
|
});
|