diff --git a/.github/workflows/website-evidence-capture.yml b/.github/workflows/website-evidence-capture.yml new file mode 100644 index 0000000..a2d69bc --- /dev/null +++ b/.github/workflows/website-evidence-capture.yml @@ -0,0 +1,62 @@ +name: Capture Sui Website Evidence + +on: + push: + branches: + - website-evidence-capture-20260804-v2 + paths: + - .github/workflows/website-evidence-capture.yml + - website-evidence/capture.mjs + workflow_dispatch: + +permissions: + contents: write + +jobs: + capture: + runs-on: ubuntu-latest + timeout-minutes: 25 + + steps: + - name: Check out capture branch + uses: actions/checkout@v4 + with: + ref: website-evidence-capture-20260804-v2 + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install browser and image tooling + working-directory: website-evidence + run: | + npm init -y + npm install --no-audit --no-fund playwright@1.57.0 sharp@0.33.5 + npx playwright install --with-deps chromium + + - name: Capture live website screenshots + run: node website-evidence/capture.mjs + + - name: Verify generated evidence + run: | + find website-evidence-output -maxdepth 3 -type f -printf '%p\t%s bytes\n' | sort + test -f website-evidence-output/README.txt + test -f website-evidence-output/MANIFEST.json + + - name: Upload evidence artifact + uses: actions/upload-artifact@v4 + with: + name: SUI_WEBSITE_EVIDENCE_2026-08-04 + path: website-evidence-output + if-no-files-found: error + retention-days: 7 + + - name: Commit captures to the evidence branch + run: | + git config user.name 'Sui Evidence Bot' + git config user.email 'actions@users.noreply.github.com' + git add -f website-evidence-output + git commit -m 'Capture live Sui website evidence [skip ci]' + git push origin HEAD:website-evidence-capture-20260804-v2 diff --git a/website-evidence-output/CAPTURE_LOG.txt b/website-evidence-output/CAPTURE_LOG.txt new file mode 100644 index 0000000..f21bbec --- /dev/null +++ b/website-evidence-output/CAPTURE_LOG.txt @@ -0,0 +1,28 @@ +OPEN https://www.dtsinemkarakaskalak.com/ +ROUTE_ERROR https://www.dtsinemkarakaskalak.com/: page.goto: net::ERR_NAME_NOT_RESOLVED at https://www.dtsinemkarakaskalak.com/ +Call log: + - navigating to "https://www.dtsinemkarakaskalak.com/", waiting until "domcontentloaded" + +OPEN https://dtsinemkarakaskalak.com/ +ROUTE_ERROR https://dtsinemkarakaskalak.com/: page.goto: net::ERR_NAME_NOT_RESOLVED at https://dtsinemkarakaskalak.com/ +Call log: + - navigating to "https://dtsinemkarakaskalak.com/", waiting until "domcontentloaded" + +OPEN https://sinemkarakas.com/ +SAVED RAW_FULL_PAGE/01_Website_Home_Full_Page_2400px.png 2400x4300 +SAVED CROPPED_EVIDENCE/01_Website_Home_Header_Hero_HD_CROP.png 2400x1600 +SKIP language selector crop: no visible control matched +OPEN https://sinemkarakas.com/# +SAVED RAW_FULL_PAGE/02_EN_Home_Full_Page_2400px.png 2400x4300 +SAVED CROPPED_EVIDENCE/03_EN_Home_Header_Hero_HD_CROP.png 2400x1600 +SKIP CROPPED_EVIDENCE/04_EN_International_Patients_Note_CROP.png: text not found: /Patients travelling from abroad are welcome/i +SKIP CROPPED_EVIDENCE/05_EN_Contact_Address_Hours_CROP.png: text not found: /Begin a conversation/i +OPEN https://sinemkarakas.com/en/patient-journey +ROUTE_REJECTED https://sinemkarakas.com/en/patient-journey status=404 chars=566 +OPEN https://sinemkarakas.com/en/hasta-yolculugu +ROUTE_REJECTED https://sinemkarakas.com/en/hasta-yolculugu status=404 chars=566 +OPEN https://sinemkarakas.com/patient-journey +ROUTE_REJECTED https://sinemkarakas.com/patient-journey status=404 chars=566 +OPEN https://sinemkarakas.com/ +OPEN https://sinemkarakas.com/# +SAVED CROPPED_EVIDENCE/07_DE_Home_Header_Hero_HD_CROP.png 2400x1600 diff --git a/website-evidence-output/CROPPED_EVIDENCE/01_Website_Home_Header_Hero_HD_CROP.png b/website-evidence-output/CROPPED_EVIDENCE/01_Website_Home_Header_Hero_HD_CROP.png new file mode 100644 index 0000000..11a591b Binary files /dev/null and b/website-evidence-output/CROPPED_EVIDENCE/01_Website_Home_Header_Hero_HD_CROP.png differ diff --git a/website-evidence-output/CROPPED_EVIDENCE/03_EN_Home_Header_Hero_HD_CROP.png b/website-evidence-output/CROPPED_EVIDENCE/03_EN_Home_Header_Hero_HD_CROP.png new file mode 100644 index 0000000..11a591b Binary files /dev/null and b/website-evidence-output/CROPPED_EVIDENCE/03_EN_Home_Header_Hero_HD_CROP.png differ diff --git a/website-evidence-output/CROPPED_EVIDENCE/07_DE_Home_Header_Hero_HD_CROP.png b/website-evidence-output/CROPPED_EVIDENCE/07_DE_Home_Header_Hero_HD_CROP.png new file mode 100644 index 0000000..11a591b Binary files /dev/null and b/website-evidence-output/CROPPED_EVIDENCE/07_DE_Home_Header_Hero_HD_CROP.png differ diff --git a/website-evidence-output/DISCOVERED_LINKS.json b/website-evidence-output/DISCOVERED_LINKS.json new file mode 100644 index 0000000..34aa852 --- /dev/null +++ b/website-evidence-output/DISCOVERED_LINKS.json @@ -0,0 +1,321 @@ +{ + "alternates": [], + "languageControls": [ + { + "tag": "A", + "text": "English", + "href": "https://sinemkarakas.com/#", + "ariaLabel": null, + "title": null + }, + { + "tag": "A", + "text": "Türkçe", + "href": "https://sinemkarakas.com/#", + "ariaLabel": null, + "title": null + } + ], + "links": [ + { + "text": "", + "href": "https://sinemkarakas.com/" + }, + { + "text": "", + "href": "https://sinemkarakas.com/#" + }, + { + "text": "", + "href": "https://sinemkarakas.com/" + }, + { + "text": "", + "href": "https://sinemkarakas.com/#" + }, + { + "text": "Anasayfa", + "href": "https://sinemkarakas.com/" + }, + { + "text": "Hizmetlerimiz", + "href": "https://sinemkarakas.com/#" + }, + { + "text": "Diş Çekimi", + "href": "https://sinemkarakas.com/dis-cekimi/" + }, + { + "text": "Diş Dolgusu", + "href": "https://sinemkarakas.com/dis-dolgusu/" + }, + { + "text": "Diş Beyazlatma", + "href": "https://sinemkarakas.com/dis-beyazlatma/" + }, + { + "text": "İmplant Tedavisi", + "href": "https://sinemkarakas.com/implant-tedavisi/" + }, + { + "text": "Kanal Tedavi Uygulaması", + "href": "https://sinemkarakas.com/kanal-tedavi-uygulamasi/" + }, + { + "text": "Protez Uygulamaları", + "href": "https://sinemkarakas.com/protez-uygulamalari/" + }, + { + "text": "Çocuk Diş Tedavisi", + "href": "https://sinemkarakas.com/cocuk-dis-tedavisi/" + }, + { + "text": "Diş Teli Uygulaması", + "href": "https://sinemkarakas.com/dis-teli-uygulamasi/" + }, + { + "text": "Hakkımda", + "href": "https://sinemkarakas.com/hakkimda/" + }, + { + "text": "Sağlık Turizmi", + "href": "https://sinemkarakas.com/saglik-turizmi/" + }, + { + "text": "İletişim", + "href": "https://sinemkarakas.com/#footer" + }, + { + "text": "Protez Uygulamaları", + "href": "https://sinemkarakas.com/protez-uygulamalari/" + }, + { + "text": "Çocuk Diş Tedavisi", + "href": "https://sinemkarakas.com/cocuk-dis-tedavisi/" + }, + { + "text": "Diş Teli Uygulaması", + "href": "https://sinemkarakas.com/dis-teli-uygulamasi/" + }, + { + "text": "Diş Dolgusu", + "href": "https://sinemkarakas.com/dis-dolgusu/" + }, + { + "text": "Diş Beyazlatma", + "href": "https://sinemkarakas.com/dis-beyazlatma/" + }, + { + "text": "Kanal Tedavi Uygulaması", + "href": "https://sinemkarakas.com/kanal-tedavi-uygulamasi/" + }, + { + "text": "Diş Çekimi", + "href": "https://sinemkarakas.com/dis-cekimi/" + }, + { + "text": "İmplant Tedavisi", + "href": "https://sinemkarakas.com/implant-tedavisi/" + }, + { + "text": "", + "href": "https://sinemkarakas.com/" + }, + { + "text": "Anasayfa", + "href": "https://sinemkarakas.com/" + }, + { + "text": "Hizmetlerimiz", + "href": "https://sinemkarakas.com/#" + }, + { + "text": "Diş Çekimi", + "href": "https://sinemkarakas.com/dis-cekimi/" + }, + { + "text": "Diş Dolgusu", + "href": "https://sinemkarakas.com/dis-dolgusu/" + }, + { + "text": "Diş Beyazlatma", + "href": "https://sinemkarakas.com/dis-beyazlatma/" + }, + { + "text": "İmplant Tedavisi", + "href": "https://sinemkarakas.com/implant-tedavisi/" + }, + { + "text": "Kanal Tedavi Uygulaması", + "href": "https://sinemkarakas.com/kanal-tedavi-uygulamasi/" + }, + { + "text": "Protez Uygulamaları", + "href": "https://sinemkarakas.com/protez-uygulamalari/" + }, + { + "text": "Çocuk Diş Tedavisi", + "href": "https://sinemkarakas.com/cocuk-dis-tedavisi/" + }, + { + "text": "Diş Teli Uygulaması", + "href": "https://sinemkarakas.com/dis-teli-uygulamasi/" + }, + { + "text": "Hakkımda", + "href": "https://sinemkarakas.com/hakkimda/" + }, + { + "text": "Sağlık Turizmi", + "href": "https://sinemkarakas.com/saglik-turizmi/" + }, + { + "text": "İletişim", + "href": "https://sinemkarakas.com/#footer" + }, + { + "text": "", + "href": "https://sinemkarakas.com/" + }, + { + "text": "0540 784 33 68", + "href": "tel://05407843368" + }, + { + "text": "HEMEN ARA", + "href": "tel://05407843368" + }, + { + "text": "", + "href": "https://sinemkarakas.com/implant-tedavisi/" + }, + { + "text": "İmplant", + "href": "https://sinemkarakas.com/implant-tedavisi/" + }, + { + "text": "", + "href": "https://sinemkarakas.com/dis-cekimi/" + }, + { + "text": "Diş Çekimi", + "href": "https://sinemkarakas.com/dis-cekimi/" + }, + { + "text": "", + "href": "https://sinemkarakas.com/kanal-tedavi-uygulamasi/" + }, + { + "text": "Kanal tedavisi", + "href": "https://sinemkarakas.com/kanal-tedavi-uygulamasi/" + }, + { + "text": "", + "href": "https://sinemkarakas.com/dis-beyazlatma/" + }, + { + "text": "Diş Beyazlatma", + "href": "https://sinemkarakas.com/dis-beyazlatma/" + }, + { + "text": "", + "href": "https://sinemkarakas.com/dis-dolgusu/" + }, + { + "text": "Diş Dolgusu", + "href": "https://sinemkarakas.com/dis-dolgusu/" + }, + { + "text": "", + "href": "https://sinemkarakas.com/dis-teli-uygulamasi/" + }, + { + "text": "Diş Teli", + "href": "https://sinemkarakas.com/dis-teli-uygulamasi/" + }, + { + "text": "", + "href": "https://sinemkarakas.com/cocuk-dis-tedavisi/" + }, + { + "text": "Çocuk Diş", + "href": "https://sinemkarakas.com/cocuk-dis-tedavisi/" + }, + { + "text": "", + "href": "https://sinemkarakas.com/protez-uygulamalari/" + }, + { + "text": "Protez Uygulamaları", + "href": "https://sinemkarakas.com/protez-uygulamalari/" + }, + { + "text": "OKUMAK İÇİN TIKLAYINIZ", + "href": "https://sinemkarakas.com/#hakkimda" + }, + { + "text": "Fenerbahçe, Operatör Cemil Topuzlu Cd. No: 31/7, Kadıköy/İstanbul", + "href": "https://maps.app.goo.gl/5TVQwBkhKXmGzCNq8" + }, + { + "text": "DEVAMINI OKU", + "href": "https://sinemkarakas.com/#hakkimda" + }, + { + "text": "HEMEN ARA", + "href": "tel://05407843368" + }, + { + "text": "İmplant", + "href": "https://sinemkarakas.com/implant-tedavisi/" + }, + { + "text": "Diş Çekimi", + "href": "https://sinemkarakas.com/dis-cekimi/" + }, + { + "text": "Kanal Tedavisi", + "href": "https://sinemkarakas.com/kanal-tedavi-uygulamasi/" + }, + { + "text": "Dolgu Uygulamaları", + "href": "https://sinemkarakas.com/dis-dolgusu/" + }, + { + "text": "0540 784 33 68", + "href": "tel://05407843368" + }, + { + "text": "info@dtsinemkarakas.com", + "href": "mailto://%20info@dtsinemkarakas.com" + }, + { + "text": "Fenerbahçe, Operatör Cemil Topuzlu Cd. No: 31/7, Kadıköy/İstanbul", + "href": "https://maps.app.goo.gl/5TVQwBkhKXmGzCNq8" + }, + { + "text": "COPYRIGHT 2025 Via Danışmanlık - DReklam İş Ortaklığı İle Tasarlanmıştır.", + "href": "https://viadanismanlik.com/" + }, + { + "text": "Son Güncelleme Tarihi: 27.12.2025", + "href": "https://viadanismanlik.com/" + }, + { + "text": "Register", + "href": "https://sinemkarakas.com/dreklam-web/?action=register&redirect_to=https://sinemkarakas.com/" + }, + { + "text": "English", + "href": "https://sinemkarakas.com/#" + }, + { + "text": "Deutsch", + "href": "https://sinemkarakas.com/#" + }, + { + "text": "Türkçe", + "href": "https://sinemkarakas.com/#" + } + ] +} \ No newline at end of file diff --git a/website-evidence-output/MANIFEST.json b/website-evidence-output/MANIFEST.json new file mode 100644 index 0000000..c4cb2e8 --- /dev/null +++ b/website-evidence-output/MANIFEST.json @@ -0,0 +1,71 @@ +{ + "generatedAt": "2026-08-04T09:06:08.758Z", + "declaredWebsiteCandidates": [ + "https://www.dtsinemkarakaskalak.com", + "https://dtsinemkarakaskalak.com", + "https://sinemkarakas.com" + ], + "viewport": { + "width": 2400, + "height": 1600 + }, + "captures": [ + { + "file": "RAW_FULL_PAGE/01_Website_Home_Full_Page_2400px.png", + "width": 2400, + "height": 4300, + "url": "https://sinemkarakas.com/", + "title": "Anasayfa - DT. SİNEM KARAKAŞ KALAK" + }, + { + "file": "CROPPED_EVIDENCE/01_Website_Home_Header_Hero_HD_CROP.png", + "width": 2400, + "height": 1600, + "url": "https://sinemkarakas.com/", + "evidence": "Declared website homepage, brand identity and navigation" + }, + { + "file": "RAW_FULL_PAGE/02_EN_Home_Full_Page_2400px.png", + "width": 2400, + "height": 4300, + "url": "https://sinemkarakas.com/#", + "title": "Anasayfa - DT. SİNEM KARAKAŞ KALAK" + }, + { + "file": "CROPPED_EVIDENCE/03_EN_Home_Header_Hero_HD_CROP.png", + "width": 2400, + "height": 1600, + "url": "https://sinemkarakas.com/#", + "evidence": "English-language website page" + }, + { + "file": "CROPPED_EVIDENCE/07_DE_Home_Header_Hero_HD_CROP.png", + "width": 2400, + "height": 1600, + "url": "https://sinemkarakas.com/#", + "evidence": "German-language website page" + } + ], + "warnings": [], + "selectedWebsite": "https://sinemkarakas.com/", + "homeTitle": "Anasayfa - DT. SİNEM KARAKAŞ KALAK", + "englishCandidates": [ + "https://sinemkarakas.com/#", + "https://sinemkarakas.com/en", + "https://sinemkarakas.com/en/", + "https://sinemkarakas.com/?lang=en", + "https://sinemkarakas.com/?locale=en", + "https://sinemkarakas.com/english" + ], + "englishWebsite": "https://sinemkarakas.com/#", + "germanCandidates": [ + "https://sinemkarakas.com/#", + "https://sinemkarakas.com/de", + "https://sinemkarakas.com/de/", + "https://sinemkarakas.com/?lang=de", + "https://sinemkarakas.com/?locale=de", + "https://sinemkarakas.com/german" + ], + "germanWebsite": "https://sinemkarakas.com/#", + "captureCount": 5 +} \ No newline at end of file diff --git a/website-evidence-output/RAW_FULL_PAGE/01_Website_Home_Full_Page_2400px.png b/website-evidence-output/RAW_FULL_PAGE/01_Website_Home_Full_Page_2400px.png new file mode 100644 index 0000000..304ff80 Binary files /dev/null and b/website-evidence-output/RAW_FULL_PAGE/01_Website_Home_Full_Page_2400px.png differ diff --git a/website-evidence-output/RAW_FULL_PAGE/02_EN_Home_Full_Page_2400px.png b/website-evidence-output/RAW_FULL_PAGE/02_EN_Home_Full_Page_2400px.png new file mode 100644 index 0000000..304ff80 Binary files /dev/null and b/website-evidence-output/RAW_FULL_PAGE/02_EN_Home_Full_Page_2400px.png differ diff --git a/website-evidence-output/README.txt b/website-evidence-output/README.txt new file mode 100644 index 0000000..8fff283 --- /dev/null +++ b/website-evidence-output/README.txt @@ -0,0 +1,17 @@ +SUI / DT. SINEM KARAKAŞ — WEBSITE EVIDENCE + +Declared website candidates: +- https://www.dtsinemkarakaskalak.com +- https://dtsinemkarakaskalak.com +- https://sinemkarakas.com + +Selected live website: https://sinemkarakas.com/ +Captured: 2026-08-04T09:06:08.758Z +Viewport: 2400 × 1600px +Format: lossless PNG +Capture count: 5 + +RAW_FULL_PAGE contains un-cropped full-page captures. +CROPPED_EVIDENCE contains clean, tightly framed evidence images suitable for the TÜSKA documentation file. + +The files were captured from publicly accessible live pages by an automated Chromium browser. Page content was not fabricated or rewritten. Motion was disabled only to prevent animation blur. Any missing language page is recorded transparently in MANIFEST.json and CAPTURE_LOG.txt. diff --git a/website-evidence/capture.mjs b/website-evidence/capture.mjs new file mode 100644 index 0000000..1f0b274 --- /dev/null +++ b/website-evidence/capture.mjs @@ -0,0 +1,304 @@ +import { chromium } from 'playwright'; +import sharp from 'sharp'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +const DECLARED_BASES = [ + 'https://www.dtsinemkarakaskalak.com', + 'https://dtsinemkarakaskalak.com', + 'https://sinemkarakas.com', +]; +const OUT = path.resolve('website-evidence-output'); +const RAW = path.join(OUT, 'RAW_FULL_PAGE'); +const CROPPED = path.join(OUT, 'CROPPED_EVIDENCE'); +const WIDTH = 2400; +const HEIGHT = 1600; + +await fs.rm(OUT, { recursive: true, force: true }); +await fs.mkdir(RAW, { recursive: true }); +await fs.mkdir(CROPPED, { recursive: true }); + +const log = []; +const discovered = { alternates: [], languageControls: [], links: [] }; +const manifest = { + generatedAt: new Date().toISOString(), + declaredWebsiteCandidates: DECLARED_BASES, + viewport: { width: WIDTH, height: HEIGHT }, + captures: [], + warnings: [], +}; + +function absoluteUrl(candidate, base) { + try { return new URL(candidate, base).toString(); } catch { return null; } +} + +async function preparePage(page, url) { + log.push(`OPEN ${url}`); + const response = await page.goto(url, { + waitUntil: 'domcontentloaded', + timeout: 120_000, + }); + const status = response?.status() ?? null; + await page.waitForTimeout(5000); + + for (const label of [/accept all/i, /accept/i, /kabul et/i, /tümünü kabul/i, /allow all/i]) { + const btn = page.getByRole('button', { name: label }).first(); + if (await btn.isVisible().catch(() => false)) { + await btn.click({ timeout: 3000 }).catch(() => {}); + break; + } + } + + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.addStyleTag({ + content: ` + html { scroll-behavior: auto !important; } + *, *::before, *::after { + animation-duration: 0.001ms !important; + animation-delay: 0ms !important; + transition-duration: 0.001ms !important; + transition-delay: 0ms !important; + } + .motion-hidden { opacity: 1 !important; animation: none !important; transform: none !important; } + `, + }).catch(() => {}); + + await page.evaluate(async () => { + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const total = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); + for (let y = 0; y < total; y += Math.max(600, window.innerHeight * 0.75)) { + window.scrollTo(0, y); + await sleep(140); + } + window.scrollTo(0, 0); + await sleep(500); + if (document.fonts?.ready) await document.fonts.ready; + }); + await page.waitForTimeout(1200); + + const bodyText = await page.locator('body').innerText().catch(() => ''); + return { + status, + title: await page.title(), + finalUrl: page.url(), + bodyText, + }; +} + +async function saveScreenshot(page, relPath, options, meta = {}) { + const target = path.join(OUT, relPath); + await fs.mkdir(path.dirname(target), { recursive: true }); + await page.screenshot({ + path: target, + type: 'png', + animations: 'disabled', + caret: 'hide', + ...options, + }); + const buf = await sharp(target).png({ compressionLevel: 9, adaptiveFiltering: true }).toBuffer(); + await fs.writeFile(target, buf); + const info = await sharp(buf).metadata(); + manifest.captures.push({ file: relPath, width: info.width, height: info.height, ...meta }); + log.push(`SAVED ${relPath} ${info.width}x${info.height}`); + return target; +} + +async function captureLocator(page, locator, relPath, meta = {}) { + if (!(await locator.isVisible().catch(() => false))) return false; + await locator.scrollIntoViewIfNeeded().catch(() => {}); + await page.waitForTimeout(500); + const box = await locator.boundingBox(); + if (!box || box.width < 10 || box.height < 10) return false; + const padded = { + x: Math.max(0, box.x - 40), + y: Math.max(0, box.y - 40), + width: Math.min(WIDTH, box.width + 80), + height: box.height + 80, + }; + await saveScreenshot(page, relPath, { fullPage: false, clip: padded }, meta); + return true; +} + +async function captureClosestSection(page, textRegex, relPath, url) { + const text = page.getByText(textRegex).first(); + if (!(await text.isVisible().catch(() => false))) { + log.push(`SKIP ${relPath}: text not found: ${textRegex}`); + return false; + } + let section = text.locator('xpath=ancestor::section[1]'); + if (!(await section.isVisible().catch(() => false))) section = text.locator('xpath=ancestor::div[1]'); + return captureLocator(page, section, relPath, { url, evidence: String(textRegex) }); +} + +function isAcceptablePage(meta) { + if (!meta) return false; + const short = meta.bodyText.trim().slice(0, 4000); + const looks404 = /(^|\s)404(\s|$)|page not found|sayfa bulunamadı|not found/i.test(short) && short.length < 3000; + return (meta.status === null || meta.status < 400) && !looks404 && short.length > 80; +} + +async function openFirstWorking(page, urls) { + for (const raw of urls.filter(Boolean)) { + const url = absoluteUrl(raw, page.url() || DECLARED_BASES[0]); + if (!url) continue; + try { + const meta = await preparePage(page, url); + if (isAcceptablePage(meta)) return { url: meta.finalUrl, ...meta }; + log.push(`ROUTE_REJECTED ${url} status=${meta.status} chars=${meta.bodyText.length}`); + } catch (error) { + log.push(`ROUTE_ERROR ${url}: ${error.message}`); + } + } + return null; +} + +async function discoverLinks(page) { + const data = await page.evaluate(() => { + const alternates = [...document.querySelectorAll('link[rel="alternate"][hreflang]')].map((el) => ({ + hreflang: el.getAttribute('hreflang'), + href: el.href, + })); + const controls = [...document.querySelectorAll('a, button')].map((el) => ({ + tag: el.tagName, + text: (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 120), + href: el instanceof HTMLAnchorElement ? el.href : null, + ariaLabel: el.getAttribute('aria-label'), + title: el.getAttribute('title'), + })).filter((item) => { + const hay = `${item.text} ${item.href || ''} ${item.ariaLabel || ''} ${item.title || ''}`; + return /(^|\b)(tr|en|de|english|german|türkçe|ingilizce|almanca)(\b|$)|lang=|locale=/i.test(hay); + }); + const links = [...document.querySelectorAll('a[href]')].map((el) => ({ + text: (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 160), + href: el.href, + })); + return { alternates, controls, links }; + }); + discovered.alternates = data.alternates; + discovered.languageControls = data.controls; + discovered.links = data.links; + return data; +} + +function languageCandidates(data, lang, currentUrl) { + const regex = lang === 'en' + ? /(^|\b)(en|english|ingilizce)(\b|$)|[?&](lang|locale)=en\b|\/en(\/|$)/i + : /(^|\b)(de|german|deutsch|almanca)(\b|$)|[?&](lang|locale)=de\b|\/de(\/|$)/i; + const out = []; + for (const alt of data.alternates) { + if ((alt.hreflang || '').toLowerCase().startsWith(lang)) out.push(alt.href); + } + for (const item of data.controls) { + const hay = `${item.text} ${item.href || ''} ${item.ariaLabel || ''} ${item.title || ''}`; + if (regex.test(hay) && item.href) out.push(item.href); + } + for (const item of data.links) { + const hay = `${item.text} ${item.href}`; + if (regex.test(hay)) out.push(item.href); + } + const origin = new URL(currentUrl).origin; + if (lang === 'en') { + out.push(`${origin}/en`, `${origin}/en/`, `${origin}/?lang=en`, `${origin}/?locale=en`, `${origin}/english`); + } else { + out.push(`${origin}/de`, `${origin}/de/`, `${origin}/?lang=de`, `${origin}/?locale=de`, `${origin}/german`); + } + return [...new Set(out.filter(Boolean))]; +} + +const browser = await chromium.launch({ + headless: true, + args: ['--no-sandbox', '--disable-dev-shm-usage'], +}); +const context = await browser.newContext({ + viewport: { width: WIDTH, height: HEIGHT }, + deviceScaleFactor: 1, + locale: 'en-US', + colorScheme: 'light', + ignoreHTTPSErrors: true, +}); +const page = await context.newPage(); +page.setDefaultTimeout(20_000); + +let home = null; +try { + home = await openFirstWorking(page, DECLARED_BASES); + if (!home) { + manifest.warnings.push('No declared website candidate could be opened.'); + } else { + manifest.selectedWebsite = home.url; + manifest.homeTitle = home.title; + + await saveScreenshot(page, 'RAW_FULL_PAGE/01_Website_Home_Full_Page_2400px.png', { fullPage: true }, { url: home.url, title: home.title }); + await page.evaluate(() => window.scrollTo(0, 0)); + await saveScreenshot(page, 'CROPPED_EVIDENCE/01_Website_Home_Header_Hero_HD_CROP.png', { + fullPage: false, + clip: { x: 0, y: 0, width: WIDTH, height: HEIGHT }, + }, { url: home.url, evidence: 'Declared website homepage, brand identity and navigation' }); + + const linkData = await discoverLinks(page); + const languageControl = page.locator('a,button').filter({ hasText: /^(TR|EN|DE|Türkçe|English|Deutsch|İngilizce|Almanca)$/i }).first(); + if (!(await captureLocator(page, languageControl, 'CROPPED_EVIDENCE/02_Language_Selector_CROP.png', { + url: home.url, + evidence: 'Visible language selector/control', + }))) { + log.push('SKIP language selector crop: no visible control matched'); + } + + const enCandidates = languageCandidates(linkData, 'en', home.url); + manifest.englishCandidates = enCandidates; + const en = await openFirstWorking(page, enCandidates); + if (en) { + manifest.englishWebsite = en.url; + await saveScreenshot(page, 'RAW_FULL_PAGE/02_EN_Home_Full_Page_2400px.png', { fullPage: true }, { url: en.url, title: en.title }); + await page.evaluate(() => window.scrollTo(0, 0)); + await saveScreenshot(page, 'CROPPED_EVIDENCE/03_EN_Home_Header_Hero_HD_CROP.png', { + fullPage: false, + clip: { x: 0, y: 0, width: WIDTH, height: HEIGHT }, + }, { url: en.url, evidence: 'English-language website page' }); + await captureClosestSection(page, /Patients travelling from abroad are welcome/i, 'CROPPED_EVIDENCE/04_EN_International_Patients_Note_CROP.png', en.url); + await captureClosestSection(page, /Begin a conversation/i, 'CROPPED_EVIDENCE/05_EN_Contact_Address_Hours_CROP.png', en.url); + + const journeyLinks = discovered.links.filter((item) => /patient journey|hasta yolculuğu|journey/i.test(`${item.text} ${item.href}`)).map((item) => item.href); + const origin = new URL(en.url).origin; + const journey = await openFirstWorking(page, [ + ...journeyLinks, + `${origin}/en/patient-journey`, + `${origin}/en/hasta-yolculugu`, + `${origin}/patient-journey`, + ]); + if (journey) { + await saveScreenshot(page, 'RAW_FULL_PAGE/03_EN_Patient_Journey_Full_Page_2400px.png', { fullPage: true }, { url: journey.url, title: journey.title }); + await captureClosestSection(page, /For patients travelling from abroad/i, 'CROPPED_EVIDENCE/06_EN_Patient_Journey_International_Section_CROP.png', journey.url); + } + } else { + manifest.warnings.push('No separate English page was discoverable on the live website.'); + log.push('NO_ENGLISH_PAGE_DISCOVERED'); + } + + await preparePage(page, home.url); + const refreshedLinks = await discoverLinks(page); + const deCandidates = languageCandidates(refreshedLinks, 'de', home.url); + manifest.germanCandidates = deCandidates; + const de = await openFirstWorking(page, deCandidates); + if (de) { + manifest.germanWebsite = de.url; + await page.evaluate(() => window.scrollTo(0, 0)); + await saveScreenshot(page, 'CROPPED_EVIDENCE/07_DE_Home_Header_Hero_HD_CROP.png', { + fullPage: false, + clip: { x: 0, y: 0, width: WIDTH, height: HEIGHT }, + }, { url: de.url, evidence: 'German-language website page' }); + } + } +} catch (error) { + manifest.warnings.push(`Capture error: ${error.message}`); + log.push(`FATAL_CAPTURE_ERROR ${error.stack || error.message}`); +} finally { + await browser.close(); +} + +manifest.captureCount = manifest.captures.length; +await fs.writeFile(path.join(OUT, 'MANIFEST.json'), JSON.stringify(manifest, null, 2)); +await fs.writeFile(path.join(OUT, 'DISCOVERED_LINKS.json'), JSON.stringify(discovered, null, 2)); +await fs.writeFile(path.join(OUT, 'CAPTURE_LOG.txt'), log.join('\n') + '\n'); +await fs.writeFile(path.join(OUT, 'README.txt'), `SUI / DT. SINEM KARAKAŞ — WEBSITE EVIDENCE\n\nDeclared website candidates:\n${DECLARED_BASES.map((url) => `- ${url}`).join('\n')}\n\nSelected live website: ${manifest.selectedWebsite || 'Not resolved'}\nCaptured: ${manifest.generatedAt}\nViewport: ${WIDTH} × ${HEIGHT}px\nFormat: lossless PNG\nCapture count: ${manifest.captureCount}\n\nRAW_FULL_PAGE contains un-cropped full-page captures.\nCROPPED_EVIDENCE contains clean, tightly framed evidence images suitable for the TÜSKA documentation file.\n\nThe files were captured from publicly accessible live pages by an automated Chromium browser. Page content was not fabricated or rewritten. Motion was disabled only to prevent animation blur. Any missing language page is recorded transparently in MANIFEST.json and CAPTURE_LOG.txt.\n`); +console.log(log.join('\n'));