From 0c45435db9245ca7e55ab52a326fa7538ff3e9a6 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Wed, 4 Feb 2026 09:32:25 +0700 Subject: [PATCH] feat: finalize provider parity and SEO/frontend route coverage --- .../Http/Controllers/Web/SiteController.php | 126 ++++++++++++++++++ app/resources/views/site/api-docs.blade.php | 73 +++++----- .../views/site/emoji-detail.blade.php | 30 ++++- app/resources/views/site/home.blade.php | 10 +- app/resources/views/site/layout.blade.php | 13 ++ app/resources/views/site/pricing.blade.php | 85 +++++++++++- app/resources/views/site/privacy.blade.php | 2 +- app/resources/views/site/support.blade.php | 52 ++++++++ app/resources/views/site/terms.blade.php | 2 +- app/routes/web.php | 3 + app/tests/Feature/ApiV1EndpointsTest.php | 65 +++++++++ app/tests/Feature/SitePagesTest.php | 3 + migration-test-guide.md | 121 +++++++++++++++++ rebuild-progress.md | 22 +-- 14 files changed, 550 insertions(+), 57 deletions(-) create mode 100644 app/resources/views/site/support.blade.php create mode 100644 migration-test-guide.md diff --git a/app/app/Http/Controllers/Web/SiteController.php b/app/app/Http/Controllers/Web/SiteController.php index f4442c9..ea1ca15 100644 --- a/app/app/Http/Controllers/Web/SiteController.php +++ b/app/app/Http/Controllers/Web/SiteController.php @@ -97,6 +97,11 @@ class SiteController extends Controller return view('site.pricing'); } + public function support(): View + { + return view('site.support'); + } + public function privacy(): View { return view('site.privacy'); @@ -143,6 +148,59 @@ class SiteController extends Controller ]); } + public function robotsTxt(): Response + { + $base = rtrim(config('app.url', request()->getSchemeAndHttpHost()), '/'); + $body = "User-agent: *\nAllow: /\n\nSitemap: ".$base."/sitemap.xml\n"; + + return response($body, 200)->header('Content-Type', 'text/plain; charset=UTF-8'); + } + + public function sitemapXml(): Response + { + $data = $this->loadDataset(); + $items = is_array($data['emojis'] ?? null) ? $data['emojis'] : []; + $base = rtrim(config('app.url', request()->getSchemeAndHttpHost()), '/'); + + $lastUpdatedTs = isset($data['last_updated_ts']) ? (int) $data['last_updated_ts'] : time(); + $lastUpdated = gmdate('Y-m-d\TH:i:s\Z', $lastUpdatedTs); + + $urls = [ + ['loc' => $base.'/', 'priority' => '0.8', 'changefreq' => 'daily'], + ['loc' => $base.'/api-docs', 'priority' => '0.5', 'changefreq' => 'weekly'], + ['loc' => $base.'/pricing', 'priority' => '0.7', 'changefreq' => 'weekly'], + ['loc' => $base.'/privacy', 'priority' => '0.3', 'changefreq' => 'monthly'], + ['loc' => $base.'/terms', 'priority' => '0.3', 'changefreq' => 'monthly'], + ['loc' => $base.'/support', 'priority' => '0.4', 'changefreq' => 'weekly'], + ]; + + foreach ($items as $item) { + $slug = trim((string) ($item['slug'] ?? '')); + if ($slug === '' || $this->shouldHideForSitemap($item)) { + continue; + } + $urls[] = [ + 'loc' => $base.'/emoji/'.$slug, + 'priority' => '0.6', + 'changefreq' => 'weekly', + ]; + } + + $xml = ''."\n"; + $xml .= ''."\n"; + foreach ($urls as $url) { + $xml .= " \n"; + $xml .= ' '.htmlspecialchars((string) $url['loc'], ENT_XML1)."\n"; + $xml .= ' '.$lastUpdated."\n"; + $xml .= ' '.$url['changefreq']."\n"; + $xml .= ' '.$url['priority']."\n"; + $xml .= " \n"; + } + $xml .= ''."\n"; + + return response($xml, 200)->header('Content-Type', 'application/xml; charset=UTF-8'); + } + /** * @return array */ @@ -155,4 +213,72 @@ class SiteController extends Controller return $out; } + + /** + * @return array + */ + private function loadDataset(): array + { + $dataPath = (string) config('dewemoji.data_path'); + if (!is_file($dataPath)) { + return ['emojis' => []]; + } + + $raw = file_get_contents($dataPath); + if ($raw === false) { + return ['emojis' => []]; + } + + $decoded = json_decode($raw, true); + if (!is_array($decoded)) { + return ['emojis' => []]; + } + + return $decoded; + } + + /** + * @param array $emoji + */ + private function shouldHideForSitemap(array $emoji): bool + { + $name = strtolower(trim((string) ($emoji['name'] ?? ''))); + $category = strtolower(trim((string) ($emoji['category'] ?? ''))); + $subcategory = strtolower(trim((string) ($emoji['subcategory'] ?? ''))); + + if ($subcategory === 'family' || str_starts_with($name, 'family:')) { + return true; + } + if (preg_match('~\bwoman: beard\b~i', $name)) { + return true; + } + if (preg_match('~\bmen with bunny ears\b~i', $name)) { + return true; + } + if (preg_match('~\bpregnant man\b~i', $name)) { + return true; + } + if ($category === 'people & body') { + if (preg_match('~\bmen holding hands\b~i', $name)) { + return true; + } + if (preg_match('~\bwomen holding hands\b~i', $name)) { + return true; + } + if (preg_match('~kiss:.*\bman,\s*man\b~i', $name)) { + return true; + } + if (preg_match('~kiss:.*\bwoman,\s*woman\b~i', $name)) { + return true; + } + if (preg_match('~couple.*\bman,\s*man\b~i', $name)) { + return true; + } + if (preg_match('~couple.*\bwoman,\s*woman\b~i', $name)) { + return true; + } + } + + return false; + } } diff --git a/app/resources/views/site/api-docs.blade.php b/app/resources/views/site/api-docs.blade.php index 50bb6e2..164f635 100644 --- a/app/resources/views/site/api-docs.blade.php +++ b/app/resources/views/site/api-docs.blade.php @@ -1,49 +1,42 @@ @extends('site.layout') @section('title', 'API Docs - Dewemoji') +@section('meta_description', 'Dewemoji API docs for emoji search, categories, license verification, activation, and system endpoints.') + +@push('jsonld') + +@endpush @section('content') -
-

API Docs

-

Current extension-compatible endpoints exposed by the rebuild app.

+
+

API Docs

+

Base URL: {{ url('/v1') }}

-

Base URL

-

{{ url('/') }}/v1

- -

Endpoints

-
    -
  • GET /v1/categories - category + subcategory map
  • -
  • GET /v1/emojis - paginated emoji list/search
  • -
  • POST /v1/license/verify - license validation contract
  • +
    +

    Core endpoints

    +
      +
    • GET /v1/categories
    • +
    • GET /v1/emojis?q=<term>&category=<label>&subcategory=<slug>
    • +
    • GET /v1/emoji/{slug} or /v1/emoji?slug=
    • +
    • POST /v1/license/verify
    • +
    • POST /v1/license/activate
    • +
    • POST /v1/license/deactivate
    +
    -

    Example: emojis

    -
    GET /v1/emojis?q=love&category=Smileys%20%26%20Emotion&page=1&limit=20
    - -

    Example response

    -
    {
    -  "items": [
    -    {
    -      "emoji": "😀",
    -      "name": "grinning face",
    -      "slug": "grinning-face",
    -      "category": "Smileys & Emotion",
    -      "subcategory": "face-smiling",
    -      "supports_skin_tone": false,
    -      "summary": "A happy smiling face."
    -    }
    -  ],
    -  "total": 1,
    -  "page": 1,
    -  "limit": 20
    -}
    - -

    Try it quickly

    -

    - Open categories JSON - · - Open emojis JSON -

    -
+
+

Auth

+

Use Authorization: Bearer YOUR_LICENSE_KEY (preferred) or X-License-Key.

+
curl -H "Authorization: Bearer YOUR_LICENSE_KEY" \
+"{{ url('/v1/emojis') }}?q=love&limit=50&page=1"
+
+ @endsection - diff --git a/app/resources/views/site/emoji-detail.blade.php b/app/resources/views/site/emoji-detail.blade.php index bf1cada..aad6cf2 100644 --- a/app/resources/views/site/emoji-detail.blade.php +++ b/app/resources/views/site/emoji-detail.blade.php @@ -1,6 +1,7 @@ @extends('site.layout') @section('title', ($emoji['name'] ?? 'Emoji').' - Dewemoji') +@section('meta_description', ($emoji['description'] ?? 'Emoji detail').' Discover meaning, keywords, and copy-ready formats on Dewemoji.') @php $name = $emoji['name'] ?? ''; @@ -22,6 +23,30 @@ $keywords = array_slice($emoji['keywords_en'] ?? [], 0, 16); @endphp +@push('jsonld') + +@endpush + @section('content')