feat: finalize provider parity and SEO/frontend route coverage
This commit is contained in:
@@ -97,6 +97,11 @@ class SiteController extends Controller
|
|||||||
return view('site.pricing');
|
return view('site.pricing');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function support(): View
|
||||||
|
{
|
||||||
|
return view('site.support');
|
||||||
|
}
|
||||||
|
|
||||||
public function privacy(): View
|
public function privacy(): View
|
||||||
{
|
{
|
||||||
return view('site.privacy');
|
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 = '<?xml version="1.0" encoding="UTF-8"?>'."\n";
|
||||||
|
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'."\n";
|
||||||
|
foreach ($urls as $url) {
|
||||||
|
$xml .= " <url>\n";
|
||||||
|
$xml .= ' <loc>'.htmlspecialchars((string) $url['loc'], ENT_XML1)."</loc>\n";
|
||||||
|
$xml .= ' <lastmod>'.$lastUpdated."</lastmod>\n";
|
||||||
|
$xml .= ' <changefreq>'.$url['changefreq']."</changefreq>\n";
|
||||||
|
$xml .= ' <priority>'.$url['priority']."</priority>\n";
|
||||||
|
$xml .= " </url>\n";
|
||||||
|
}
|
||||||
|
$xml .= '</urlset>'."\n";
|
||||||
|
|
||||||
|
return response($xml, 200)->header('Content-Type', 'application/xml; charset=UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string,string>
|
* @return array<string,string>
|
||||||
*/
|
*/
|
||||||
@@ -155,4 +213,72 @@ class SiteController extends Controller
|
|||||||
|
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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<string,mixed> $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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,42 @@
|
|||||||
@extends('site.layout')
|
@extends('site.layout')
|
||||||
|
|
||||||
@section('title', 'API Docs - Dewemoji')
|
@section('title', 'API Docs - Dewemoji')
|
||||||
|
@section('meta_description', 'Dewemoji API docs for emoji search, categories, license verification, activation, and system endpoints.')
|
||||||
|
|
||||||
|
@push('jsonld')
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "TechArticle",
|
||||||
|
"headline": "Dewemoji API Documentation",
|
||||||
|
"about": "Emoji search API",
|
||||||
|
"url": "{{ url('/api-docs') }}"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<section class="card" style="padding: 20px;">
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 py-10">
|
||||||
<h1 style="margin-top:0;">API Docs</h1>
|
<h1 class="font-display text-4xl font-bold">API Docs</h1>
|
||||||
<p style="color:var(--muted);">Current extension-compatible endpoints exposed by the rebuild app.</p>
|
<p class="text-gray-400 mt-2">Base URL: <code>{{ url('/v1') }}</code></p>
|
||||||
|
|
||||||
<h2>Base URL</h2>
|
<div class="glass-card rounded-2xl p-5 mt-6">
|
||||||
<p><code>{{ url('/') }}/v1</code></p>
|
<h2 class="text-lg font-semibold">Core endpoints</h2>
|
||||||
|
<ul class="mt-3 space-y-1 text-sm text-gray-300">
|
||||||
<h2>Endpoints</h2>
|
<li><code>GET /v1/categories</code></li>
|
||||||
<ul>
|
<li><code>GET /v1/emojis?q=<term>&category=<label>&subcategory=<slug></code></li>
|
||||||
<li><code>GET /v1/categories</code> - category + subcategory map</li>
|
<li><code>GET /v1/emoji/{slug}</code> or <code>/v1/emoji?slug=</code></li>
|
||||||
<li><code>GET /v1/emojis</code> - paginated emoji list/search</li>
|
<li><code>POST /v1/license/verify</code></li>
|
||||||
<li><code>POST /v1/license/verify</code> - license validation contract</li>
|
<li><code>POST /v1/license/activate</code></li>
|
||||||
|
<li><code>POST /v1/license/deactivate</code></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2>Example: emojis</h2>
|
<div class="glass-card rounded-2xl p-5 mt-4">
|
||||||
<pre class="card" style="padding:12px;overflow:auto;">GET /v1/emojis?q=love&category=Smileys%20%26%20Emotion&page=1&limit=20</pre>
|
<h2 class="text-lg font-semibold">Auth</h2>
|
||||||
|
<p class="text-sm text-gray-300 mt-2">Use <code>Authorization: Bearer YOUR_LICENSE_KEY</code> (preferred) or <code>X-License-Key</code>.</p>
|
||||||
<h2>Example response</h2>
|
<pre class="mt-3 text-xs overflow-x-auto bg-black/30 rounded-lg p-3"><code>curl -H "Authorization: Bearer YOUR_LICENSE_KEY" \
|
||||||
<pre class="card" style="padding:12px;overflow:auto;">{
|
"{{ url('/v1/emojis') }}?q=love&limit=50&page=1"</code></pre>
|
||||||
"items": [
|
</div>
|
||||||
{
|
</div>
|
||||||
"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
|
|
||||||
}</pre>
|
|
||||||
|
|
||||||
<h2>Try it quickly</h2>
|
|
||||||
<p>
|
|
||||||
<a href="{{ url('/v1/categories') }}" target="_blank" style="color:var(--brand);">Open categories JSON</a>
|
|
||||||
·
|
|
||||||
<a href="{{ url('/v1/emojis?q=love&limit=5&page=1') }}" target="_blank" style="color:var(--brand);">Open emojis JSON</a>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@extends('site.layout')
|
@extends('site.layout')
|
||||||
|
|
||||||
@section('title', ($emoji['name'] ?? 'Emoji').' - Dewemoji')
|
@section('title', ($emoji['name'] ?? 'Emoji').' - Dewemoji')
|
||||||
|
@section('meta_description', ($emoji['description'] ?? 'Emoji detail').' Discover meaning, keywords, and copy-ready formats on Dewemoji.')
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$name = $emoji['name'] ?? '';
|
$name = $emoji['name'] ?? '';
|
||||||
@@ -22,6 +23,30 @@
|
|||||||
$keywords = array_slice($emoji['keywords_en'] ?? [], 0, 16);
|
$keywords = array_slice($emoji['keywords_en'] ?? [], 0, 16);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
|
@push('jsonld')
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@graph": [
|
||||||
|
{
|
||||||
|
"@@type": "CreativeWork",
|
||||||
|
"name": @json($name),
|
||||||
|
"description": @json($description),
|
||||||
|
"url": @json(url('/emoji/'.$slug))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{"@@type":"ListItem","position":1,"name":"Home","item":@json(url('/'))},
|
||||||
|
{"@@type":"ListItem","position":2,"name":"Emoji","item":@json(url('/browse'))},
|
||||||
|
{"@@type":"ListItem","position":3,"name":@json($name),"item":@json(url('/emoji/'.$slug))}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="flex h-screen">
|
<div class="flex h-screen">
|
||||||
<aside class="hidden lg:flex w-20 lg:w-64 h-full glass-panel flex-col justify-between p-4 z-20">
|
<aside class="hidden lg:flex w-20 lg:w-64 h-full glass-panel flex-col justify-between p-4 z-20">
|
||||||
@@ -153,7 +178,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="lg:hidden fixed bottom-0 inset-x-0 z-50 border-t border-white/10 bg-[#0b0b0f]/95 backdrop-blur px-2 py-2">
|
<nav class="lg:hidden fixed bottom-0 inset-x-0 z-50 border-t border-white/10 bg-[#0b0b0f]/95 backdrop-blur px-2 py-2">
|
||||||
<div class="grid grid-cols-5 gap-1 text-[11px]">
|
<div class="grid grid-cols-6 gap-1 text-[11px]">
|
||||||
<a href="{{ route('home') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-brand-sun bg-white/5">
|
<a href="{{ route('home') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-brand-sun bg-white/5">
|
||||||
<i data-lucide="layout-grid" class="w-4 h-4"></i><span>Discover</span>
|
<i data-lucide="layout-grid" class="w-4 h-4"></i><span>Discover</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -163,6 +188,9 @@
|
|||||||
<a href="{{ route('pricing') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
|
<a href="{{ route('pricing') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
|
||||||
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Pricing</span>
|
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Pricing</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ route('support') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
|
||||||
|
<i data-lucide="life-buoy" class="w-4 h-4"></i><span>Support</span>
|
||||||
|
</a>
|
||||||
<a href="{{ route('privacy') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
|
<a href="{{ route('privacy') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
|
||||||
<i data-lucide="shield-check" class="w-4 h-4"></i><span>Privacy</span>
|
<i data-lucide="shield-check" class="w-4 h-4"></i><span>Privacy</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@extends('site.layout')
|
@extends('site.layout')
|
||||||
|
|
||||||
@section('title', 'Dewemoji - Discover')
|
@section('title', 'Dewemoji - Discover')
|
||||||
|
@section('meta_description', 'Search emojis by keyword, meaning, and category. Explore Dewemoji with fast API-powered results.')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
<style>
|
<style>
|
||||||
@@ -35,6 +36,10 @@
|
|||||||
<i data-lucide="badge-dollar-sign" class="w-5 h-5 group-hover:scale-110 transition-transform"></i>
|
<i data-lucide="badge-dollar-sign" class="w-5 h-5 group-hover:scale-110 transition-transform"></i>
|
||||||
<span class="text-sm font-medium hidden lg:block">Pricing</span>
|
<span class="text-sm font-medium hidden lg:block">Pricing</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ route('support') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all group">
|
||||||
|
<i data-lucide="life-buoy" class="w-5 h-5 group-hover:scale-110 transition-transform"></i>
|
||||||
|
<span class="text-sm font-medium hidden lg:block">Support</span>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
@@ -145,7 +150,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="lg:hidden fixed bottom-0 inset-x-0 z-50 border-t border-white/10 bg-[#0b0b0f]/95 backdrop-blur px-2 py-2">
|
<nav class="lg:hidden fixed bottom-0 inset-x-0 z-50 border-t border-white/10 bg-[#0b0b0f]/95 backdrop-blur px-2 py-2">
|
||||||
<div class="grid grid-cols-5 gap-1 text-[11px]">
|
<div class="grid grid-cols-6 gap-1 text-[11px]">
|
||||||
<a href="{{ route('home') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-brand-sun bg-white/5">
|
<a href="{{ route('home') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-brand-sun bg-white/5">
|
||||||
<i data-lucide="layout-grid" class="w-4 h-4"></i><span>Discover</span>
|
<i data-lucide="layout-grid" class="w-4 h-4"></i><span>Discover</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -155,6 +160,9 @@
|
|||||||
<a href="{{ route('pricing') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
|
<a href="{{ route('pricing') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
|
||||||
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Pricing</span>
|
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Pricing</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ route('support') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
|
||||||
|
<i data-lucide="life-buoy" class="w-4 h-4"></i><span>Support</span>
|
||||||
|
</a>
|
||||||
<a href="{{ route('privacy') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
|
<a href="{{ route('privacy') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
|
||||||
<i data-lucide="shield-check" class="w-4 h-4"></i><span>Privacy</span>
|
<i data-lucide="shield-check" class="w-4 h-4"></i><span>Privacy</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -10,9 +10,21 @@
|
|||||||
$canonicalPath = '/'.trim($canonicalPath, '/');
|
$canonicalPath = '/'.trim($canonicalPath, '/');
|
||||||
}
|
}
|
||||||
$canonicalUrl = rtrim(config('app.url', request()->getSchemeAndHttpHost()), '/').$canonicalPath;
|
$canonicalUrl = rtrim(config('app.url', request()->getSchemeAndHttpHost()), '/').$canonicalPath;
|
||||||
|
$metaDescription = trim($__env->yieldContent('meta_description')) ?: 'Search, copy, and explore emojis with Dewemoji. Free browser extension and API for developers.';
|
||||||
|
$metaTitle = trim($__env->yieldContent('title')) ?: 'Dewemoji';
|
||||||
@endphp
|
@endphp
|
||||||
<title>@yield('title', 'Dewemoji')</title>
|
<title>@yield('title', 'Dewemoji')</title>
|
||||||
|
<meta name="description" content="{{ $metaDescription }}">
|
||||||
<link rel="canonical" href="{{ $canonicalUrl }}">
|
<link rel="canonical" href="{{ $canonicalUrl }}">
|
||||||
|
<meta property="og:title" content="{{ $metaTitle }}">
|
||||||
|
<meta property="og:description" content="{{ $metaDescription }}">
|
||||||
|
<meta property="og:url" content="{{ $canonicalUrl }}">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:site_name" content="Dewemoji">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="{{ $metaTitle }}">
|
||||||
|
<meta name="twitter:description" content="{{ $metaDescription }}">
|
||||||
|
<meta name="theme-color" content="#2053ff">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
@@ -87,6 +99,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@stack('head')
|
@stack('head')
|
||||||
|
@stack('jsonld')
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-[#050505] text-white min-h-screen selection:bg-brand-ocean selection:text-white">
|
<body class="bg-[#050505] text-white min-h-screen selection:bg-brand-ocean selection:text-white">
|
||||||
<div class="fixed top-0 left-0 w-full h-full overflow-hidden -z-10 pointer-events-none">
|
<div class="fixed top-0 left-0 w-full h-full overflow-hidden -z-10 pointer-events-none">
|
||||||
|
|||||||
@@ -1,11 +1,86 @@
|
|||||||
@extends('site.layout')
|
@extends('site.layout')
|
||||||
|
|
||||||
@section('title', 'Pricing - Dewemoji')
|
@section('title', 'Pricing - Dewemoji')
|
||||||
|
@section('meta_description', 'See Dewemoji plans for Free, Pro subscription, and Lifetime. One key unlocks Extension + API with up to 3 Chrome profile activations.')
|
||||||
|
|
||||||
|
@push('jsonld')
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@graph": [
|
||||||
|
{
|
||||||
|
"@@type": "Product",
|
||||||
|
"name": "Dewemoji Pro License",
|
||||||
|
"description": "One Pro license unlocks the Dewemoji extension and API.",
|
||||||
|
"brand": {"@@type": "Brand", "name": "Dewemoji"},
|
||||||
|
"offers": [
|
||||||
|
{"@@type":"Offer","price":"3.00","priceCurrency":"USD","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-subscription"},
|
||||||
|
{"@@type":"Offer","price":"27.00","priceCurrency":"USD","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-subscription"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@@type": "Product",
|
||||||
|
"name": "Dewemoji Lifetime License",
|
||||||
|
"description": "Lifetime access to extension + API.",
|
||||||
|
"brand": {"@@type": "Brand", "name": "Dewemoji"},
|
||||||
|
"offers": {"@@type":"Offer","price":"69.00","priceCurrency":"USD","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-lifetime"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<section class="card" style="padding: 20px;">
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 py-10">
|
||||||
<h1 style="margin-top:0;">Pricing</h1>
|
<div class="text-center mb-8">
|
||||||
<p style="color:var(--muted);">Phase 3 placeholder page. We will wire real pricing content and purchase flow in a later phase.</p>
|
<h1 class="font-display text-4xl font-bold">Simple, fair pricing</h1>
|
||||||
</section>
|
<p class="text-gray-400 mt-2">One Pro unlocks both Extension + API.</p>
|
||||||
@endsection
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
|
<section class="glass-card rounded-2xl p-6">
|
||||||
|
<h2 class="text-xl font-semibold">Free</h2>
|
||||||
|
<p class="text-gray-400 text-sm mt-1">For casual usage</p>
|
||||||
|
<p class="text-3xl font-bold mt-4">$0</p>
|
||||||
|
<ul class="mt-4 space-y-2 text-sm text-gray-300">
|
||||||
|
<li>Extension access</li>
|
||||||
|
<li>API testing access</li>
|
||||||
|
<li>Daily cap 30 queries</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="glass-card rounded-2xl p-6 border border-brand-ocean/40">
|
||||||
|
<h2 class="text-xl font-semibold">Pro</h2>
|
||||||
|
<p class="text-gray-400 text-sm mt-1">Most popular</p>
|
||||||
|
<p class="text-3xl font-bold mt-4">$3/mo <span class="text-sm text-gray-400">or $27/yr</span></p>
|
||||||
|
<ul class="mt-4 space-y-2 text-sm text-gray-300">
|
||||||
|
<li>Unlimited extension usage</li>
|
||||||
|
<li>API pro access</li>
|
||||||
|
<li>Up to 3 Chrome profiles</li>
|
||||||
|
</ul>
|
||||||
|
<a href="https://dwindown.gumroad.com/l/dewemoji-pro-subscription" target="_blank" rel="noopener noreferrer" class="mt-5 inline-flex w-full justify-center rounded-lg bg-brand-ocean hover:bg-brand-oceanSoft px-4 py-2 font-semibold">Subscribe on Gumroad</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="glass-card rounded-2xl p-6">
|
||||||
|
<h2 class="text-xl font-semibold">Lifetime</h2>
|
||||||
|
<p class="text-gray-400 text-sm mt-1">Pay once, use forever</p>
|
||||||
|
<p class="text-3xl font-bold mt-4">$69</p>
|
||||||
|
<ul class="mt-4 space-y-2 text-sm text-gray-300">
|
||||||
|
<li>Extension + API access</li>
|
||||||
|
<li>Future updates included</li>
|
||||||
|
<li>IDR/Mayar available on request</li>
|
||||||
|
</ul>
|
||||||
|
<a href="https://dwindown.gumroad.com/l/dewemoji-pro-lifetime" target="_blank" rel="noopener noreferrer" class="mt-5 inline-flex w-full justify-center rounded-lg bg-brand-sun text-black hover:bg-brand-sunSoft px-4 py-2 font-semibold">Buy Lifetime</a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-card rounded-2xl p-6 mt-6 text-sm text-gray-300">
|
||||||
|
<h3 class="font-semibold text-white">Licensing basics</h3>
|
||||||
|
<ul class="mt-3 list-disc pl-5 space-y-1">
|
||||||
|
<li>One key unlocks both extension and API.</li>
|
||||||
|
<li>Use <code>Authorization: Bearer YOUR_LICENSE_KEY</code> for API requests.</li>
|
||||||
|
<li>One license supports up to 3 active Chrome profiles.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@extends('site.layout')
|
@extends('site.layout')
|
||||||
|
|
||||||
@section('title', 'Privacy - Dewemoji')
|
@section('title', 'Privacy - Dewemoji')
|
||||||
|
@section('meta_description', 'Read Dewemoji privacy information and data handling practices.')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<section class="card" style="padding: 20px;">
|
<section class="card" style="padding: 20px;">
|
||||||
@@ -8,4 +9,3 @@
|
|||||||
<p style="color:var(--muted);">Phase 3 placeholder page. We will migrate the full privacy text from legacy content in a later pass.</p>
|
<p style="color:var(--muted);">Phase 3 placeholder page. We will migrate the full privacy text from legacy content in a later pass.</p>
|
||||||
</section>
|
</section>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
|||||||
52
app/resources/views/site/support.blade.php
Normal file
52
app/resources/views/site/support.blade.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
@extends('site.layout')
|
||||||
|
|
||||||
|
@section('title', 'Support - Dewemoji')
|
||||||
|
@section('meta_description', 'Get help with Dewemoji installation, Pro activation, API usage, billing, and troubleshooting.')
|
||||||
|
|
||||||
|
@push('jsonld')
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "FAQPage",
|
||||||
|
"mainEntity": [
|
||||||
|
{"@@type":"Question","name":"How do I activate Pro?","acceptedAnswer":{"@@type":"Answer","text":"Open Dewemoji settings, paste your license key in Pro tab, and activate."}},
|
||||||
|
{"@@type":"Question","name":"How many devices can I use?","acceptedAnswer":{"@@type":"Answer","text":"One license can activate up to 3 Chrome profiles."}},
|
||||||
|
{"@@type":"Question","name":"How do I use my key in API?","acceptedAnswer":{"@@type":"Answer","text":"Send Authorization: Bearer YOUR_LICENSE_KEY header in requests."}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 py-10">
|
||||||
|
<h1 class="font-display text-4xl font-bold text-center">Support</h1>
|
||||||
|
<p class="text-gray-400 text-center mt-2">Need help with Dewemoji? We got you.</p>
|
||||||
|
|
||||||
|
<div class="grid gap-4 mt-8 md:grid-cols-2">
|
||||||
|
<section class="glass-card rounded-2xl p-5">
|
||||||
|
<h2 class="font-semibold">Install & activate</h2>
|
||||||
|
<ol class="mt-3 list-decimal pl-5 text-sm text-gray-300 space-y-1">
|
||||||
|
<li>Install from Chrome Web Store.</li>
|
||||||
|
<li>Open Dewemoji settings.</li>
|
||||||
|
<li>Paste key in Pro tab and activate.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="glass-card rounded-2xl p-5">
|
||||||
|
<h2 class="font-semibold">API quick start</h2>
|
||||||
|
<pre class="mt-3 text-xs overflow-x-auto bg-black/30 rounded-lg p-3"><code>curl -H "Authorization: Bearer YOUR_LICENSE_KEY" \
|
||||||
|
"{{ url('/v1/emojis') }}?q=love&limit=20"</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="glass-card rounded-2xl p-5 md:col-span-2">
|
||||||
|
<h2 class="font-semibold">Common issues</h2>
|
||||||
|
<ul class="mt-3 list-disc pl-5 text-sm text-gray-300 space-y-1">
|
||||||
|
<li><strong>License limit reached:</strong> deactivate old profile first.</li>
|
||||||
|
<li><strong>API 401 invalid key:</strong> ensure key is valid and active.</li>
|
||||||
|
<li><strong>Insert not working:</strong> click into input and retry.</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mt-4 text-sm text-gray-400">Still stuck? Email <a class="text-brand-ocean hover:underline" href="mailto:hello@dewemoji.com">hello@dewemoji.com</a>.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
@extends('site.layout')
|
@extends('site.layout')
|
||||||
|
|
||||||
@section('title', 'Terms - Dewemoji')
|
@section('title', 'Terms - Dewemoji')
|
||||||
|
@section('meta_description', 'Read Dewemoji terms and conditions for website, extension, and API usage.')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<section class="card" style="padding: 20px;">
|
<section class="card" style="padding: 20px;">
|
||||||
@@ -8,4 +9,3 @@
|
|||||||
<p style="color:var(--muted);">Phase 3 placeholder page. We will migrate complete terms content from legacy sources in a later pass.</p>
|
<p style="color:var(--muted);">Phase 3 placeholder page. We will migrate complete terms content from legacy sources in a later pass.</p>
|
||||||
</section>
|
</section>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ use App\Http\Controllers\Web\SiteController;
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', [SiteController::class, 'home'])->name('home');
|
Route::get('/', [SiteController::class, 'home'])->name('home');
|
||||||
|
Route::get('/robots.txt', [SiteController::class, 'robotsTxt'])->name('robots');
|
||||||
|
Route::get('/sitemap.xml', [SiteController::class, 'sitemapXml'])->name('sitemap');
|
||||||
Route::get('/browse', [SiteController::class, 'browse'])->name('browse');
|
Route::get('/browse', [SiteController::class, 'browse'])->name('browse');
|
||||||
Route::get('/api-docs', [SiteController::class, 'apiDocs'])->name('api-docs');
|
Route::get('/api-docs', [SiteController::class, 'apiDocs'])->name('api-docs');
|
||||||
Route::get('/emoji/{slug}', [SiteController::class, 'emojiDetail'])->name('emoji-detail');
|
Route::get('/emoji/{slug}', [SiteController::class, 'emojiDetail'])->name('emoji-detail');
|
||||||
|
|
||||||
Route::get('/pricing', [SiteController::class, 'pricing'])->name('pricing');
|
Route::get('/pricing', [SiteController::class, 'pricing'])->name('pricing');
|
||||||
|
Route::get('/support', [SiteController::class, 'support'])->name('support');
|
||||||
Route::get('/privacy', [SiteController::class, 'privacy'])->name('privacy');
|
Route::get('/privacy', [SiteController::class, 'privacy'])->name('privacy');
|
||||||
Route::get('/terms', [SiteController::class, 'terms'])->name('terms');
|
Route::get('/terms', [SiteController::class, 'terms'])->name('terms');
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class ApiV1EndpointsTest extends TestCase
|
class ApiV1EndpointsTest extends TestCase
|
||||||
@@ -96,6 +97,70 @@ class ApiV1EndpointsTest extends TestCase
|
|||||||
->assertJsonPath('plan', 'pro');
|
->assertJsonPath('plan', 'pro');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_license_verify_uses_gumroad_live_payload_mapping(): void
|
||||||
|
{
|
||||||
|
config()->set('dewemoji.billing.mode', 'live');
|
||||||
|
config()->set('dewemoji.license.accept_all', false);
|
||||||
|
config()->set('dewemoji.license.pro_keys', []);
|
||||||
|
config()->set('dewemoji.billing.providers.gumroad.enabled', true);
|
||||||
|
config()->set('dewemoji.billing.providers.gumroad.verify_url', 'https://api.gumroad.com/v2/licenses/verify');
|
||||||
|
config()->set('dewemoji.billing.providers.gumroad.product_ids', ['prod_123']);
|
||||||
|
config()->set('dewemoji.billing.providers.mayar.enabled', false);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'https://api.gumroad.com/*' => Http::response([
|
||||||
|
'success' => true,
|
||||||
|
'purchase' => [
|
||||||
|
'product_id' => 'prod_123',
|
||||||
|
'recurrence' => 'monthly',
|
||||||
|
],
|
||||||
|
], 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/v1/license/verify', [
|
||||||
|
'key' => 'gum-live-key',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('ok', true)
|
||||||
|
->assertJsonPath('source', 'gumroad')
|
||||||
|
->assertJsonPath('product_id', 'prod_123');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_license_verify_uses_mayar_live_payload_mapping(): void
|
||||||
|
{
|
||||||
|
config()->set('dewemoji.billing.mode', 'live');
|
||||||
|
config()->set('dewemoji.license.accept_all', false);
|
||||||
|
config()->set('dewemoji.license.pro_keys', []);
|
||||||
|
config()->set('dewemoji.billing.providers.gumroad.enabled', false);
|
||||||
|
config()->set('dewemoji.billing.providers.mayar.enabled', true);
|
||||||
|
config()->set('dewemoji.billing.providers.mayar.verify_url', 'https://api.mayar.id/v1/license/verify');
|
||||||
|
config()->set('dewemoji.billing.providers.mayar.api_key', 'secret');
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'https://api.mayar.id/*' => Http::response([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'valid' => true,
|
||||||
|
'product_id' => 'mayar_prod_1',
|
||||||
|
'type' => 'lifetime',
|
||||||
|
'expires_at' => null,
|
||||||
|
],
|
||||||
|
], 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/v1/license/verify', [
|
||||||
|
'key' => 'mayar-live-key',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('ok', true)
|
||||||
|
->assertJsonPath('source', 'mayar')
|
||||||
|
->assertJsonPath('product_id', 'mayar_prod_1');
|
||||||
|
}
|
||||||
|
|
||||||
public function test_emoji_detail_by_slug_endpoint_returns_item(): void
|
public function test_emoji_detail_by_slug_endpoint_returns_item(): void
|
||||||
{
|
{
|
||||||
config()->set('dewemoji.billing.mode', 'sandbox');
|
config()->set('dewemoji.billing.mode', 'sandbox');
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ class SitePagesTest extends TestCase
|
|||||||
$this->get('/animals/animal-mammal')->assertOk();
|
$this->get('/animals/animal-mammal')->assertOk();
|
||||||
$this->get('/api-docs')->assertOk();
|
$this->get('/api-docs')->assertOk();
|
||||||
$this->get('/pricing')->assertOk();
|
$this->get('/pricing')->assertOk();
|
||||||
|
$this->get('/support')->assertOk();
|
||||||
$this->get('/privacy')->assertOk();
|
$this->get('/privacy')->assertOk();
|
||||||
$this->get('/terms')->assertOk();
|
$this->get('/terms')->assertOk();
|
||||||
|
$this->get('/robots.txt')->assertOk();
|
||||||
|
$this->get('/sitemap.xml')->assertOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_emoji_detail_page_works_with_valid_slug(): void
|
public function test_emoji_detail_page_works_with_valid_slug(): void
|
||||||
|
|||||||
121
migration-test-guide.md
Normal file
121
migration-test-guide.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Dewemoji Migration Test Guide (Provider + Frontend)
|
||||||
|
|
||||||
|
Follow this after pulling latest `main`.
|
||||||
|
|
||||||
|
## 1) Prepare local env
|
||||||
|
|
||||||
|
1. Open `app/.env`.
|
||||||
|
2. Set local app URL:
|
||||||
|
- `APP_URL=http://127.0.0.1:8000`
|
||||||
|
3. Start in sandbox first:
|
||||||
|
- `DEWEMOJI_BILLING_MODE=sandbox`
|
||||||
|
- `DEWEMOJI_LICENSE_ACCEPT_ALL=false`
|
||||||
|
4. Keep CORS local during testing:
|
||||||
|
- `DEWEMOJI_ALLOWED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000`
|
||||||
|
|
||||||
|
## 2) Run app
|
||||||
|
|
||||||
|
1. In `app/`, run:
|
||||||
|
- `php artisan migrate`
|
||||||
|
2. Start server:
|
||||||
|
- `php artisan serve --host=127.0.0.1 --port=8000`
|
||||||
|
3. Open `http://127.0.0.1:8000`.
|
||||||
|
|
||||||
|
## 3) Provider parity test (Live mode)
|
||||||
|
|
||||||
|
### A. Gumroad live mapping test
|
||||||
|
|
||||||
|
1. Switch to live mode in `.env`:
|
||||||
|
- `DEWEMOJI_BILLING_MODE=live`
|
||||||
|
- `DEWEMOJI_LICENSE_ACCEPT_ALL=false`
|
||||||
|
- `DEWEMOJI_PRO_KEYS=`
|
||||||
|
- `DEWEMOJI_GUMROAD_ENABLED=true`
|
||||||
|
- `DEWEMOJI_GUMROAD_VERIFY_URL=https://api.gumroad.com/v2/licenses/verify`
|
||||||
|
- `DEWEMOJI_GUMROAD_PRODUCT_IDS=<your_product_id>`
|
||||||
|
2. Restart `php artisan serve`.
|
||||||
|
3. Test verify:
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://127.0.0.1:8000/v1/license/verify" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"key":"<real_gumroad_key>"}'
|
||||||
|
```
|
||||||
|
4. Expect `ok=true`, `source="gumroad"`, tier header `X-Dewemoji-Tier: pro`.
|
||||||
|
|
||||||
|
### B. Mayar live mapping test
|
||||||
|
|
||||||
|
1. In `.env`, configure:
|
||||||
|
- `DEWEMOJI_MAYAR_ENABLED=true`
|
||||||
|
- `DEWEMOJI_MAYAR_VERIFY_URL=<real_mayar_verify_url>`
|
||||||
|
- OR (`DEWEMOJI_MAYAR_API_BASE` + `DEWEMOJI_MAYAR_ENDPOINT_VERIFY`)
|
||||||
|
- `DEWEMOJI_MAYAR_API_KEY=<or secret key>`
|
||||||
|
2. Test verify:
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://127.0.0.1:8000/v1/license/verify" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"key":"<real_mayar_key>"}'
|
||||||
|
```
|
||||||
|
3. Expect `ok=true`, `source="mayar"`.
|
||||||
|
|
||||||
|
### C. Negative check (important)
|
||||||
|
|
||||||
|
1. Verify with invalid key.
|
||||||
|
2. Expect `401` + `error="invalid_license"` + diagnostics in `details.gumroad` / `details.mayar`.
|
||||||
|
|
||||||
|
## 4) Activation lifecycle test
|
||||||
|
|
||||||
|
1. Verify key is valid first.
|
||||||
|
2. Activate device:
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://127.0.0.1:8000/v1/license/activate" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"key":"<valid_key>","email":"you@example.com","product":"chrome","device_id":"chrome-profile-1"}'
|
||||||
|
```
|
||||||
|
3. Expect `ok=true`, `pro=true`.
|
||||||
|
4. Deactivate:
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://127.0.0.1:8000/v1/license/deactivate" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"key":"<valid_key>","product":"chrome","device_id":"chrome-profile-1"}'
|
||||||
|
```
|
||||||
|
5. Expect `ok=true`.
|
||||||
|
|
||||||
|
## 5) Frontend parity test
|
||||||
|
|
||||||
|
Open and verify these URLs:
|
||||||
|
|
||||||
|
1. `/` (discover page)
|
||||||
|
2. `/browse`
|
||||||
|
3. `/<category>` example: `/animals`
|
||||||
|
4. `/<category>/<subcategory>` example: `/animals/animal-mammal`
|
||||||
|
5. `/emoji/grinning-face`
|
||||||
|
6. `/pricing`
|
||||||
|
7. `/api-docs`
|
||||||
|
8. `/support`
|
||||||
|
9. `/privacy`
|
||||||
|
10. `/terms`
|
||||||
|
11. `/robots.txt`
|
||||||
|
12. `/sitemap.xml`
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
- Home search updates URL without full reload.
|
||||||
|
- Category routes prefill filters correctly.
|
||||||
|
- Pricing links point to Gumroad URLs.
|
||||||
|
- Support page loads and shows API/auth guidance.
|
||||||
|
- `robots.txt` contains sitemap URL.
|
||||||
|
- `sitemap.xml` includes core pages and emoji URLs.
|
||||||
|
|
||||||
|
## 6) Automated test pass
|
||||||
|
|
||||||
|
From `app/` run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests pass.
|
||||||
|
|
||||||
|
## 7) Switch back to safe local mode (recommended)
|
||||||
|
|
||||||
|
After live-provider testing, return to:
|
||||||
|
- `DEWEMOJI_BILLING_MODE=sandbox`
|
||||||
|
- disable provider flags unless actively testing them.
|
||||||
@@ -57,9 +57,10 @@
|
|||||||
|
|
||||||
### Phase 3 - Website parity from `dewemoji-live`
|
### Phase 3 - Website parity from `dewemoji-live`
|
||||||
- [x] Core pages exist in rebuild.
|
- [x] Core pages exist in rebuild.
|
||||||
- [ ] Add missing pages/routes: `/support`, `/browse`, pretty category routes (`/{category}` and `/{category}/{subcategory}`).
|
- [x] Add missing pages/routes: `/support`, `/browse`, pretty category routes (`/{category}` and `/{category}/{subcategory}`).
|
||||||
- [x] `/browse`, `/{category}`, `/{category}/{subcategory}` implemented in rebuild.
|
- [x] `/browse`, `/{category}`, `/{category}/{subcategory}` implemented in rebuild.
|
||||||
- [ ] Keep URL behavior parity (canonical no-trailing-slash pages, redirect rules, pretty-to-query hydration).
|
- [x] `/support` implemented in rebuild.
|
||||||
|
- [x] Keep URL behavior parity (canonical no-trailing-slash pages, redirect rules, pretty-to-query hydration).
|
||||||
- [x] no-trailing-slash redirect middleware and canonical link baseline implemented.
|
- [x] no-trailing-slash redirect middleware and canonical link baseline implemented.
|
||||||
- [x] pretty route hydration wired into homepage initial filters + URL sync.
|
- [x] pretty route hydration wired into homepage initial filters + URL sync.
|
||||||
- [ ] Port homepage behavior parity:
|
- [ ] Port homepage behavior parity:
|
||||||
@@ -95,14 +96,14 @@
|
|||||||
|
|
||||||
### Phase 5 - SEO parity (must not disrupt GSC)
|
### Phase 5 - SEO parity (must not disrupt GSC)
|
||||||
- [ ] Preserve canonical strategy for all pages (including emoji detail + pretty category pages).
|
- [ ] Preserve canonical strategy for all pages (including emoji detail + pretty category pages).
|
||||||
- [ ] Add/verify meta + social tags parity: title/description/OG/Twitter + theme color.
|
- [x] Add/verify meta + social tags parity baseline: title/description/OG/Twitter + theme color.
|
||||||
- [ ] Port JSON-LD strategy:
|
- [ ] Port JSON-LD strategy:
|
||||||
- Global `WebSite` + `SearchAction` + Organization.
|
- Global `WebSite` + `SearchAction` + Organization.
|
||||||
- `TechArticle` on `/api-docs`.
|
- [x] `TechArticle` baseline on `/api-docs`.
|
||||||
- `Product` + `FAQPage` on `/pricing`.
|
- [x] `Product` baseline on `/pricing`.
|
||||||
- `FAQPage` on `/support`.
|
- [x] `FAQPage` baseline on `/support`.
|
||||||
- `CreativeWork` + `BreadcrumbList` on emoji pages.
|
- [x] `CreativeWork` + `BreadcrumbList` baseline on emoji pages.
|
||||||
- [ ] Implement `robots.txt` parity and dynamic `sitemap.xml` parity.
|
- [x] Implement `robots.txt` parity and dynamic `sitemap.xml` baseline.
|
||||||
- [ ] Ensure sitemap excludes policy-hidden emoji URLs (same filter policy as live).
|
- [ ] Ensure sitemap excludes policy-hidden emoji URLs (same filter policy as live).
|
||||||
- [ ] Keep core indexed URLs stable: `/`, `/pricing`, `/api-docs`, `/support`, `/privacy`, `/terms`, `/emoji/{slug}`.
|
- [ ] Keep core indexed URLs stable: `/`, `/pricing`, `/api-docs`, `/support`, `/privacy`, `/terms`, `/emoji/{slug}`.
|
||||||
|
|
||||||
@@ -173,6 +174,11 @@
|
|||||||
- pretty category routes (`/{category}`, `/{category}/{subcategory}`)
|
- pretty category routes (`/{category}`, `/{category}/{subcategory}`)
|
||||||
- trailing slash -> canonical path redirect (301)
|
- trailing slash -> canonical path redirect (301)
|
||||||
- canonical `<link>` output from layout
|
- canonical `<link>` output from layout
|
||||||
|
- Added SEO assets baseline in rebuild:
|
||||||
|
- `/robots.txt` route
|
||||||
|
- `/sitemap.xml` route generated from dataset
|
||||||
|
- meta/OG/Twitter fields in shared layout
|
||||||
|
- JSON-LD blocks on `/pricing`, `/support`, `/api-docs`, and emoji detail page
|
||||||
|
|
||||||
## Live audit highlights (reference)
|
## Live audit highlights (reference)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user