diff --git a/README.md b/README.md
index 411060e..53f4e0f 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# OpnForm
-
+
diff --git a/app/Http/Controllers/SpaController.php b/app/Http/Controllers/SpaController.php
index a315582..eb4ce03 100644
--- a/app/Http/Controllers/SpaController.php
+++ b/app/Http/Controllers/SpaController.php
@@ -2,6 +2,8 @@
namespace App\Http\Controllers;
+use App\Service\SeoMetaResolver;
+use Illuminate\Http\Request;
class SpaController extends Controller
{
/**
@@ -9,8 +11,10 @@ class SpaController extends Controller
*
* @return \Illuminate\Http\Response
*/
- public function __invoke()
+ public function __invoke(Request $request)
{
- return view('spa');
+ return view('spa',[
+ 'meta' => (new SeoMetaResolver($request))->getMetas(),
+ ]);
}
}
diff --git a/app/Service/SeoMetaResolver.php b/app/Service/SeoMetaResolver.php
new file mode 100644
index 0000000..a49ff4d
--- /dev/null
+++ b/app/Service/SeoMetaResolver.php
@@ -0,0 +1,178 @@
+ '/',
+ 'form_show' => '/forms/{slug}',
+ 'login' => '/login',
+ 'register' => '/register',
+ 'reset_password' => '/password/reset',
+ 'privacy_policy' => '/privacy-policy',
+ 'terms_conditions' => '/terms-conditions',
+ 'integrations' => '/integrations',
+ 'templates' => '/templates',
+ 'template_show' => '/templates/{slug}',
+ ];
+
+ /**
+ * Metas for simple route (without needing to access DB)
+ */
+ const PATTERN_STATIC_META = [
+ 'login' => [
+ 'title' => 'Login',
+ ],
+ 'register' => [
+ 'title' => 'Create your account',
+ ],
+ 'reset_password' => [
+ 'title' => 'Reset your password',
+ ],
+ 'privacy_policy' => [
+ 'title' => 'Our Privacy Policy',
+ ],
+ 'terms_conditions' => [
+ 'title' => 'Our Terms & Conditions',
+ ],
+ 'integrations' => [
+ 'title' => 'Our Integrations',
+ ],
+ 'templates' => [
+ 'title' => 'Templates',
+ 'description' => 'Free templates to quickly create beautiful forms for free!'
+ ],
+ ];
+
+ const META_CACHE_DURATION = 60 * 60 * 12; // 12 hours
+
+ const META_CACHE_KEY_PREFIX = 'seo_meta_';
+
+ public function __construct(private Request $request)
+ {
+ }
+
+ /**
+ * Returns the right metas for a given route, caches meta for 1 hour.
+ *
+ * @return array
+ */
+ public function getMetas(): array
+ {
+ $cacheKey = self::META_CACHE_KEY_PREFIX . urlencode($this->request->path());
+
+ return Cache::remember($cacheKey, now()->addSeconds(self::META_CACHE_DURATION), function () {
+ $pattern = $this->resolvePattern();
+
+ if ($this->hasPatternMetaGetter($pattern)) {
+ // Custom function for pattern
+ try {
+ return array_merge($this->getDefaultMeta(), $this->{'get' . Str::studly($pattern) . 'Meta'}());
+ } catch (\Exception $e) {
+ return $this->getDefaultMeta();
+ }
+ } elseif (in_array($pattern, array_keys(self::PATTERN_STATIC_META))) {
+ // Simple meta for pattern
+ $meta = self::PATTERN_STATIC_META[$pattern];
+ if (isset($meta['title'])) {
+ $meta['title'] .= $this->titleSuffix();
+ }
+ if (isset($meta['image'])) {
+ $meta['image'] = asset($meta['image']);
+ }
+
+ return array_merge($this->getDefaultMeta(), $meta);
+ }
+
+ return $this->getDefaultMeta();
+ });
+ }
+
+ /**
+ * Simulates the Laravel router to match route with Metas
+ *
+ * @return string
+ */
+ private function resolvePattern()
+ {
+ foreach (self::URL_PATTERNS as $patternName => $patternData) {
+ $path = rtrim($this->request->getPathInfo(), '/') ?: '/';
+
+ $route = (new Route('GET', $patternData, fn() => ''))->bind($this->request);
+ if (preg_match($route->getCompiled()->getRegex(), rawurldecode($path))) {
+ $this->patternData = $route->parameters();
+
+ return $patternName;
+ }
+ }
+
+ return 'default';
+ }
+
+ /**
+ * Determine if a get mutator exists for a pattern.
+ *
+ * @param string $key
+ * @return bool
+ */
+ private function hasPatternMetaGetter($key)
+ {
+ return method_exists($this, 'get' . Str::studly($key) . 'Meta');
+ }
+
+ private function titleSuffix()
+ {
+ return ' ยท ' . config('app.name');
+ }
+
+ private function getDefaultMeta(): array
+ {
+ return [
+ 'title' => 'Create beautiful forms for free' . $this->titleSuffix(),
+ 'description' => "Create beautiful forms for free. Unlimited fields, unlimited submissions. It's free and it takes less than 1 minute to create your first form.",
+ 'image' => asset('/img/social-preview.jpg'),
+ ];
+ }
+
+ private function getFormShowMeta(): array
+ {
+ $form = Form::whereSlug($this->patternData['slug'])->firstOrFail();
+
+ return [
+ 'title' => $form->title . $this->titleSuffix(),
+ ];
+ }
+
+ private function getTemplateShowMeta(): array
+ {
+ $template = Template::whereSlug($this->patternData['slug'])->firstOrFail();
+
+ return [
+ 'title' => $template->name . $this->titleSuffix(),
+ 'description' => Str::of($template->description)->limit(160) ,
+ 'image' => $template->image_url
+ ];
+ }
+}
diff --git a/public/img/social-preview.jpg b/public/img/social-preview.jpg
new file mode 100644
index 0000000..22ed688
Binary files /dev/null and b/public/img/social-preview.jpg differ
diff --git a/public/img/social-preview.png b/public/img/social-preview.png
deleted file mode 100644
index d35c1cb..0000000
Binary files a/public/img/social-preview.png and /dev/null differ
diff --git a/resources/js/components/App.vue b/resources/js/components/App.vue
index ce41c96..6f063c3 100644
--- a/resources/js/components/App.vue
+++ b/resources/js/components/App.vue
@@ -94,10 +94,10 @@ export default {
{ vmid: 'description', name: 'description', content: description },
{ vmid: 'og:title', property: 'og:title', content: appName },
{ vmid: 'og:description', property: 'og:description', content: description },
- { vmid: 'og:image', property: 'og:image', content: '/img/social-preview.png' },
+ { vmid: 'og:image', property: 'og:image', content: this.asset('img/social-preview.jpg') },
{ vmid: 'twitter:title', property: 'twitter:title', content: appName },
{ vmid: 'twitter:description', property: 'twitter:description', content: description },
- { vmid: 'twitter:image', property: 'twitter:image', content: '/img/social-preview.png' },
+ { vmid: 'twitter:image', property: 'twitter:image', content: this.asset('img/social-preview.jpg') },
{ vmid: 'twitter:card', property: 'twitter:card', content: 'summary_large_image' }
]
}
diff --git a/resources/js/pages/templates/show.vue b/resources/js/pages/templates/show.vue
index 3284657..a39a72b 100644
--- a/resources/js/pages/templates/show.vue
+++ b/resources/js/pages/templates/show.vue
@@ -73,11 +73,6 @@ export default {
next()
},
- props: {
- metaTitle: {type: String, default: 'Templates'},
- metaDescription: {type: String, default: 'Public templates for create form quickly!'}
- },
-
data() {
return {}
},
@@ -105,7 +100,10 @@ export default {
},
form() {
return new Form(this.template.structure)
- }
+ },
+ metaTitle () {
+ return this.template ? this.template.name : 'Template'
+ },
}
}
diff --git a/resources/views/spa.blade.php b/resources/views/spa.blade.php
index 31711ce..9d24e1f 100644
--- a/resources/views/spa.blade.php
+++ b/resources/views/spa.blade.php
@@ -21,11 +21,23 @@
- {{ config('app.name') }}
-
+ @if($meta)
+ {{$meta['title']}}
+
+
+
+
+
+
+
+
+
+
+ @endif
+