From 05cb3136926b80aaf52c174cf513b6543a81d785 Mon Sep 17 00:00:00 2001
From: Mustafa Zeydani
Date: Wed, 19 Aug 2026 23:46:13 +0300
Subject: [PATCH 1/2] feat: improve search visibility and developer discovery
---
README.md | 8 +-
messages/ar.json | 65 +++-
messages/en.json | 59 +++-
.../_components/dataset-highlights.tsx | 118 ++++---
.../(index)/_components/landing-hero.tsx | 14 +-
src/app/[locale]/(index)/_utils/metadata.ts | 9 +-
src/app/[locale]/api/_components/api-page.tsx | 333 ++++++++++++++++++
src/app/[locale]/api/_utils/locale.ts | 21 ++
src/app/[locale]/api/_utils/metadata.ts | 70 ++++
.../[locale]/api/_utils/structured-data.ts | 78 ++++
src/app/[locale]/api/page.tsx | 32 ++
.../_components/dataset-catalog-page.tsx | 15 +-
.../_components/dataset-detail-page.tsx | 162 +++++++--
.../_components/dataset-page-header.tsx | 12 +-
.../_components/dataset-page-navigation.tsx | 21 +-
src/app/[locale]/datasets/_utils/metadata.ts | 19 +-
.../datasets/_utils/structured-data.ts | 4 +-
src/app/[locale]/layout.tsx | 12 -
src/app/auth.md/route.ts | 1 +
src/app/index.md/route.ts | 1 +
src/app/llms.txt/route.ts | 1 +
src/app/robots.txt | 3 -
src/app/sitemap.ts | 29 +-
src/components/site-footer.tsx | 13 +-
src/lib/agent-discovery.ts | 3 +
src/lib/datasets.ts | 320 ++++++++++++-----
src/lib/site.ts | 65 ++--
src/proxy.ts | 29 +-
28 files changed, 1195 insertions(+), 322 deletions(-)
create mode 100644 src/app/[locale]/api/_components/api-page.tsx
create mode 100644 src/app/[locale]/api/_utils/locale.ts
create mode 100644 src/app/[locale]/api/_utils/metadata.ts
create mode 100644 src/app/[locale]/api/_utils/structured-data.ts
create mode 100644 src/app/[locale]/api/page.tsx
diff --git a/README.md b/README.md
index 2d5c80e..8cb2697 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
Public website for [OpenSyria](https://opensyria.org), a public data commons for reliable Syrian datasets, API access, and civic intelligence.
-The website is intentionally small: a localized landing page, dataset catalog and detail pages, FAQ content, SEO metadata, breadcrumb navigation and structured data, social images, contributor attribution, theme and language controls, and links into the public API documentation and GitHub organization.
+The website is intentionally focused: a localized landing page, dataset catalog and detail pages, a developer API guide, FAQ content, SEO metadata, breadcrumb navigation and structured data, social images, contributor attribution, theme and language controls, and links into the public API documentation and GitHub organization.
## Public URLs
@@ -18,6 +18,7 @@ The website is intentionally small: a localized landing page, dataset catalog an
| | Universities dataset page |
| | Transport dataset page |
| | Telecom dataset page |
+| | Syrian data API guide for developers |
| | API documentation |
| | GitHub organization |
@@ -27,6 +28,9 @@ The site publishes public, read-only discovery metadata for agents:
- `/llms.txt` and `/index.md` describe the project and link to the main public resources.
- `/auth.md` explains that public website and dataset API access does not require registration, OAuth, API keys, or credentials.
+- These machine-readable discovery documents use `X-Robots-Tag: noindex, follow`
+ so search engines can follow their links without treating them as duplicate
+ landing pages.
- `/.well-known/api-catalog` links to the public API documentation, OpenAPI
description, health endpoint, and the shared geography, universities,
transport, and telecom discovery list.
@@ -117,6 +121,8 @@ values. The host-side Infisical `/website` export mirrors those values plus
The site supports English and Arabic.
- English is the default locale and renders at `/`.
+- Explicit `/en` URLs permanently redirect to the equivalent canonical,
+ unprefixed English URL.
- Arabic renders at `/ar`.
- Locale prefixes use next-intl `as-needed` routing.
- The HTML `dir` attribute and Base UI `DirectionProvider` are both driven from `src/i18n/routing.ts`.
diff --git a/messages/ar.json b/messages/ar.json
index 3d3ec91..3f37a18 100644
--- a/messages/ar.json
+++ b/messages/ar.json
@@ -3,7 +3,7 @@
"contributionCount": "{count, plural, one {مساهمة واحدة} two {مساهمتان} few {# مساهمات} many {# مساهمة} other {# مساهمة}}",
"contributors": "من يطوّرون المشروع",
"datasets": "استكشف البيانات",
- "docs": "التوثيق",
+ "docs": "استخدم API",
"eyebrow": "بيانات سورية مفتوحة",
"github": "GitHub",
"githubWithStars": "{count} نجوم",
@@ -16,7 +16,7 @@
"switchThemeLight": "فعّل الوضع الفاتح"
},
"Navigation": {
- "apiDocs": "توثيق API",
+ "apiDocs": "واجهة API للمطورين",
"datasets": "البيانات",
"primary": "التنقل الرئيسي",
"skipToContent": "تخطَّ إلى المحتوى الرئيسي"
@@ -38,7 +38,7 @@
"repositoriesTitle": "المستودعات"
},
"HomeDatasets": {
- "description": "حمّل بيانات سورية موثقة للمدن والمحافظات والجامعات والنقل والخرائط والبحث والصحافة والأدوات المدنية وبيانات مرجعية قابلة لإعادة الاستخدام.",
+ "description": "حمّل بيانات سورية موثقة للمدن والمحافظات والجامعات والنقل والاتصالات والخرائط والبحث والصحافة والأدوات المدنية وبيانات مرجعية قابلة لإعادة الاستخدام.",
"eyebrow": "مجموعات البيانات الحالية",
"openDataset": "افتح المجموعة",
"records": "السجلات",
@@ -50,7 +50,7 @@
"eyebrow": "أسئلة شائعة",
"items": {
"apiAccess": {
- "answer": "نعم. توفر واجهة البيانات القراءة فقط فهرس المجموعات وبيانات الإصدارات ومسارات الجغرافيا والجامعات والنقل ونسب المصادر وتوثيق OpenAPI.",
+ "answer": "نعم. توفر واجهة البيانات للقراءة فقط فهرس المجموعات وبيانات الإصدارات ومسارات الجغرافيا والجامعات والنقل والاتصالات ونسب المصادر وتوثيق OpenAPI.",
"question": "هل يمكنني استخدام API بدل تنزيل الملفات؟"
},
"availableDatasets": {
@@ -76,23 +76,76 @@
},
"title": "ما الذي تحتاج إلى معرفته قبل استخدام OpenSyria؟"
},
+ "Api": {
+ "access": "الوصول",
+ "allEndpointsShown": "كل المسارات المتاحة ظاهرة",
+ "apiSummary": "الواجهة باختصار",
+ "baseUrl": "العنوان الأساسي",
+ "browseDatasets": "تصفح البيانات",
+ "coverageDescription": "استعلم عن مجموعات البيانات العامة الموثقة والمُرقّمة بالإصدارات نفسها المعروضة في فهرس OpenSyria. يرتبط كل مجال بصفحة عربية تشرح النطاق والتنزيلات والمصادر والحدود المعروفة.",
+ "coverageEyebrow": "مجالات البيانات المتاحة",
+ "coverageTitle": "واجهة واحدة لمجموعات بيانات OpenSyria المنشورة",
+ "datasetDetails": "تفاصيل مجموعة البيانات",
+ "description": "استعلم عن البيانات المرجعية العامة للجغرافيا والجامعات والنقل والاتصالات في سوريا عبر مسارات JSON مستقرة. لا تحتاج طلبات القراءة إلى مفتاح API.",
+ "eyebrow": "واجهة عامة للبيانات السورية",
+ "moreEndpoints": "{count, plural, one {+# مسار إضافي} two {+# مساران إضافيان} few {+# مسارات إضافية} many {+# مسارًا إضافيًا} other {+# مسار إضافي}}",
+ "openApiSchema": "اعرض مخطط OpenAPI",
+ "openDocs": "افتح التوثيق التفاعلي",
+ "policyDescription": "تقدم الواجهة بيانات مرجعية عامة وغير شخصية، ولا تعرض سجلات خاصة أو تتبعًا مباشرًا أو معلومات تشغيلية. لا يتطلب الوصول العام تسجيلًا أو مفتاح API، ويقتصر على 500 طلب لكل عميل يوميًا. تعامل مع الاستجابات بوصفها مراجع مرتبطة بإصدارات، وراجع حدود كل مجموعة وترخيصها، واحتفظ بنسبة المصادر في الأعمال المشتقة.",
+ "policyTitle": "بيانات عامة بحدود واضحة",
+ "publicReadOnly": "عامة · بلا مفتاح API · للقراءة فقط",
+ "quickstartDescription": "ابدأ بمسار اكتشاف مجموعات البيانات للحصول على المجالات العامة الحالية وبيانات إصداراتها. تأتي الاستجابات بصيغة JSON، وتشرح وثيقة OpenAPI مسارات الطلب ومخططات الاستجابة.",
+ "quickstartEyebrow": "بدء سريع",
+ "quickstartTitle": "أرسل طلبك الأول",
+ "rateLimit": "حد الاستخدام المجاني",
+ "rateLimitValue": "500 طلب لكل عميل يوميًا",
+ "recordCount": "{count, plural, zero {لا سجلات} one {سجل واحد} two {سجلان} few {# سجلات} many {# سجلًا} other {# سجل}}",
+ "responseFormat": "الاستجابة والمخطط",
+ "steps": {
+ "discover": {
+ "description": "اقرأ مسار فهرس البيانات، ثم اختر مجالًا وراجع الإصدار الحالي وعدد السجلات وروابط التوثيق.",
+ "title": "اكتشف مجموعة البيانات"
+ },
+ "query": {
+ "description": "استخدم مسارًا مُرقّمًا تحت /api/v1، واتبع المعرفات الموثقة، وتحقق من التكامل وفق مخطط استجابة OpenAPI.",
+ "title": "استعلم من مسار مستقر"
+ },
+ "verify": {
+ "description": "استخدم صفحة البيانات والمستودع المرتبطين لمراجعة المصادر والحدود وملفات الإصدار والإحالات والترخيص قبل النشر.",
+ "title": "تحقق قبل إعادة الاستخدام"
+ }
+ },
+ "title": "ابنِ باستخدام واجهة OpenSyria للبيانات السورية",
+ "workflowDescription": "تمثل الواجهة والإصدارات القابلة للتنزيل منتجات البيانات العامة نفسها. استخدم الواجهة للاستعلام وصفحات البيانات لفهم السياق والتحقق.",
+ "workflowEyebrow": "مسار العمل المقترح",
+ "workflowTitle": "من الاكتشاف إلى إعادة الاستخدام المسؤولة"
+ },
"Datasets": {
"apiDocs": "توثيق API",
+ "apiGuide": "واجهة API للمطورين",
"apiEndpoints": "مسارات API",
- "catalogDescription": "تصفح بيانات OpenSyria للمدن والمحافظات والمناطق والنواحي والمحلات والجامعات والنقل في سوريا، مع روابط GitHub وملفات JSON وCSV للخرائط والبحث والصحافة.",
+ "catalogDescription": "تصفح بيانات OpenSyria للمدن والمحافظات والمناطق والنواحي والمحلات والجامعات والنقل والاتصالات في سوريا، مع ملفات JSON وCSV وواجهات API للبحث والتطوير.",
"catalogList": "مجموعات البيانات المتاحة",
"catalogTitle": "بيانات سورية للخرائط والبحث والتنزيل",
"contains": "محتوى مجموعة البيانات",
+ "coverageTitle": "التغطية",
+ "detailsDescription": "اطلع على نطاق البيانات ومنهج توثيقها واستخداماتها المناسبة وحدودها المعروفة قبل إعادة استخدام هذا الإصدار.",
+ "detailsTitle": "فهم مجموعة البيانات",
"downloadFiles": "ملفات التنزيل",
"formats": "الصيغ",
"githubRepository": "مستودع GitHub",
"home": "الرئيسية",
- "keywords": "موضوعات بحث مرتبطة",
+ "intendedUsesTitle": "الاستخدامات المناسبة",
+ "lastUpdated": "آخر تحديث",
"latestRelease": "آخر إصدار",
+ "limitationsTitle": "الحدود والنواقص",
"navigation": "تنقل مجموعات البيانات",
+ "notAvailable": "غير متاح",
"openDataset": "افتح المجموعة",
"openData": "بيانات مفتوحة",
"records": "السجلات",
+ "provenanceTitle": "المصادر والمنهج",
+ "releaseContextTitle": "سياق الإصدار",
"reuseDescription": "تنشر OpenSyria بيانات مرجعية عامة وغير شخصية وموثقة بالمصادر. راجع ترخيص كل مستودع قبل إعادة استخدام ملفات الإصدارات في الإنتاج.",
"reuseTitle": "سياسة المصادر وإعادة الاستخدام",
"sourceBacked": "موثق بالمصادر",
diff --git a/messages/en.json b/messages/en.json
index c451f42..b7c4b00 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -3,7 +3,7 @@
"contributionCount": "{count, plural, one {# contribution} other {# contributions}}",
"contributors": "Contributors",
"datasets": "Explore data",
- "docs": "Read docs",
+ "docs": "Use the API",
"eyebrow": "Open civic intelligence",
"github": "GitHub",
"githubWithStars": "{count} stars",
@@ -22,7 +22,7 @@
"skipToContent": "Skip to main content"
},
"Footer": {
- "apiDocs": "API documentation",
+ "apiDocs": "Developer API",
"communityTitle": "Community",
"copyright": "© OpenSyria",
"datasets": "Dataset catalog",
@@ -76,23 +76,76 @@
},
"title": "What should you know before using OpenSyria?"
},
+ "Api": {
+ "access": "Access",
+ "allEndpointsShown": "All listed endpoints shown",
+ "apiSummary": "API at a glance",
+ "baseUrl": "Base URL",
+ "browseDatasets": "Browse datasets",
+ "coverageDescription": "Query the same versioned, source-backed public datasets available from the OpenSyria catalog. Every domain links back to a human-readable dataset page with scope, downloads, provenance, and limitations.",
+ "coverageEyebrow": "Available data domains",
+ "coverageTitle": "One API for OpenSyria's released datasets",
+ "datasetDetails": "Dataset details",
+ "description": "Query public Syrian geography, university, transport, and telecom reference data through stable JSON endpoints. No API key is required for read-only requests.",
+ "eyebrow": "Public Syrian data API",
+ "moreEndpoints": "{count, plural, one {+# more endpoint} other {+# more endpoints}}",
+ "openApiSchema": "View OpenAPI schema",
+ "openDocs": "Open interactive docs",
+ "policyDescription": "The API serves public, non-personal reference data and does not expose private records, live tracking, or operational intelligence. Public access requires no registration or API key and is limited to 500 requests per client per day. Treat responses as versioned reference material, review each dataset's limitations and license, and retain source attribution in downstream work.",
+ "policyTitle": "Public data with explicit boundaries",
+ "publicReadOnly": "Public · no API key · read-only",
+ "quickstartDescription": "Start with dataset discovery to retrieve the current public domains and release metadata. Responses use JSON, and the OpenAPI document describes request paths and response schemas.",
+ "quickstartEyebrow": "Quick start",
+ "quickstartTitle": "Make your first request",
+ "rateLimit": "Free-tier limit",
+ "rateLimitValue": "500 requests per client per day",
+ "recordCount": "{count, plural, one {# record} other {# records}}",
+ "responseFormat": "Response and schema",
+ "steps": {
+ "discover": {
+ "description": "Read the dataset catalog endpoint, then choose a domain and inspect its current release, record counts, and documentation links.",
+ "title": "Discover the dataset"
+ },
+ "query": {
+ "description": "Call a versioned /api/v1 endpoint, follow the documented identifiers, and validate your integration against the OpenAPI response schema.",
+ "title": "Query a stable endpoint"
+ },
+ "verify": {
+ "description": "Use the linked dataset page and repository to review provenance, known limits, release files, source citations, and licensing before publication.",
+ "title": "Verify before reuse"
+ }
+ },
+ "title": "Build with OpenSyria's Syrian datasets API",
+ "workflowDescription": "The API and downloadable releases represent the same public data products. Use the API for querying and the dataset pages for context and verification.",
+ "workflowEyebrow": "Recommended workflow",
+ "workflowTitle": "From discovery to responsible reuse"
+ },
"Datasets": {
"apiDocs": "API docs",
+ "apiGuide": "Developer API",
"apiEndpoints": "API endpoints",
"catalogDescription": "Browse OpenSyria datasets for Syrian cities, governorates, districts, localities, universities, transport, telecom, maps, research, journalism, and JSON/CSV downloads.",
"catalogList": "Available datasets",
"catalogTitle": "Syrian datasets for maps, research, and downloads",
"contains": "What this dataset contains",
+ "coverageTitle": "Coverage",
+ "detailsDescription": "Read the scope, sourcing method, suitable uses, and known limits before reusing this release.",
+ "detailsTitle": "Understand this dataset",
"downloadFiles": "Download files",
"formats": "Formats",
"githubRepository": "GitHub repository",
"home": "Home",
- "keywords": "Related search topics",
+ "intendedUsesTitle": "Suitable uses",
+ "lastUpdated": "Last updated",
"latestRelease": "Latest release",
+ "limitationsTitle": "Limitations",
"navigation": "Dataset navigation",
+ "notAvailable": "Not available",
"openDataset": "Open dataset",
"openData": "Open data",
"records": "Records",
+ "provenanceTitle": "Provenance and method",
+ "releaseContextTitle": "Release context",
"reuseDescription": "OpenSyria publishes public, non-personal, source-backed reference data. Check each repository license before reusing release artifacts in production.",
"reuseTitle": "Source and reuse policy",
"sourceBacked": "Source-backed",
diff --git a/src/app/[locale]/(index)/_components/dataset-highlights.tsx b/src/app/[locale]/(index)/_components/dataset-highlights.tsx
index e473c9f..38f2657 100644
--- a/src/app/[locale]/(index)/_components/dataset-highlights.tsx
+++ b/src/app/[locale]/(index)/_components/dataset-highlights.tsx
@@ -50,64 +50,76 @@ export async function DatasetHighlights({ locale }: DatasetHighlightsProps) {
- {datasets.map((dataset) => (
-
-
-
-
- {dataset.repositoryName}
-
-
- {dataset.title[locale]}
-
-
-
-
-
-
- {dataset.shortDescription[locale]}
-
+ {datasets.map((dataset) => {
+ const datasetHref = getPathname({
+ href: `/datasets/${dataset.slug}`,
+ locale,
+ })
-
-
+ return (
+
+
-
- {t("records")}
-
-
- {numberFormatter.format(dataset.totalRecords)}
-
+
+ {dataset.repositoryName}
+
+
+
+ {dataset.title[locale]}
+
+
-
-
- {t("release")}
- - {dataset.releaseTag}
-
-
-
-
- {t("openDataset")}
-
-
-
-
- ))}
+
+
+
+ {dataset.shortDescription[locale]}
+
+
+
+
+
+
- {t("records")}
+ -
+ {numberFormatter.format(dataset.totalRecords)}
+
+
+
+
- {t("release")}
+ -
+ {dataset.releaseTag}
+
+
+
+
+
+ {t("openDataset")}: {dataset.title[locale]}
+
+
+
+
+ )
+ })}
diff --git a/src/app/[locale]/(index)/_components/landing-hero.tsx b/src/app/[locale]/(index)/_components/landing-hero.tsx
index 1c0cd97..fe32959 100644
--- a/src/app/[locale]/(index)/_components/landing-hero.tsx
+++ b/src/app/[locale]/(index)/_components/landing-hero.tsx
@@ -1,4 +1,4 @@
-import { ArrowUpRight, BookOpenText, Database, Star } from "lucide-react"
+import { Braces, Database, Star } from "lucide-react"
import { cacheLife } from "next/cache"
import Image from "next/image"
import NextLink from "next/link"
@@ -107,19 +107,15 @@ export async function LandingHero({ locale }: LandingHeroProps) {
-
+
{t("docs")}
-
+
+const workflowSteps = ["discover", "query", "verify"] as const
+
+export async function ApiPage({ datasets, locale }: ApiPageProps) {
+ const t = await getTranslations({ locale, namespace: "Api" })
+ const exampleCommand = `curl --request GET \\
+ --url ${siteLinks.datasetsApi}/api/v1/datasets \\
+ --header 'accept: application/json'`
+
+ return (
+ <>
+
+
+
+ {t("browseDatasets")}
+
+
+
+
+
+
+
+
+
+ {t("eyebrow")}
+
+
+ {t("title")}
+
+
+ {t("description")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{t("quickstartEyebrow")}
+
+
+ {t("quickstartTitle")}
+
+
+ {t("quickstartDescription")}
+
+
+
+
+ {exampleCommand}
+
+
+
+
+
+
+
+
+ {t("coverageEyebrow")}
+
+
+ {t("coverageTitle")}
+
+
+ {t("coverageDescription")}
+
+
+
+
+ {datasets.map((dataset) => {
+ const hiddenEndpointCount = Math.max(
+ dataset.apiRoutes.length - 3,
+ 0
+ )
+
+ return (
+
+
+
+
+ {t("recordCount", { count: dataset.totalRecords })}
+
+
+
+ {dataset.title[locale]}
+
+
+
+
+
+
+ {dataset.shortDescription[locale]}
+
+
+ {dataset.apiRoutes.slice(0, 3).map((route) => (
+ -
+
+ {route}
+
+
+ ))}
+
+
+
+ {hiddenEndpointCount > 0
+ ? t("moreEndpoints", { count: hiddenEndpointCount })
+ : t("allEndpointsShown")}
+
+
+ {t("datasetDetails")}
+
+
+
+
+ )
+ })}
+
+
+
+
+
+
+
+
+ {t("workflowEyebrow")}
+
+
+ {t("workflowTitle")}
+
+
+ {t("workflowDescription")}
+
+
+
+
+ {workflowSteps.map((step, index) => (
+ -
+
+
+ {String(index + 1).padStart(2, "0")}
+
+
+
+
+ {t(`steps.${step}.title`)}
+
+
+ {t(`steps.${step}.description`)}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ {t("policyTitle")}
+
+
+ {t("policyDescription")}
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/app/[locale]/api/_utils/locale.ts b/src/app/[locale]/api/_utils/locale.ts
new file mode 100644
index 0000000..ca57e18
--- /dev/null
+++ b/src/app/[locale]/api/_utils/locale.ts
@@ -0,0 +1,21 @@
+import { notFound } from "next/navigation"
+import { hasLocale } from "next-intl"
+
+import { type Locale, routing } from "@/i18n/routing"
+
+type ApiPageParams = Promise<{
+ locale: string
+}>
+
+async function resolveApiLocale(params: ApiPageParams) {
+ const { locale } = await params
+
+ if (!hasLocale(routing.locales, locale)) {
+ notFound()
+ }
+
+ return locale as Locale
+}
+
+export type { ApiPageParams }
+export { resolveApiLocale }
diff --git a/src/app/[locale]/api/_utils/metadata.ts b/src/app/[locale]/api/_utils/metadata.ts
new file mode 100644
index 0000000..80f811d
--- /dev/null
+++ b/src/app/[locale]/api/_utils/metadata.ts
@@ -0,0 +1,70 @@
+import type { Metadata } from "next"
+
+import { type Locale, routing } from "@/i18n/routing"
+import { getLocalizedPath } from "@/lib/datasets"
+import { indexableRobots, siteConfig, socialPreviewImages } from "@/lib/site"
+import { type ApiPageParams, resolveApiLocale } from "./locale"
+
+const apiPageSeo: Record = {
+ ar: {
+ description:
+ "استخدم واجهة OpenSyria العامة لبيانات الجغرافيا والجامعات والنقل والاتصالات السورية، مع مسارات JSON وتوثيق OpenAPI وأمثلة جاهزة.",
+ title: "واجهة API للبيانات السورية للمطورين",
+ },
+ en: {
+ description:
+ "Use the public OpenSyria Syrian data API for geography, universities, transport, and telecom, with JSON endpoints, OpenAPI docs, and examples.",
+ title: "Syrian Data API for Developers",
+ },
+}
+
+async function generateMetadata({
+ params,
+}: {
+ params: ApiPageParams
+}): Promise {
+ const locale = await resolveApiLocale(params)
+ const seo = apiPageSeo[locale]
+ const pageUrl = `${siteConfig.url}${getLocalizedPath(locale, "api")}`
+
+ return {
+ alternates: {
+ canonical: pageUrl,
+ languages: {
+ ...Object.fromEntries(
+ routing.locales.map((alternateLocale) => [
+ alternateLocale,
+ `${siteConfig.url}${getLocalizedPath(alternateLocale, "api")}`,
+ ])
+ ),
+ "x-default": `${siteConfig.url}/api`,
+ },
+ },
+ description: seo.description,
+ metadataBase: new URL(siteConfig.url),
+ openGraph: {
+ alternateLocale: routing.locales
+ .filter((alternateLocale) => alternateLocale !== locale)
+ .map((alternateLocale) => siteConfig.locales[alternateLocale].ogLocale),
+ description: seo.description,
+ images: [socialPreviewImages.openGraph],
+ locale: siteConfig.locales[locale].ogLocale,
+ siteName: siteConfig.name,
+ title: seo.title,
+ type: "website",
+ url: pageUrl,
+ },
+ robots: indexableRobots,
+ title: {
+ absolute: `${seo.title} | ${siteConfig.name}`,
+ },
+ twitter: {
+ card: "summary_large_image",
+ description: seo.description,
+ images: [socialPreviewImages.twitter],
+ title: seo.title,
+ },
+ }
+}
+
+export { apiPageSeo, generateMetadata }
diff --git a/src/app/[locale]/api/_utils/structured-data.ts b/src/app/[locale]/api/_utils/structured-data.ts
new file mode 100644
index 0000000..f52aebf
--- /dev/null
+++ b/src/app/[locale]/api/_utils/structured-data.ts
@@ -0,0 +1,78 @@
+import type { BreadcrumbList, Graph, WebAPI } from "schema-dts"
+
+import type { Locale } from "@/i18n/routing"
+import { getLocalizedPath } from "@/lib/datasets"
+import {
+ createJsonLdGraph,
+ getCommonPageJsonLd,
+ organizationJsonLdId,
+ schemaReference,
+ toJsonLd,
+} from "@/lib/json-ld"
+import { getLocalePath, siteConfig, siteLinks } from "@/lib/site"
+import { apiPageSeo } from "./metadata"
+
+const breadcrumbLabels: Record = {
+ ar: { api: "واجهة API", home: "الرئيسية" },
+ en: { api: "Data API", home: "Home" },
+}
+
+function getApiStructuredData(locale: Locale): Graph {
+ const seo = apiPageSeo[locale]
+ const pageUrl = `${siteConfig.url}${getLocalizedPath(locale, "api")}`
+ const homeUrl = `${siteConfig.url}${getLocalePath(locale)}`
+ const labels = breadcrumbLabels[locale]
+ const breadcrumbs: BreadcrumbList = {
+ "@id": `${pageUrl}#breadcrumb`,
+ "@type": "BreadcrumbList",
+ itemListElement: [
+ {
+ "@type": "ListItem",
+ item: homeUrl,
+ name: labels.home,
+ position: 1,
+ },
+ {
+ "@type": "ListItem",
+ item: pageUrl,
+ name: labels.api,
+ position: 2,
+ },
+ ],
+ }
+ const api: WebAPI = {
+ "@id": `${siteLinks.datasetsApi}/#api`,
+ "@type": "WebAPI",
+ areaServed: {
+ "@type": "Country",
+ name: locale === "ar" ? "سوريا" : "Syria",
+ },
+ category: locale === "ar" ? "واجهة بيانات مفتوحة" : "Open data API",
+ description: seo.description,
+ documentation: siteLinks.docs,
+ name:
+ locale === "ar"
+ ? "واجهة OpenSyria لمجموعات البيانات"
+ : "OpenSyria Datasets API",
+ provider: schemaReference(organizationJsonLdId),
+ serviceType:
+ locale === "ar"
+ ? "واجهة عامة لمجموعات البيانات للقراءة فقط"
+ : "Public read-only datasets API",
+ url: siteLinks.datasetsApi,
+ }
+
+ return createJsonLdGraph([
+ ...getCommonPageJsonLd({
+ description: seo.description,
+ locale,
+ pageId: `${pageUrl}#webpage`,
+ pageUrl,
+ title: seo.title,
+ }),
+ breadcrumbs,
+ api,
+ ])
+}
+
+export { getApiStructuredData, toJsonLd }
diff --git a/src/app/[locale]/api/page.tsx b/src/app/[locale]/api/page.tsx
new file mode 100644
index 0000000..54f785b
--- /dev/null
+++ b/src/app/[locale]/api/page.tsx
@@ -0,0 +1,32 @@
+import { setRequestLocale } from "next-intl/server"
+
+import { getDatasetCatalog } from "@/lib/datasets"
+import { ApiPage } from "./_components/api-page"
+import { type ApiPageParams, resolveApiLocale } from "./_utils/locale"
+import { getApiStructuredData, toJsonLd } from "./_utils/structured-data"
+
+export { generateMetadata } from "./_utils/metadata"
+
+type PageProps = Readonly<{
+ params: ApiPageParams
+}>
+
+export default async function Page({ params }: PageProps) {
+ const locale = await resolveApiLocale(params)
+ const datasets = await getDatasetCatalog()
+
+ setRequestLocale(locale)
+
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/src/app/[locale]/datasets/_components/dataset-catalog-page.tsx b/src/app/[locale]/datasets/_components/dataset-catalog-page.tsx
index 071b904..aae3d72 100644
--- a/src/app/[locale]/datasets/_components/dataset-catalog-page.tsx
+++ b/src/app/[locale]/datasets/_components/dataset-catalog-page.tsx
@@ -71,7 +71,12 @@ export async function DatasetCatalogPage({
{dataset.repositoryName}
- {dataset.title[locale]}
+
+ {dataset.title[locale]}
+
- {t("openDataset")}
+ {t("openDataset")}: {dataset.title[locale]}
8
const breadcrumbs = getDatasetBreadcrumbs(locale, dataset)
+ const updatedAt = formatUpdatedAt(dataset.updatedAt, locale)
return (
<>
-
+
{dataset.releaseTag}
+
+
+ {t("lastUpdated")}
+
+
+ {updatedAt && dataset.updatedAt ? (
+
+ ) : (
+ t("notAvailable")
+ )}
+
+
{t("formats")}
@@ -256,31 +296,101 @@ export async function DatasetDetailPage({
-
-
-
-
- {t("keywords")}
-
-
- {dataset.keywords.map((keyword) => (
- -
- {keyword}
-
- ))}
-
+ {dataset.details ? (
+
+
+
+
+ {t("detailsTitle")}
+
+
+ {t("detailsDescription")}
+
+
+
+
+
+
+
+ {t("coverageTitle")}
+
+
+ {dataset.details.coverage[locale]}
+
+
+
+
+
+
+ {t("provenanceTitle")}
+
+
+ {dataset.details.provenance[locale]}
+
+
+
+
+
+
+ {t("intendedUsesTitle")}
+
+
+ {dataset.details.intendedUses[locale].map((use) => (
+ -
+
+ {use}
+
+ ))}
+
+
+
+
+
+
+ {t("limitationsTitle")}
+
+
+ {dataset.details.limitations[locale]}
+
+
+
+
+
-
-
+
+ ) : null}
>
)
diff --git a/src/app/[locale]/datasets/_components/dataset-page-header.tsx b/src/app/[locale]/datasets/_components/dataset-page-header.tsx
index 90ebb8b..ca646c1 100644
--- a/src/app/[locale]/datasets/_components/dataset-page-header.tsx
+++ b/src/app/[locale]/datasets/_components/dataset-page-header.tsx
@@ -1,23 +1,15 @@
import { getTranslations } from "next-intl/server"
import { SiteHeader } from "@/components/site-header"
-import { siteLinks } from "@/lib/site"
import { DatasetPageNavigation } from "./dataset-page-navigation"
-type DatasetPageHeaderProps = {
- apiDocsHref?: string
-}
-
-export async function DatasetPageHeader({
- apiDocsHref = siteLinks.docs,
-}: DatasetPageHeaderProps = {}) {
+export async function DatasetPageHeader() {
const t = await getTranslations("Datasets")
return (
diff --git a/src/app/[locale]/datasets/_components/dataset-page-navigation.tsx b/src/app/[locale]/datasets/_components/dataset-page-navigation.tsx
index 648d8f0..5f3d455 100644
--- a/src/app/[locale]/datasets/_components/dataset-page-navigation.tsx
+++ b/src/app/[locale]/datasets/_components/dataset-page-navigation.tsx
@@ -1,6 +1,6 @@
"use client"
-import { BookOpenText } from "lucide-react"
+import { Braces } from "lucide-react"
import {
NavigationMenu,
@@ -10,18 +10,15 @@ import {
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import { Link } from "@/i18n/navigation"
-import { trustedExternalLinkRel } from "@/lib/links"
type DatasetPageNavigationProps = {
- apiDocsHref: string
- apiDocsLabel: string
+ apiGuideLabel: string
ariaLabel: string
openDataLabel: string
}
export function DatasetPageNavigation({
- apiDocsHref,
- apiDocsLabel,
+ apiGuideLabel,
ariaLabel,
openDataLabel,
}: DatasetPageNavigationProps) {
@@ -39,16 +36,10 @@ export function DatasetPageNavigation({
- }
+ render={}
>
-
- {apiDocsLabel}
+
+ {apiGuideLabel}
diff --git a/src/app/[locale]/datasets/_utils/metadata.ts b/src/app/[locale]/datasets/_utils/metadata.ts
index e786bd8..13db5c6 100644
--- a/src/app/[locale]/datasets/_utils/metadata.ts
+++ b/src/app/[locale]/datasets/_utils/metadata.ts
@@ -5,11 +5,10 @@ import { type Locale, routing } from "@/i18n/routing"
import {
getAbsoluteUrl,
getDatasetBySlug,
- getDatasetCatalog,
getDatasetPath,
getDatasetsPath,
} from "@/lib/datasets"
-import { siteConfig, socialPreviewImages } from "@/lib/site"
+import { indexableRobots, siteConfig, socialPreviewImages } from "@/lib/site"
import {
type DatasetParams,
type LocaleParams,
@@ -20,12 +19,12 @@ import {
const catalogSeo: Record = {
ar: {
description:
- "تصفح بيانات OpenSyria للمدن والمحافظات والمناطق والمحلات والجامعات السورية، مع ملفات JSON وCSV للخرائط والبحث والصحافة.",
+ "تصفح بيانات OpenSyria للمدن والمحافظات والمناطق والجامعات والنقل والاتصالات في سوريا، مع ملفات JSON وCSV وواجهات API للبحث والتطوير.",
title: "بيانات سورية للخرائط والبحث والتنزيل",
},
en: {
description:
- "Browse OpenSyria datasets for Syrian cities, governorates, districts, localities, universities, transport, telecom, maps, research, journalism, and JSON/CSV downloads.",
+ "Browse source-backed Syrian datasets for cities, governorates, localities, universities, transport, and telecom, with JSON/CSV downloads and API access.",
title: "Syrian Datasets for Maps, Research and Downloads",
},
}
@@ -36,7 +35,6 @@ async function generateDatasetCatalogMetadata({
params: LocaleParams
}): Promise {
const locale = await resolvePageLocale(params)
- const datasets = await getDatasetCatalog()
const seo = catalogSeo[locale]
const pageUrl = getAbsoluteUrl(getDatasetsPath(locale))
@@ -48,10 +46,6 @@ async function generateDatasetCatalogMetadata({
),
},
description: seo.description,
- keywords: getUniqueKeywords([
- ...siteConfig.keywords,
- ...datasets.flatMap((dataset) => dataset.keywords),
- ]),
metadataBase: new URL(siteConfig.url),
openGraph: {
alternateLocale: routing.locales
@@ -65,6 +59,7 @@ async function generateDatasetCatalogMetadata({
type: "website",
url: pageUrl,
},
+ robots: indexableRobots,
title: {
absolute: `${seo.title} | ${siteConfig.name}`,
},
@@ -101,7 +96,6 @@ async function generateDatasetMetadata({
),
},
description,
- keywords: getUniqueKeywords([...siteConfig.keywords, ...dataset.keywords]),
metadataBase: new URL(siteConfig.url),
openGraph: {
alternateLocale: routing.locales
@@ -115,6 +109,7 @@ async function generateDatasetMetadata({
type: "website",
url: pageUrl,
},
+ robots: indexableRobots,
title: {
absolute: `${title} | ${siteConfig.name}`,
},
@@ -136,8 +131,4 @@ function getAlternateLanguages(getPath: (locale: Locale) => string) {
}
}
-function getUniqueKeywords(keywords: readonly string[]) {
- return Array.from(new Set(keywords))
-}
-
export { generateDatasetCatalogMetadata, generateDatasetMetadata }
diff --git a/src/app/[locale]/datasets/_utils/structured-data.ts b/src/app/[locale]/datasets/_utils/structured-data.ts
index 1c9e246..a3f155a 100644
--- a/src/app/[locale]/datasets/_utils/structured-data.ts
+++ b/src/app/[locale]/datasets/_utils/structured-data.ts
@@ -36,7 +36,7 @@ const catalogStructuredDataText: Record<
> = {
ar: {
description:
- "فهرس OpenSyria لبيانات المدن والمحافظات والمناطق والمحلات والجامعات السورية، مع ملفات JSON وCSV للخرائط والبحث والصحافة.",
+ "فهرس OpenSyria لبيانات المدن والمحافظات والمناطق والجامعات والنقل والاتصالات في سوريا، مع ملفات JSON وCSV وواجهات API للبحث والتطوير.",
title: "بيانات سورية للخرائط والبحث والتنزيل",
},
en: {
@@ -141,7 +141,7 @@ function getDatasetJsonLd({
includedInDataCatalog: schemaReference(catalogId),
inLanguage: ["en", "ar"],
isAccessibleForFree: true,
- keywords: dataset.keywords,
+ keywords: dataset.keywords[locale],
license: getDatasetLicenseJsonLd(dataset),
name: dataset.title[locale],
publisher: schemaReference(organizationJsonLdId),
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx
index df1542f..30ea940 100644
--- a/src/app/[locale]/layout.tsx
+++ b/src/app/[locale]/layout.tsx
@@ -35,17 +35,6 @@ export const metadata: Metadata = {
},
publisher: siteConfig.name,
referrer: "origin-when-cross-origin",
- robots: {
- follow: true,
- googleBot: {
- follow: true,
- index: true,
- "max-image-preview": "large",
- "max-snippet": -1,
- "max-video-preview": -1,
- },
- index: true,
- },
title: {
default: siteConfig.defaultTitle,
template: `%s | ${siteConfig.name}`,
@@ -136,7 +125,6 @@ export default async function LocaleLayout({
Date
getPath: (locale: Locale) => string
- priority: number
+ lastModified?: Date
}
export default async function sitemap(): Promise {
const datasets = await getDatasetCatalog()
- const defaultLastModified = new Date()
const routes: SitemapRoute[] = [
{
- changeFrequency: "weekly",
getPath: getLocalizedPath,
- priority: 1,
},
{
- changeFrequency: "weekly",
getPath: getDatasetsPath,
- priority: 0.95,
+ },
+ {
+ getPath: (locale) => getLocalizedPath(locale, "api"),
},
...datasets.map((dataset) => ({
- changeFrequency: "weekly" as const,
- getLastModified: () => getDateOrFallback(dataset.updatedAt),
getPath: (locale: Locale) => getDatasetPath(locale, dataset.slug),
- priority: 0.9,
+ lastModified: getValidDate(dataset.updatedAt),
})),
]
@@ -53,23 +47,18 @@ export default async function sitemap(): Promise {
alternates: {
languages,
},
- changeFrequency: route.changeFrequency,
- lastModified: route.getLastModified?.() ?? defaultLastModified,
- priority:
- locale === routing.defaultLocale
- ? route.priority
- : route.priority - 0.05,
+ ...(route.lastModified ? { lastModified: route.lastModified } : {}),
url: getAbsoluteUrl(route.getPath(locale)),
}))
})
}
-function getDateOrFallback(value: string | null) {
+function getValidDate(value: string | null) {
if (!value) {
- return new Date()
+ return undefined
}
const date = new Date(value)
- return Number.isNaN(date.getTime()) ? new Date() : date
+ return Number.isNaN(date.getTime()) ? undefined : date
}
diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx
index 8bdbd6f..b36e01b 100644
--- a/src/components/site-footer.tsx
+++ b/src/components/site-footer.tsx
@@ -55,18 +55,9 @@ export async function SiteFooter({ locale }: SiteFooterProps) {
-
+
{t("apiDocs")}
-
-
+
+type LocalizedList = Record
+
+type DatasetDetails = {
+ coverage: LocalizedText
+ intendedUses: LocalizedList
+ limitations: LocalizedText
+ provenance: LocalizedText
+ releaseContext: LocalizedText
+}
+
type DatasetRecordGroup = {
count: number
name: LocalizedText
@@ -21,10 +31,11 @@ export type DatasetCatalogItem = {
apiRoutes: readonly string[]
category: string
description: LocalizedText
+ details: DatasetDetails | null
distributions: readonly DatasetDistribution[]
formats: readonly string[]
id: string
- keywords: readonly string[]
+ keywords: LocalizedList
apiDocsUrl: string
licenseUrl: string
openApiUrl: string
@@ -44,7 +55,8 @@ export type DatasetSlug = string
type DatasetDescriptor = {
apiRoutes?: readonly string[]
description?: LocalizedText
- keywords?: readonly string[]
+ details?: DatasetDetails
+ keywords?: LocalizedList
recordGroupLabels?: Record
recordGroupOrder?: readonly string[]
repository?: string
@@ -119,6 +131,7 @@ type ReleaseData = {
const GITHUB_API_BASE = "https://api.github.com"
const RELEASE_MANIFEST_FILE = "release-manifest.json"
+const emptyLocalizedList: LocalizedList = { ar: [], en: [] }
const datasetApiRoutes = {
geography: [
@@ -160,26 +173,60 @@ const datasetDescriptors = [
ar: "حمّل بيانات المدن والمحافظات والمناطق والنواحي والبلدات والقرى والمحلات السورية مع الإحداثيات وملفات JSON وCSV للخرائط والبحث.",
en: "Download Syrian cities, governorates, districts, subdistricts, towns, villages, localities, coordinates, and JSON/CSV files for maps and research.",
},
- keywords: [
- "Syrian cities dataset",
- "Syrian cities CSV",
- "Syrian cities JSON",
- "Syria governorates",
- "Syria governorates dataset",
- "Syria districts",
- "Syria districts data",
- "Syria subdistricts",
- "Syria localities",
- "Syrian towns and villages",
- "Syrian administrative divisions",
- "Syria places CSV",
- "Syria maps data",
- "بيانات المدن السورية",
- "بيانات المحافظات السورية",
- "محافظات سوريا",
- "مناطق سوريا",
- "نواحي سوريا",
- ],
+ details: {
+ coverage: {
+ ar: "تغطي البيانات المرجعية الإدارية المحافظات والمناطق والنواحي والمحلات، بما فيها المدن والبلدات والقرى وغيرها من التجمعات السكانية. وقد يتضمن السجل أسماء عربية وإنجليزية، وروابط التسلسل الإداري، والمراكز الجغرافية، والمساحة، وقياسات سكانية مؤرخة، ومعرفات عامة عندما يدعمها مصدر قابل لإعادة الاستخدام.",
+ en: "Administrative reference data spans governorates, districts, subdistricts, and localities, including cities, towns, villages, and other populated places. Records may include Arabic and English names, hierarchy links, centroids, area, dated population measurements, and public identifiers when a reusable source supports them.",
+ },
+ intendedUses: {
+ ar: [
+ "توحيد أسماء الأماكن السورية والعلاقات بين مستوياتها الإدارية.",
+ "ربط مجموعات البيانات العامة باستخدام معرفات OpenSyria أو P-code أو GeoNames أو Wikidata أو geoBoundaries عند توفرها.",
+ "إنشاء خرائط بحثية وأدلة وأدوات بحث وتحليلات صحفية اعتمادًا على بيانات مرجعية قابلة للتنزيل.",
+ ],
+ en: [
+ "Normalize Syrian place names and administrative relationships.",
+ "Join public datasets through stable OpenSyria, P-code, GeoNames, Wikidata, or geoBoundaries identifiers when present.",
+ "Build research maps, search tools, directories, and journalistic analyses from downloadable reference data.",
+ ],
+ },
+ limitations: {
+ ar: "هذه بيانات مرجعية، وليست خريطة مباشرة أو ضمانًا للاكتمال. قد تغيب بعض الأسماء البديلة أو الإحداثيات أو المساحة أو السكان أو المعرفات الخارجية. القيم السكانية قياسات تاريخية يجب قراءتها مع سنة المصدر، ولا تتضمن سجلات المحلات حاليًا بيانات سكانية.",
+ en: "This is reference data, not a live map or a guarantee of completeness. Optional names, aliases, coordinates, area, population, and external IDs can be missing. Subnational population values are historical measurements that must be read with their source year, and locality records do not currently include population.",
+ },
+ provenance: {
+ ar: "تُجمع السجلات من مصادر عامة معتمدة، منها HDX/OCHA وgeoBoundaries وGeoNames وWikidata وجداول مكتب الإحصاء الأميركي المنشورة عبر HDX. يربط كل سجل معرفات المصادر بمراجع مؤرخة، وتتحقق الاختبارات الآلية من التسلسل الإداري ونسب المصادر.",
+ en: "Records are compiled from approved public sources including HDX/OCHA, geoBoundaries, GeoNames, Wikidata, and U.S. Census Bureau tables distributed through HDX. Each record connects source IDs to dated source references, while automated validation checks hierarchy and attribution.",
+ },
+ releaseContext: {
+ ar: "تُتحقق البيانات الأساسية بصيغة JSON آليًا، ثم تُحول إلى إصدارات مرقمة بصيغ JSON وNDJSON وCSV وSQL وYAML وXML. ملفات الإصدار المنشور ثابتة، وتصدر التصحيحات في نسخة جديدة. تحتفظ قيم السكان بسنة مصدرها حتى لا تُعرض القياسات التاريخية بوصفها تقديرات حالية.",
+ en: "Canonical JSON is validated and converted into versioned JSON, NDJSON, CSV, SQL, YAML, and XML artifacts. Published release assets are immutable; fixes are issued in a new tagged release. Population values retain their source year so historical measurements are not presented as current estimates.",
+ },
+ },
+ keywords: {
+ ar: [
+ "بيانات المدن السورية",
+ "بيانات المحافظات السورية",
+ "محافظات سوريا",
+ "مناطق سوريا",
+ "نواحي سوريا",
+ ],
+ en: [
+ "Syrian cities dataset",
+ "Syrian cities CSV",
+ "Syrian cities JSON",
+ "Syria governorates",
+ "Syria governorates dataset",
+ "Syria districts",
+ "Syria districts data",
+ "Syria subdistricts",
+ "Syria localities",
+ "Syrian towns and villages",
+ "Syrian administrative divisions",
+ "Syria places CSV",
+ "Syria maps data",
+ ],
+ },
recordGroupLabels: {
districts: { ar: "منطقة", en: "Districts" },
governorates: { ar: "محافظة", en: "Governorates" },
@@ -218,28 +265,62 @@ const datasetDescriptors = [
ar: "حمّل بيانات الجامعات السورية: الجامعات العامة والخاصة، المعاهد العليا، المواقع الرسمية، المحافظات، التصنيفات، وملفات JSON وCSV.",
en: "Download Syrian university and higher education data with public and private universities, locations, official websites, rankings, and JSON/CSV files.",
},
- keywords: [
- "Syrian universities dataset",
- "Syria universities",
- "Syrian universities list",
- "Syrian higher education data",
- "Syrian university rankings",
- "Syria public universities",
- "Syria private universities",
- "Syria universities CSV",
- "Syria universities JSON",
- "Damascus University data",
- "Syrian higher education dataset",
- "Syrian university data download",
- "جامعات سوريا",
- "بيانات الجامعات السورية",
- "قائمة الجامعات السورية",
- "الجامعات الحكومية السورية",
- "الجامعات الخاصة السورية",
- "تصنيفات الجامعات السورية",
- "بيانات التعليم العالي السوري",
- "تحميل بيانات الجامعات السورية",
- ],
+ details: {
+ coverage: {
+ ar: "تشمل الملفات التعريفية الجامعات العامة والخاصة والافتراضية والمؤسسات التقنية والمعاهد العليا الواقعة ضمن نطاق الإنتاج المعتمد. وبحسب توفر المصادر، قد يتضمن الملف أسماء باللغتين، ونوع المؤسسة، وسنة التأسيس، والموقع الرسمي، والمكان العام، ومعرفات خارجية، وشعارًا معتمدًا، ولقطات تصنيف مؤرخة.",
+ en: "Profiles cover public, private, virtual, and technical or higher institutions within the approved production scope. Depending on available sources, a profile may include bilingual names, type, founding year, official website, public location, external IDs, an approved logo asset, and dated ranking snapshots.",
+ },
+ intendedUses: {
+ ar: [
+ "إنشاء أدلة موحدة للجامعات والمعاهد السورية بأسمائها ومعرفاتها العامة.",
+ "دراسة التوزع الجغرافي وأنواع المؤسسات بالاعتماد على الحقائق العامة المتاحة.",
+ "استخدام لقطات التصنيف المؤرخة في البحث مع الاحتفاظ باسم المزود وسنة اللقطة.",
+ ],
+ en: [
+ "Create normalized directories of Syrian universities and higher institutes with public names and identifiers.",
+ "Study the public geographic distribution and institution types represented in the approved scope.",
+ "Use dated ranking snapshots in research while retaining the provider name and snapshot year.",
+ ],
+ },
+ limitations: {
+ ar: "تبقى حالة التشغيل غير مؤكدة في الملفات الحالية، وقد تغيب بعض المواقع الرسمية أو معرفات Wikidata أو الإحداثيات الموثقة. تغطية التصنيفات جزئية ومرتبطة بزمن ومزود محددين، ولا يجب اعتبارها تقييمًا شاملًا أو آنيًا للجودة. لا تتضمن البيانات سجلات للطلاب أو العاملين.",
+ en: "Operating status remains unconfirmed in the current profiles, and some official websites, Wikidata IDs, or source-backed centroids are missing. Ranking coverage is partial and time-specific; it must not be treated as a complete or current quality assessment. The dataset contains no student or staff records.",
+ },
+ provenance: {
+ ar: "يجب أن تكون المؤسسة ضمن النطاق المعتمد، وأن يؤكد هويتها مصدر عام معتمد. تستشهد السجلات بمصادر مثل المواقع الرسمية للمؤسسات، وWikidata، والأدلة المرجعية القابلة لإعادة الاستخدام، ومزودي التصنيفات المسمّين. ويقترن كل مصدر بمرجع مؤرخ على مستوى السجل.",
+ en: "An institution must fall within the approved scope and be confirmed by an approved public source. Records cite sources such as official institution pages, Wikidata, reusable reference lists, and named ranking providers; every source is paired with a dated record-level reference.",
+ },
+ releaseContext: {
+ ar: "تُنشر هويات الجامعات وأصول الشعارات ولقطات التصنيف في ملفات منفصلة. وتبقى ملفات الكليات والبرامج فارغة عمدًا إلى أن تُراجع مصادر معتمدة قابلة لإعادة الاستخدام. ملفات النسخة المنشورة ثابتة، وتتطلب التصحيحات إصدار نسخة جديدة.",
+ en: "University identities, logo assets, and ranking snapshots are released as separate artifacts. Faculty and program artifacts remain intentionally empty until approved reusable sources are reviewed. Published version assets are immutable, and corrections require a new release.",
+ },
+ },
+ keywords: {
+ ar: [
+ "جامعات سوريا",
+ "بيانات الجامعات السورية",
+ "قائمة الجامعات السورية",
+ "الجامعات الحكومية السورية",
+ "الجامعات الخاصة السورية",
+ "تصنيفات الجامعات السورية",
+ "بيانات التعليم العالي السوري",
+ "تحميل بيانات الجامعات السورية",
+ ],
+ en: [
+ "Syrian universities dataset",
+ "Syria universities",
+ "Syrian universities list",
+ "Syrian higher education data",
+ "Syrian university rankings",
+ "Syria public universities",
+ "Syria private universities",
+ "Syria universities CSV",
+ "Syria universities JSON",
+ "Damascus University data",
+ "Syrian higher education dataset",
+ "Syrian university data download",
+ ],
+ },
recordGroupLabels: {
assets: { ar: "أصل شعار معتمد", en: "Approved logo assets" },
rankings: { ar: "لقطة تصنيف", en: "Ranking snapshots" },
@@ -265,28 +346,62 @@ const datasetDescriptors = [
{
description: {
ar: "حمّل بيانات مواقع النقل السورية: المطارات والموانئ والمعابر الحدودية ومحطات السكك والطرق، مع لقطات حالة مؤرخة وملفات JSON وCSV.",
- en: "Download Syrian transport reference data with public airports, seaports, border crossings, road terminals, rail terminals, dated status snapshots, and JSON/CSV files.",
+ en: "Download source-backed Syrian transport data for airports, ports, border crossings, road and rail terminals, with dated status snapshots and JSON/CSV files.",
+ },
+ details: {
+ coverage: {
+ ar: "تغطي البيانات مواقع مرجعية عامة، منها المطارات المدنية، والموانئ، والمعابر الحدودية، ومحطات السكك الحديدية والطرق، مع إحداثيات ومعرفات عامة موثقة. وتُنشر ملاحظات الحالة والمسار على شكل لقطات تاريخية منفصلة عن هوية الموقع الثابتة.",
+ en: "Coverage includes public civil airports, seaports, border crossings, rail and road terminals, and trade locations with source-backed coordinates and public identifiers. Status and route observations are published as dated historical snapshots separate from stable location identity.",
+ },
+ intendedUses: {
+ ar: [
+ "البحث عن مواقع النقل العامة ومطابقة معرفاتها بين المصادر.",
+ "إنشاء خرائط وأدلة بحثية للمطارات والموانئ والمعابر والمحطات.",
+ "دراسة لقطات الحالة والمسار ضمن تاريخها ومصدرها المحددين.",
+ ],
+ en: [
+ "Look up public transport locations and match their identifiers across sources.",
+ "Build research maps and directories of airports, ports, crossings, and terminals.",
+ "Study status and route snapshots within their explicit date and source context.",
+ ],
+ },
+ limitations: {
+ ar: "لا توفر المجموعة مسارات أو جداول رحلات أو حالة تشغيل مباشرة. تبقى حالة التشغيل غير معروفة لمعظم المواقع، وقد تغيب بعض الأسماء العربية أو الروابط الإدارية أو المعرفات. لا تتضمن البيانات هندسة المسارات أو تفاصيل تكتيكية أو مواقع عسكرية أو نقاط تفتيش.",
+ en: "This dataset does not provide live routing, schedules, access, or operating conditions. Status remains unknown for most locations, and some Arabic names, administrative links, or external IDs are missing. Route geometry, tactical details, military locations, and checkpoints are excluded.",
+ },
+ provenance: {
+ ar: "تشمل المصادر العامة المراجعة OurAirports وUN/LOCODE وGeoNames وWikidata ومؤشر الموانئ العالمي وبيانات HIU/HDX العامة، مع روابط إدارية مراجعة من OpenSyria. وتستخدم مصادر Logistics Cluster فقط لإثبات اللقطات المؤرخة.",
+ en: "Reviewed public sources include OurAirports, UN/LOCODE, GeoNames, Wikidata, the World Port Index, public HIU/HDX data, and reviewed OpenSyria administrative links. Logistics Cluster sources are used only as evidence for dated status and route snapshots.",
+ },
+ releaseContext: {
+ ar: "تبقى هوية الموقع الثابتة منفصلة عن ملاحظات الحالة والمسار المؤرخة. يحمل كل سجل لقطة تاريخ الحالة ومرجع المصدر، وتبقى ملفات الإصدار المرقم ثابتة بعد نشرها.",
+ en: "Stable location identity is kept separate from dated status and route observations. Every snapshot carries an as-of date and source reference, while published versioned release assets remain immutable after publication.",
+ },
+ },
+ keywords: {
+ ar: [
+ "بيانات النقل السورية",
+ "مطارات سوريا",
+ "موانئ سوريا",
+ "المعابر الحدودية في سوريا",
+ ],
+ en: [
+ "Syrian transport dataset",
+ "Syria transport data",
+ "Syria airports dataset",
+ "Syrian airports data",
+ "Syria ports data",
+ "Syria border crossings dataset",
+ "Syria border crossings data",
+ "Syria rail stations data",
+ "Syria road terminals",
+ "Syria transport CSV",
+ "Syria transport JSON",
+ "Syrian logistics data",
+ "Damascus airport data",
+ "Aleppo airport data",
+ ],
},
- keywords: [
- "Syrian transport dataset",
- "Syria transport data",
- "Syria airports dataset",
- "Syrian airports data",
- "Syria ports data",
- "Syria border crossings dataset",
- "Syria border crossings data",
- "Syria rail stations data",
- "Syria road terminals",
- "Syria transport CSV",
- "Syria transport JSON",
- "Syrian logistics data",
- "Damascus airport data",
- "Aleppo airport data",
- "بيانات النقل السورية",
- "مطارات سوريا",
- "موانئ سوريا",
- "المعابر الحدودية في سوريا",
- ],
recordGroupLabels: {
locations: {
ar: "موقع نقل",
@@ -318,26 +433,60 @@ const datasetDescriptors = [
{
description: {
ar: "حمّل بيانات ترقيم الاتصالات السورية مع رمز الدولة، رموز المناطق الثابتة، بادئات الهاتف المحمول، المشغلين، ونطاقات الأرقام العامة مع نسب المصادر وملفات JSON وCSV.",
- en: "Download Syrian telecom numbering metadata with country code, fixed area codes, mobile prefixes, operators, public numbering ranges, source attribution, and JSON/CSV files.",
+ en: "Download source-backed Syrian telecom numbering data with country code, fixed area codes, mobile prefixes, operators, ranges, and JSON/CSV files.",
+ },
+ details: {
+ coverage: {
+ ar: "تغطي المجموعة رمز الدولة +963، وجهات التشغيل والمرجع، ورموز المناطق الثابتة المرتبطة بالمحافظات، وبادئات الهاتف المحمول، ونطاقات الترقيم العامة. هي بيانات وصفية عامة للترقيم، وليست بيانات اشتراكات.",
+ en: "Coverage includes Syria's +963 country code, public operator and reference entities, fixed area codes linked to governorates, assigned mobile prefixes, and public numbering ranges. It is numbering metadata, not subscriber data.",
+ },
+ intendedUses: {
+ ar: [
+ "التحقق من أنماط الأرقام السورية وتنسيقها باستخدام بيانات مرجعية عامة.",
+ "البحث المرجعي عن رموز المناطق وبادئات الهاتف المحمول وجهات الترقيم.",
+ "إنشاء أدلة مدنية وأدوات بحثية تحتفظ بنسب المصدر وتاريخه.",
+ ],
+ en: [
+ "Validate and format Syrian number patterns with public reference metadata.",
+ "Look up fixed area codes, mobile prefixes, and numbering entities.",
+ "Build civic directories and research tools that preserve source attribution and dates.",
+ ],
+ },
+ limitations: {
+ ar: "هذه السجلات ليست سجلًا آنيًا للتخصيصات أو الشبكات. لا تتضمن أرقامًا شخصية أو سجلات مشتركين أو أبراجًا أو تغطية أو أعطالًا. التسميات العربية غير مكتملة، وتبقى بادئة النفاذ الدولي دون قيمة إلى أن يؤكدها مصدر صريح.",
+ en: "These records are not a live assignment or network registry. They contain no personal numbers, subscriber records, towers, coverage, or outages. Arabic labels are incomplete, and the international access prefix remains unset until an explicit source confirms it.",
+ },
+ provenance: {
+ ar: "يعتمد الإصدار على مواد خطة الترقيم العامة التي تستضيفها المنظمة الدولية للاتصالات وأعلنتها الهيئة السورية الناظمة للاتصالات والبريد. وتوفر بيانات الجغرافيا من OpenSyria معرفات المحافظات فقط. يحمل كل سجل معرفات مصادر ومراجع مؤرخة، مع إظهار قيود إعادة الاستخدام.",
+ en: "The release is based on public ITU-hosted numbering-plan material announced by the Syrian telecommunications regulator. OpenSyria geography supplies governorate identifiers only. Every record carries source IDs and dated references, while source reuse limitations remain documented.",
+ },
+ releaseContext: {
+ ar: "تُنشر حقائق الترقيم على شكل لقطات مرجعية مؤرخة، وليس كحقيقة تشغيلية مباشرة. تُولد ملفات JSON وNDJSON وCSV وSQL وYAML وXML من البيانات الأساسية، وتصدر التصحيحات في نسخة مرقمة جديدة.",
+ en: "Numbering facts are published as dated reference snapshots, not live network truth. JSON, NDJSON, CSV, SQL, YAML, and XML artifacts are generated from canonical data, and corrections are issued in a new versioned release.",
+ },
+ },
+ keywords: {
+ ar: [
+ "بيانات الاتصالات السورية",
+ "رموز الهاتف في سوريا",
+ "رمز سوريا الدولي 963",
+ "بادئات الهاتف المحمول في سوريا",
+ ],
+ en: [
+ "Syrian telecom dataset",
+ "Syria phone numbering data",
+ "Syria country code 963",
+ "Syria area codes dataset",
+ "Syrian fixed area codes",
+ "Syria mobile prefixes",
+ "Syriatel prefixes",
+ "MTN Syria prefixes",
+ "WAFA Telecom prefixes",
+ "Syria telecom CSV",
+ "Syria telecom JSON",
+ "Syrian numbering plan",
+ ],
},
- keywords: [
- "Syrian telecom dataset",
- "Syria phone numbering data",
- "Syria country code 963",
- "Syria area codes dataset",
- "Syrian fixed area codes",
- "Syria mobile prefixes",
- "Syriatel prefixes",
- "MTN Syria prefixes",
- "WAFA Telecom prefixes",
- "Syria telecom CSV",
- "Syria telecom JSON",
- "Syrian numbering plan",
- "بيانات الاتصالات السورية",
- "رموز الهاتف في سوريا",
- "رمز سوريا الدولي 963",
- "بادئات الهاتف المحمول في سوريا",
- ],
recordGroupLabels: {
"country-numbering-plans": {
ar: "خطة ترقيم وطنية",
@@ -511,10 +660,11 @@ function buildDatasetCatalogItem(
descriptor?.description ??
apiDescription ??
getFallbackLocalizedText(titleFromSlug(slug)),
+ details: descriptor?.details ?? null,
distributions,
formats: getFormats(artifacts),
id: summary.id ?? manifest?.dataset?.id ?? `opensyria-${slug}`,
- keywords: descriptor?.keywords ?? [],
+ keywords: descriptor?.keywords ?? emptyLocalizedList,
apiDocsUrl: getDatasetApiDocsUrl(slug),
licenseUrl: `${repositoryUrl}/blob/main/LICENSE.md`,
openApiUrl: `${getDatasetsApiBase()}/openapi/${slug}.json`,
diff --git a/src/lib/site.ts b/src/lib/site.ts
index e8bcd35..875f731 100644
--- a/src/lib/site.ts
+++ b/src/lib/site.ts
@@ -1,3 +1,5 @@
+import type { Metadata } from "next"
+
const brandAssetVersion = "20260704-brand-wordmark"
const productionSiteUrl = "https://opensyria.org"
const siteUrl =
@@ -7,60 +9,21 @@ export const siteConfig = {
name: "OpenSyria",
url: siteUrl,
logo: `${siteUrl}/web-app-manifest-512x512.png?v=${brandAssetVersion}`,
- defaultTitle: "OpenSyria | Syrian Datasets, Maps and Research Data",
+ defaultTitle: "OpenSyria | Open, Source-Backed Data About Syria",
defaultDescription:
- "OpenSyria publishes source-backed Syrian datasets for cities, governorates, universities, transport, telecom, maps, research, journalism, civic tools, and Syria-focused apps.",
- keywords: [
- "OpenSyria",
- "Syria data",
- "Syrian datasets",
- "Syria data for developers",
- "Syrian cities dataset",
- "Syrian cities data",
- "Syrian cities CSV",
- "Syria governorates dataset",
- "Syria districts data",
- "Syria subdistricts",
- "Syria localities",
- "Syrian administrative divisions",
- "Syrian universities dataset",
- "Syrian universities data",
- "Syrian university rankings",
- "Syrian higher education data",
- "Syrian transport dataset",
- "Syria transport data",
- "Syria airports dataset",
- "Syria border crossings data",
- "Syria ports data",
- "Syrian telecom dataset",
- "Syria telecom numbering data",
- "Syria mobile prefixes",
- "Syria maps data",
- "Syria app data",
- "Syria CSV data",
- "Syria JSON data",
- "open data Syria",
- "civic intelligence",
- "public data commons",
- "بيانات سوريا",
- "بيانات سورية",
- "بيانات المدن السورية",
- "بيانات المحافظات السورية",
- "بيانات الجامعات السورية",
- "تصنيفات الجامعات السورية",
- ],
+ "OpenSyria is a public data commons for reliable Syrian geography, universities, transport, and telecom data, with downloads, APIs, and cited sources.",
locales: {
ar: {
description:
- "OpenSyria تنشر بيانات سورية موثقة للمدن والمحافظات والمناطق والجامعات، مع تنزيلات JSON وCSV ومراجع للخرائط والبحث والصحافة.",
+ "OpenSyria مشروع بيانات عامة ينشر بيانات سورية موثقة للجغرافيا والجامعات والنقل والاتصالات، مع تنزيلات وواجهات API ومصادر مرجعية.",
ogLocale: "ar_SY",
- title: "OpenSyria | بيانات سورية للمدن والجامعات والخرائط",
+ title: "OpenSyria | بيانات سورية مفتوحة وموثقة بالمصادر",
},
en: {
description:
- "OpenSyria publishes source-backed Syrian datasets for cities, governorates, universities, transport, telecom, maps, research, journalism, civic tools, and Syria-focused apps.",
+ "OpenSyria is a public data commons for reliable Syrian geography, universities, transport, and telecom data, with downloads, APIs, and cited sources.",
ogLocale: "en_US",
- title: "OpenSyria | Syrian Datasets, Maps and Research Data",
+ title: "OpenSyria | Open, Source-Backed Data About Syria",
},
},
} as const
@@ -84,6 +47,18 @@ export const organizationSameAsLinks = [
siteLinks.linkedIn,
] as const
+export const indexableRobots = {
+ follow: true,
+ googleBot: {
+ follow: true,
+ index: true,
+ "max-image-preview": "large",
+ "max-snippet": -1,
+ "max-video-preview": -1,
+ },
+ index: true,
+} as const satisfies Metadata["robots"]
+
export const socialPreviewImages = {
openGraph: {
alt: "OpenSyria social preview showing the OpenSyria wordmark and Syria map.",
diff --git a/src/proxy.ts b/src/proxy.ts
index 9427187..1fdfe05 100644
--- a/src/proxy.ts
+++ b/src/proxy.ts
@@ -28,6 +28,12 @@ const trackingSearchParamNames = new Set([
const trackingSearchParamPrefixes = ["utm_", "hsa_", "mtm_", "pk_"] as const
export default function proxy(request: NextRequest) {
+ const canonicalEnglishUrl = getCanonicalEnglishUrl(request)
+
+ if (canonicalEnglishUrl) {
+ return NextResponse.redirect(canonicalEnglishUrl, 308)
+ }
+
const cleanUrl = getCleanTrackingUrl(request)
if (cleanUrl) {
@@ -46,8 +52,27 @@ export default function proxy(request: NextRequest) {
return intlMiddleware(request)
}
+function getCanonicalEnglishUrl(request: NextRequest) {
+ const pathname = request.nextUrl.pathname
+
+ if (pathname !== "/en" && !pathname.startsWith("/en/")) {
+ return null
+ }
+
+ const url = request.nextUrl.clone()
+ url.pathname = pathname.slice(3) || "/"
+ removeTrackingSearchParams(url)
+
+ return url
+}
+
function getCleanTrackingUrl(request: NextRequest) {
const url = request.nextUrl.clone()
+
+ return removeTrackingSearchParams(url) ? url : null
+}
+
+function removeTrackingSearchParams(url: { searchParams: URLSearchParams }) {
let hasTrackingParam = false
for (const param of [...url.searchParams.keys()]) {
@@ -59,7 +84,7 @@ function getCleanTrackingUrl(request: NextRequest) {
}
}
- return hasTrackingParam ? url : null
+ return hasTrackingParam
}
function isTrackingSearchParam(param: string) {
@@ -89,5 +114,5 @@ function appendHeader(response: NextResponse, name: string, value: string) {
}
export const config = {
- matcher: "/((?!api|trpc|health|_next|_vercel|.*\\..*).*)",
+ matcher: "/((?!trpc|health|_next|_vercel|.*\\..*).*)",
}
From 98da9432a32942966b4ff22460569d21b6ee8acc Mon Sep 17 00:00:00 2001
From: Mustafa Zeydani
Date: Wed, 19 Aug 2026 23:46:19 +0300
Subject: [PATCH 2/2] chore(deps): resolve audited transitive advisories
---
pnpm-lock.yaml | 89 ++++++++++++++++++++++++---------------------
pnpm-workspace.yaml | 11 ++++--
2 files changed, 55 insertions(+), 45 deletions(-)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e7f2b77..685490a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,10 +6,15 @@ settings:
overrides:
'@hono/node-server': 2.0.11
- brace-expansion: 5.0.8
- fast-uri: 3.1.4
- postcss: 8.5.18
+ brace-expansion: 5.0.9
+ fast-uri: 3.1.5
+ hono: 4.12.34
+ ip-address: 10.3.1
+ js-yaml: 4.3.1
+ nanoid: 3.3.18
+ postcss: 8.5.23
sharp: 0.35.3
+ undici: 7.29.0
importers:
@@ -441,7 +446,7 @@ packages:
resolution: {integrity: sha512-bjD221KPLoJTWUwso1J6fGKiTXEUFedG/s0visavY4zakFPkeGURMRNly+FhBHs7T8Dz4qHaZIMX9ZoJHSJtKA==}
engines: {node: '>=20'}
peerDependencies:
- hono: ^4
+ hono: 4.12.34
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
@@ -1080,8 +1085,8 @@ packages:
resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==}
engines: {node: '>=18'}
- brace-expansion@5.0.8:
- resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
+ brace-expansion@5.0.9:
+ resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
engines: {node: 20 || >=22}
braces@3.0.3:
@@ -1399,8 +1404,8 @@ packages:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
- fast-uri@3.1.4:
- resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==}
+ fast-uri@3.1.5:
+ resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==}
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@@ -1509,8 +1514,8 @@ packages:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'}
- hono@4.12.27:
- resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==}
+ hono@4.12.34:
+ resolution: {integrity: sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==}
engines: {node: '>=16.9.0'}
http-errors@2.0.1:
@@ -1555,8 +1560,8 @@ packages:
intl-messageformat@11.2.9:
resolution: {integrity: sha512-cGzymZerpDhVXRKjKLgXKda9gI29TU2o88L7gwNMHp3WZVxA/0c5tX52udXbW9JklDApolvMXZG6Dhhdz5eirA==}
- ip-address@10.2.0:
- resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==}
+ ip-address@10.3.1:
+ resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==}
engines: {node: '>= 12'}
ipaddr.js@1.9.1:
@@ -1669,8 +1674,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- js-yaml@4.3.0:
- resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
+ js-yaml@4.3.1:
+ resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
hasBin: true
jsesc@3.1.0:
@@ -1876,8 +1881,8 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
- nanoid@3.3.15:
- resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
+ nanoid@3.3.18:
+ resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@@ -2051,8 +2056,8 @@ packages:
resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==}
engines: {node: '>=4'}
- postcss@8.5.18:
- resolution: {integrity: sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==}
+ postcss@8.5.23:
+ resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
engines: {node: ^10 || ^12 || >=14}
powershell-utils@0.1.0:
@@ -2348,8 +2353,8 @@ packages:
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
- undici@7.28.0:
- resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
+ undici@7.29.0:
+ resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
engines: {node: '>=20.18.1'}
unicorn-magic@0.3.0:
@@ -2836,7 +2841,7 @@ snapshots:
open: 8.4.2
picomatch: 4.0.4
systeminformation: 5.31.12
- undici: 7.28.0
+ undici: 7.29.0
which: 4.0.0
yocto-spinner: 1.2.0
@@ -2876,9 +2881,9 @@ snapshots:
dependencies:
'@formatjs/fast-memoize': 3.1.6
- '@hono/node-server@2.0.11(hono@4.12.27)':
+ '@hono/node-server@2.0.11(hono@4.12.34)':
dependencies:
- hono: 4.12.27
+ hono: 4.12.34
'@img/colour@1.1.0': {}
@@ -3007,7 +3012,7 @@ snapshots:
'@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)':
dependencies:
- '@hono/node-server': 2.0.11(hono@4.12.27)
+ '@hono/node-server': 2.0.11(hono@4.12.34)
ajv: 8.20.0
ajv-formats: 3.0.1(ajv@8.20.0)
content-type: 1.0.5
@@ -3017,7 +3022,7 @@ snapshots:
eventsource-parser: 3.1.0
express: 5.2.1
express-rate-limit: 8.5.2(express@5.2.1)
- hono: 4.12.27
+ hono: 4.12.34
jose: 6.2.3
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
@@ -3269,7 +3274,7 @@ snapshots:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.3.2
'@tailwindcss/oxide': 4.3.2
- postcss: 8.5.18
+ postcss: 8.5.23
tailwindcss: 4.3.2
'@ts-morph/common@0.27.0':
@@ -3308,7 +3313,7 @@ snapshots:
ajv@8.20.0:
dependencies:
fast-deep-equal: 3.1.3
- fast-uri: 3.1.4
+ fast-uri: 3.1.5
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
@@ -3350,7 +3355,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- brace-expansion@5.0.8:
+ brace-expansion@5.0.9:
dependencies:
balanced-match: 4.0.4
@@ -3479,7 +3484,7 @@ snapshots:
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.1
- js-yaml: 4.3.0
+ js-yaml: 4.3.1
parse-json: 5.2.0
optionalDependencies:
typescript: 6.0.3
@@ -3617,7 +3622,7 @@ snapshots:
express-rate-limit@8.5.2(express@5.2.1):
dependencies:
express: 5.2.1
- ip-address: 10.2.0
+ ip-address: 10.3.1
express@5.2.1:
dependencies:
@@ -3662,7 +3667,7 @@ snapshots:
merge2: 1.4.1
micromatch: 4.0.8
- fast-uri@3.1.4: {}
+ fast-uri@3.1.5: {}
fastq@1.20.1:
dependencies:
@@ -3768,7 +3773,7 @@ snapshots:
dependencies:
function-bind: 1.1.2
- hono@4.12.27: {}
+ hono@4.12.34: {}
http-errors@2.0.1:
dependencies:
@@ -3808,7 +3813,7 @@ snapshots:
'@formatjs/fast-memoize': 3.1.6
'@formatjs/icu-messageformat-parser': 3.5.12
- ip-address@10.2.0: {}
+ ip-address@10.3.1: {}
ipaddr.js@1.9.1: {}
@@ -3876,7 +3881,7 @@ snapshots:
js-tokens@4.0.0: {}
- js-yaml@4.3.0:
+ js-yaml@4.3.1:
dependencies:
argparse: 2.0.1
@@ -4033,13 +4038,13 @@ snapshots:
minimatch@10.2.5:
dependencies:
- brace-expansion: 5.0.8
+ brace-expansion: 5.0.9
minimist@1.2.8: {}
ms@2.1.3: {}
- nanoid@3.3.15: {}
+ nanoid@3.3.18: {}
negotiator@1.0.0: {}
@@ -4073,7 +4078,7 @@ snapshots:
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.42
caniuse-lite: 1.0.30001800
- postcss: 8.5.18
+ postcss: 8.5.23
react: 19.2.8
react-dom: 19.2.8(react@19.2.8)
styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.8)
@@ -4208,9 +4213,9 @@ snapshots:
cssesc: 3.0.0
util-deprecate: 1.0.2
- postcss@8.5.18:
+ postcss@8.5.23:
dependencies:
- nanoid: 3.3.15
+ nanoid: 3.3.18
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -4361,7 +4366,7 @@ snapshots:
kleur: 4.1.5
open: 11.0.0
ora: 8.2.0
- postcss: 8.5.18
+ postcss: 8.5.23
postcss-selector-parser: 7.1.4
prompts: 2.4.2
recast: 0.23.12
@@ -4369,7 +4374,7 @@ snapshots:
tailwind-merge: 3.6.0
ts-morph: 26.0.0
tsconfig-paths: 4.2.0
- undici: 7.28.0
+ undici: 7.29.0
validate-npm-package-name: 7.0.2
zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76)
@@ -4553,7 +4558,7 @@ snapshots:
undici-types@8.3.0: {}
- undici@7.28.0: {}
+ undici@7.29.0: {}
unicorn-magic@0.3.0: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index bd91c60..a18a658 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -3,10 +3,15 @@ packages:
overrides:
'@hono/node-server': 2.0.11
- brace-expansion: 5.0.8
- fast-uri: 3.1.4
- postcss: 8.5.18
+ brace-expansion: 5.0.9
+ fast-uri: 3.1.5
+ hono: 4.12.34
+ ip-address: 10.3.1
+ js-yaml: 4.3.1
+ nanoid: 3.3.18
+ postcss: 8.5.23
sharp: 0.35.3
+ undici: 7.29.0
minimumReleaseAge: 1440
minimumReleaseAgeStrict: true