diff --git a/README.md b/README.md index 411060e..53f4e0f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # OpnForm

- +

Build Status 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/Http/Controllers/TemplateController.php b/app/Http/Controllers/TemplateController.php index 92a28c3..9ac45d0 100644 --- a/app/Http/Controllers/TemplateController.php +++ b/app/Http/Controllers/TemplateController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Http\Requests\Templates\CreateTemplateRequest; +use App\Http\Resources\TemplateResource; use Illuminate\Http\Request; use App\Models\Template; @@ -11,8 +12,7 @@ class TemplateController extends Controller { public function index() { - // TODO: create resource - return Template::all(); + return TemplateResource::collection(Template::all()); } public function create(CreateTemplateRequest $request) diff --git a/app/Http/Requests/Templates/CreateTemplateRequest.php b/app/Http/Requests/Templates/CreateTemplateRequest.php index 43e2acc..f5123c3 100644 --- a/app/Http/Requests/Templates/CreateTemplateRequest.php +++ b/app/Http/Requests/Templates/CreateTemplateRequest.php @@ -7,6 +7,40 @@ use Illuminate\Foundation\Http\FormRequest; class CreateTemplateRequest extends FormRequest { + const IGNORED_KEYS = [ + 'id', + 'creator', + 'cleanings', + 'closes_at', + 'deleted_at', + 'updated_at', + 'form_pending_submission_key', + 'is_closed', + 'is_pro', + 'is_password_protected', + 'last_edited_human', + 'max_number_of_submissions_reached', + 'notifies', + 'notification_body', + 'notification_emails', + 'notification_sender', + 'notification_subject', + 'notifications_include_submission', + 'notifies_slack', + 'slack_webhook_url', + 'removed_properties', + 'creator_id', + 'extra', + 'workspace', + 'workspace_id', + 'submissions', + 'submissions_count', + 'views', + 'views_count', + 'visibility', + 'webhook_url', + ]; + /** * Get the validation rules that apply to the request. * @@ -24,13 +58,12 @@ class CreateTemplateRequest extends FormRequest ]; } - public function getTemplate() : Template + public function getTemplate(): Template { $structure = $this->form; - $ignoreKeys = ['id','creator','creator_id','created_at','updated_at','extra','workspace','submissions','submissions_count','views','views_count']; - foreach($structure as $key=>$val){ - if(in_array($key, $ignoreKeys)){ - $structure[$key] = null; + foreach ($structure as $key => $val) { + if (in_array($key, self::IGNORED_KEYS)) { + unset($structure[$key]); } } return new Template([ diff --git a/app/Http/Resources/TemplateResource.php b/app/Http/Resources/TemplateResource.php new file mode 100644 index 0000000..dd11577 --- /dev/null +++ b/app/Http/Resources/TemplateResource.php @@ -0,0 +1,24 @@ + '/', + '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/package-lock.json b/package-lock.json index 5000cb0..8e76d9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "@babel/eslint-parser": "^7.15.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/preset-env": "^7.15.0", + "@tailwindcss/aspect-ratio": "^0.4.2", "autoprefixer": "^10.4.12", "cross-env": "^7.0.3", "eslint": "^7.32.0", @@ -2048,6 +2049,15 @@ "node": ">=4" } }, + "node_modules/@tailwindcss/aspect-ratio": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz", + "integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==", + "dev": true, + "peerDependencies": { + "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -18695,6 +18705,13 @@ "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", "dev": true }, + "@tailwindcss/aspect-ratio": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz", + "integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==", + "dev": true, + "requires": {} + }, "@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", diff --git a/package.json b/package.json index ee2cefd..e0c9849 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "autoprefixer": "^10.4.12", "cross-env": "^7.0.3", "eslint": "^7.32.0", + "@tailwindcss/aspect-ratio": "^0.4.2", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.24.0", "eslint-plugin-node": "^11.1.0", 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/auth/components/QuickRegister.vue b/resources/js/pages/auth/components/QuickRegister.vue index dbc3bad..48b751a 100644 --- a/resources/js/pages/auth/components/QuickRegister.vue +++ b/resources/js/pages/auth/components/QuickRegister.vue @@ -3,10 +3,8 @@