diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb84a49..39f440b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -37,7 +37,10 @@ jobs: run: composer install --no-interaction --prefer-dist --no-progress - name: Lint PHP - run: php -l proxy.php + run: | + for file in ./*.php; do + php -l "$file" + done - name: Validate camera data run: | @@ -62,8 +65,47 @@ jobs: echo "Validated " . count($data) . " cameras.\n"; ' - - name: Smoke-check UI anchors + - name: Validate sitemap + run: | + php sitemap.php > /tmp/camera-sitemap.xml + php -r ' + $data = json_decode(file_get_contents("data-camera.json"), true, 512, JSON_THROW_ON_ERROR); + $xml = simplexml_load_file("/tmp/camera-sitemap.xml"); + if ($xml === false) { + fwrite(STDERR, "Generated sitemap is invalid XML.\n"); + exit(1); + } + $expected = count($data) + 1; + $actual = count($xml->url); + if ($actual !== $expected) { + fwrite(STDERR, "Expected {$expected} sitemap URLs, got {$actual}.\n"); + exit(1); + } + echo "Validated {$actual} sitemap URLs.\n"; + ' + grep -q 'https://camera-hcm.eplus.dev/sitemap.php' sitemap.xml + grep -q 'Sitemap: https://camera-hcm.eplus.dev/sitemap.xml' robots.txt + + - name: Smoke-check SEO anchors run: | grep -q 'id="cameraGrid"' index.html grep -q 'id="searchInput"' index.html - grep -q 'proxy.php?id=' index.html + grep -q 'rel="canonical" href="https://camera-hcm.eplus.dev/"' index.html + grep -q 'application/ld+json' index.html + grep -q 'camera.php?id=' index.html + grep -q 'max-image-preview:large' index.html + grep -q 'favicon.svg' index.html + grep -q 'manifest.webmanifest' index.html + + - name: HTTP smoke test + run: | + FIRST_ID="$(php -r '$data=json_decode(file_get_contents("data-camera.json"), true, 512, JSON_THROW_ON_ERROR); echo $data[0]["CamId"];')" + php -S 127.0.0.1:8080 > /tmp/camera-php-server.log 2>&1 & + SERVER_PID=$! + trap 'kill "$SERVER_PID"' EXIT + sleep 1 + + curl -fsS "http://127.0.0.1:8080/" | grep -q 'Camera giao thông TP.HCM' + curl -fsS "http://127.0.0.1:8080/camera.php?id=$FIRST_ID" | grep -q 'https://camera-hcm.eplus.dev/camera.php?id=' + curl -fsS "http://127.0.0.1:8080/camera.php?id=$FIRST_ID" | grep -q 'application/ld+json' + curl -fsS "http://127.0.0.1:8080/sitemap.php" | grep -q ' + +The project exposes crawlable resources in addition to the JavaScript camera directory: + +- `camera.php?id={CamId}` — server-rendered canonical page for each camera. +- `sitemap.xml` — sitemap index. +- `sitemap.php` — dynamic sitemap containing the homepage and all camera detail pages. +- `robots.txt` — crawler directives and sitemap discovery. +- `llms.txt` — concise AI/LLM discovery file. +- `agents.md` — machine-agent guidance and data semantics. +- `manifest.webmanifest` and `favicon.svg` — app/site identity metadata. + +The homepage and detail pages include canonical URLs, social metadata and Schema.org JSON-LD. diff --git a/agents.md b/agents.md new file mode 100644 index 0000000..5aa0119 --- /dev/null +++ b/agents.md @@ -0,0 +1,31 @@ +# Agent guidance — Camera giao thông TP.HCM + +Canonical origin: https://camera-hcm.eplus.dev/ + +## Purpose + +This site helps users find and view public traffic cameras in Ho Chi Minh City by camera name and district/area. + +## Preferred discovery flow + +1. Use /sitemap.xml or /sitemap.php to discover canonical camera detail pages. +2. Use /data-camera.json for the camera catalog. +3. Use /camera.php?id={CamId} for a human-readable, server-rendered camera page. +4. Treat /proxy.php?id={CamId} as a live image endpoint, not as a stable text document. + +## Data semantics + +- CamId: stable camera identifier used by this site. +- CamName: human-readable camera/intersection name. +- Disctrict: source field containing district/area information. The misspelling is retained for backward compatibility. +- Snapshot images are time-sensitive and may fail temporarily. + +## Crawling + +- Respect robots.txt. +- Prefer canonical URLs from sitemap and rel=canonical. +- Avoid generating or indexing arbitrary pagination/search URL combinations when a canonical camera detail page is available. + +## Attribution + +Site maintained by ePlus.DEV. Camera imagery depends on the Ho Chi Minh City traffic information source. diff --git a/camera.php b/camera.php new file mode 100644 index 0000000..5b66527 --- /dev/null +++ b/camera.php @@ -0,0 +1,402 @@ +getMessage()); +} + +$camera = null; +foreach ($data as $item) { + if (($item['CamId'] ?? null) === $cameraId) { + $camera = $item; + break; + } +} + +if (!$camera) { + http_response_code(404); + header('X-Robots-Tag: noindex, nofollow'); + $pageTitle = 'Không tìm thấy camera | ePlus.DEV'; + $pageDescription = 'Camera giao thông bạn yêu cầu không tồn tại hoặc đã được gỡ.'; + $canonical = BASE_URL . '/'; + $cameraName = 'Không tìm thấy camera'; + $district = 'TP.HCM'; + $imageUrl = ''; + $related = []; +} else { + $cameraName = trim((string) $camera['CamName']); + $district = cameraDistrict($camera); + $pageTitle = 'Camera ' . $cameraName . ' – ' . $district . ' | TP.HCM'; + $pageDescription = 'Xem camera giao thông ' . $cameraName . ' tại ' . $district . ', TP.HCM. Hình ảnh camera được cập nhật tự động để tiện theo dõi tình hình giao thông.'; + $canonical = BASE_URL . '/camera.php?id=' . rawurlencode($cameraId); + $imageUrl = BASE_URL . '/proxy.php?id=' . rawurlencode($cameraId); + + $related = array_values(array_filter($data, static function (array $item) use ($cameraId, $district): bool { + return ($item['CamId'] ?? '') !== $cameraId + && cameraDistrict($item) === $district + && !empty($item['CamName']) + && !empty($item['CamId']); + })); + $related = array_slice($related, 0, 6); +} + +$schema = $camera ? [ + '@context' => 'https://schema.org', + '@type' => 'WebPage', + 'name' => $pageTitle, + 'description' => $pageDescription, + 'url' => $canonical, + 'inLanguage' => 'vi-VN', + 'isPartOf' => [ + '@type' => 'WebSite', + 'name' => 'Camera giao thông TP.HCM', + 'url' => BASE_URL . '/', + ], + 'about' => [ + '@type' => 'Place', + 'name' => $cameraName, + 'address' => [ + '@type' => 'PostalAddress', + 'addressLocality' => 'Hồ Chí Minh', + 'addressRegion' => $district, + 'addressCountry' => 'VN', + ], + ], + 'primaryImageOfPage' => [ + '@type' => 'ImageObject', + 'contentUrl' => $imageUrl, + 'name' => 'Camera ' . $cameraName, + ], +] : null; + +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + <?= h($pageTitle) ?> + + + + + + +
+
+ ← Danh sách camera + + + +
+ + +
+

Không tìm thấy camera

+

Camera này không tồn tại hoặc đã được gỡ khỏi danh sách.

+

Quay về danh sách camera giao thông TP.HCM

+
+ +
+
+ <?= h('Camera giao thông ' . $cameraName . ', ' . $district) ?> +
Đang tải
+
+
+
Camera giao thông TP.HCM
+

+

+
+ + Mã camera: + Tự làm mới sau 10 giây +
+
+ Hình ảnh phụ thuộc vào nguồn camera giao thông TP.HCM và có thể tạm thời không khả dụng. + Trang sẽ tự làm mới ảnh khi bạn đang mở tab này. +
+
+
+ + + + + + + +
+ + diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..896eeb7 --- /dev/null +++ b/favicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/index.html b/index.html index aa872ce..ddc5167 100644 --- a/index.html +++ b/index.html @@ -5,8 +5,45 @@ - - Camera giao thông TP.HCM + + + + + + + + + + + + + + + + Camera giao thông TP.HCM trực tuyến | ePlus.DEV +