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) { - + +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("title")} +

+

+ {t("description")} +

+ + +
+ + +
+
+ +
+
+
+
+
+

+ {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) => ( +
  1. +
    + + {String(index + 1).padStart(2, "0")} + +
    +

    + {t(`steps.${step}.title`)} +

    +

    + {t(`steps.${step}.description`)} +

    +
  2. + ))} +
+
+
+ +
+
+
+
+
+
+

+ {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 ( + <> + +