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 @@ -