From 21494b668ff353d1752ce6f42b4eabc005e6ec92 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Mon, 10 Apr 2023 15:15:21 +0300 Subject: [PATCH 01/24] readme installation line, installer-name seo added --- README.md | 11 +++++++++++ composer.json | 7 +++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 38391c7..f4c3b77 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,16 @@ # SEO Plugin +### Installation +#### Before installation add this to your `composer.json` so plugin will act like original one +```json +"extra": { + "installer-paths": { + "./plugins/winter/{$name}/": ["8black/wn-seo-plugin", "8black/seo"] + } +} +``` +#### Run `composer require 8black/wn-seo-plugin` + WIP plugin for managing SEO content in Winter CMS. Inspired by https://github.com/bennothommo/wn-meta-plugin. Future plans including support for easily generating structured data and automatically attaching SEO meta fields to CMS pages, Winter.Pages pages, & generically to any Winter CMS model. diff --git a/composer.json b/composer.json index b702d51..9c5bd9b 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { - "name": "winter/wn-seo-plugin", + "name": "8black/wn-seo-plugin", "type": "winter-plugin", "description": "Winter CMS plugin for managing SEO tags", - "homepage": "https://github.com/wintercms/wn-seo-plugin", + "homepage": "https://github.com/8blvck/wn-seo-plugin", "keywords": ["cms", "wintercms", "seo"], "license": "MIT", "authors": [ @@ -24,5 +24,8 @@ "require": { "php": ">=7.2", "composer/installers": "~1.11" + }, + "extra": { + "installer-name": "seo" } } From 1a93298993bfa6c076f5c2728ad955fb6998efe4 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Mon, 10 Apr 2023 15:19:21 +0300 Subject: [PATCH 02/24] + seo global settings, + favicon, + .txt enable/disable, and bit more --- Plugin.php | 60 ---------------------- assets/counter.js | 62 ++++++++++++++++++++++ components/SEOTags.php | 88 +++++++++++++++++++++++-------- components/seotags/default.htm | 8 ++- models/settings/fields.yaml | 94 ++++++++++++++++++++++++++++++++++ routes.php | 46 ++++++++++++++--- 6 files changed, 267 insertions(+), 91 deletions(-) create mode 100644 assets/counter.js diff --git a/Plugin.php b/Plugin.php index 80c6c61..10226ce 100644 --- a/Plugin.php +++ b/Plugin.php @@ -8,66 +8,6 @@ use Winter\SEO\Models\Settings; use Yaml; -/** - * SEO Plugin Information File - * @TODO: - * - Support for robots.txt, humans.txt, and security.txt - * - humans.txt prepopulate from project composer.json? - * - Twig parsing in .txt file definitions - * - When adding support for twig parsing also add heavy caching by default for parsed results - * - Support for Model Behavior that creates meta data in separate DB - * - check for extended with in the form event listener - * - Support templated strings for meta data managed by plugin (i.e. {{ record.name }} - {{ record.category }}) - * - Support for Winter.Translate - * - Automatic extension of Winter.Blog - * - Recommended plugins: Winter.Sitemap, Winter.Pages, Winter.Blog, Winter.Redirect - * - Documentation - * - All settings should be backed / default to config values for file based configuration - * - Title prefix / suffix; includeTitle default true in component - * - Comments / help text for all provided fields ideally with references on how they should be used - * - Rename "Meta" tab to "SEO" tab - * - Character limit on description field - * - Full support for common robots settings via meta tags and site wide defaults - * - Default addition of generator meta tag in config file - * - Support for "Social Media" quick connects (FB / Twitter IDs / accounts) (maybe just via global tags) - * - Support for more sources of og:image (i.e. manual URL, fileupload, mediafinder, etc) - * - Favicon, webmanifest, browserconfig, apple touch icons, etc - * - Previews of FB / Twitter / Google - * - Support for structured data - * - Winter.Redirect - * - Model behavior to setup automatic redirects when URL attribute changes - * - Support for Winter.Builder pages - * - Support for twig parsing of meta tag values? - * - og:locale / other relevant locale flags, perhaps provide from Winter.Translate plugin? Or just from this plugin - * - Contextual settings for adding records to the sitemap rather than having to add everything in the sitemap main section? Perhaps implement in Winter.Sitemap - * - * @Related: - * - https://github.com/wintercms/wn-redirect-plugin - * - https://github.com/wintercms/wn-sitemap-plugin - * - https://github.com/wintercms/wn-pages-plugin - * - https://github.com/wintercms/wn-blog-plugin - * - https://github.com/wintercms/wn-translate-plugin - * - * @See also: - * - https://github.com/bennothommo/wn-meta-plugin - * - https://github.com/mjauvin/wn-mlsitemap-plugin - * - https://github.com/josephcrowell/wn-sitemappretty-plugin - * - https://github.com/studiobosco/wn-seo-extension - * - https://github.com/WebVPF/wn-robots-plugin - * - https://octobercms.com/plugin/renatio-seomanager - * - https://octobercms.com/plugin/lovata-mightyseo - * - https://octobercms.com/plugin/initbiz-seostorm - * - https://octobercms.com/plugin/zen-robots - * - https://octobercms.com/plugin/utopigs-seo - * - https://octobercms.com/plugin/mohsin-txt - * - https://octobercms.com/plugin/linkonoid-schemamarkup - * - https://octobercms.com/plugin/vdlp-schemaorg - * - https://octobercms.com/plugin/egalhtn-localbusiness - * - https://octobercms.com/plugin/dynamedia-posts - * - https://octobercms.com/plugin/eugene3993-seo - * - https://octobercms.com/plugin/magiczne-seotweaker - * - https://octobercms.com/plugin/eugene3993-seolight - */ class Plugin extends PluginBase { /** diff --git a/assets/counter.js b/assets/counter.js new file mode 100644 index 0000000..e0f0f66 --- /dev/null +++ b/assets/counter.js @@ -0,0 +1,62 @@ +(function (win, doc) { + counter = win.counter || {} + counter.seo = { + charCountHandler: function(target) { + $target = $(target); + let $helpBlock = $target.next('.help-block'); + let isTwig = /\{{2}.*\}{2}/.test($target.val()); + let count = isTwig ? 'unrechable' : $target.val().length; + let min = $target.data('min'); + let max = $target.data('max'); + $helpBlock.html(`Symbols: ${count} of ${min} - ${max} optimal`); + let $number = $helpBlock.find('b'); + if (count < max && count > min) { + $number.css({color: 'lime'}); + } else if (isTwig) { + $number.css({color: 'coral'}); + } else { + $number.css({color: 'salmon'}); + } + } + } + var listeners = [], + doc = win.document, + MutationObserver = win.MutationObserver || win.WebKitMutationObserver, + observer; + + function ready(selector, fn) { + listeners.push({ + selector: selector, + fn: fn + }); + if (!observer) { + observer = new MutationObserver(check); + observer.observe(doc.documentElement, { + childList: true, + subtree: true + }); + } + check(); + } + + function check() { + for (var i = 0, len = listeners.length, listener, elements; i < len; i++) { + listener = listeners[i]; + elements = doc.querySelectorAll(listener.selector); + for (var j = 0, jLen = elements.length, element; j < jLen; j++) { + element = elements[j]; + if (!element.ready) { + element.ready = true; + listener.fn.call(element, element); + } + } + } + } + win.ready = ready; + win.counter = counter; + win.ready('[data-counter]', (el) => { + counter.seo.charCountHandler(el) + el.oninput = event => counter.seo.charCountHandler(el); + }); +})(window, document); + \ No newline at end of file diff --git a/components/SEOTags.php b/components/SEOTags.php index ef2c5bb..abc403a 100644 --- a/components/SEOTags.php +++ b/components/SEOTags.php @@ -12,6 +12,7 @@ use System\Classes\MediaLibrary; use Winter\SEO\Classes\Meta; use Winter\SEO\Classes\Link; +use Winter\SEO\Models\Settings; class SEOTags extends ComponentBase { @@ -21,8 +22,8 @@ class SEOTags extends ComponentBase public function componentDetails() { return [ - 'name' => 'SEOTags Component', - 'description' => 'No description provided yet...' + 'name' => 'SEOTags', + 'description' => 'Outputs specified tags to the page' ]; } @@ -39,8 +40,42 @@ public function defineProperties() */ protected function processPageMeta() { - // $this['page_title'] = $this->page->title ?? Meta::get('og:title') ?? ''; - // $this['app_name'] = BrandSetting::get('app_name'); + + // Store page settings in order to substitute with model settings if needed + $page = (object) $this->page->settings; + + // Handle global settings + if(Settings::get('global_enable') == 1) { + $name = Settings::get('global_app_name'); + $position = Settings::get('global_app_name_pos'); + $separator = Settings::get('global_separator'); + + // Substitute empty title by global setting + if(empty(trim($page->meta_title))) { + $page->meta_title = Settings::get('global_app_title'); + } + + // Substitute empty description by global setting + if(empty(trim($page->meta_description))) { + $page->meta_description = Settings::get('global_app_description'); + } + + if(empty($name)) { + // Skip or do something about that? + } else if($position === 'prefix') { + $page->meta_title = "{$name} {$separator} {$page->meta_title}"; + } elseif($position === 'suffix') { + $page->meta_title = "{$page->meta_title} {$separator} {$name}"; + } + } + + // Set the page title + // No need to check empty since `tag missing or empty` has same effects for audit + Meta::set('title', $page->meta_title); + + // Set the page descriptioe + // No need to check empty since `tag missing or empty` has same effects for audit + Meta::set('description', $page->meta_description); // Set the cannonical URL if (empty(Link::get('canonical'))) { @@ -48,12 +83,12 @@ protected function processPageMeta() } // Parse the meta_image as a media library image - if (!empty($this->page->meta_image)) { - $this->page->meta_image = MediaLibrary::url($this->page->meta_image); + if (!empty($page->meta_image)) { + $page->meta_image = MediaLibrary::url($page->meta_image); } // Handle the nofollow meta property being set - if (!empty($this->page->meta_nofollow)) { + if (!empty($page->meta_nofollow)) { Link::set('robots', 'nofollow'); } @@ -69,13 +104,11 @@ protected function processPageMeta() 'next' => 'paginateNext', ], ]; + foreach ($metaMap as $class => $map) { foreach ($map as $name => $pageProp) { - if ( - empty($class::get($name)) - && !empty($this->page->{$pageProp}) - ) { - $class::set($name, $this->page->{$pageProp}); + if (empty($class::get($name)) && !empty($page->{$pageProp})) { + $class::set($name, $page->{$pageProp}); } } } @@ -138,14 +171,12 @@ protected function processOgImage(): void } /** - * Processes the og:description / description meta tags + * Processes the og:description meta tags */ - protected function processDescription(): void + protected function processOgDescription(): void { if (!empty(Meta::get('description')) && empty(Meta::get('og:description'))) { Meta::set('og:description', Meta::get('description')); - } elseif (!empty(Meta::get('og:description')) && empty(Meta::get('description'))) { - Meta::set('description', Meta::get('og:description')); } } @@ -179,11 +210,24 @@ protected function processOgSiteName(): void } } + /** + * Processes the icon link tag if favicon enabled + */ + protected function processFavicon(): void + { + if(Settings::get('enable_favicon') == 1) { + Link::set('icon', '/favicon.ico'); + } + } + public function getMetaTags(): array { + // TODO + // Settings::global_minify_html + $this->processFavicon(); $this->processPageMeta(); $this->processOgImage(); - $this->processDescription(); + $this->processOgDescription(); $this->processOgUrl(); $this->processOgType(); $this->processOgSiteName(); @@ -196,13 +240,13 @@ public function getLinkTags(): array return Link::all(); } - // dd(Meta::all(), Link::all(), __LINE__, __FILE__); +// dd(Meta::all(), Link::all(), __LINE__, __FILE__); - // Meta::set('og:title', $meta['title']); - // Meta::set('og:description', $meta['description']); - // Meta::set('og:image', \System\Classes\MediaLibrary::url($meta['image'])); - // Link::set('canonical', $meta['canonical_url']); +// Meta::set('og:title', $meta['title']); +// Meta::set('og:description', $meta['description']); +// Meta::set('og:image', \System\Classes\MediaLibrary::url($meta['image'])); +// Link::set('canonical', $meta['canonical_url']); // {# Pagination Links #} diff --git a/components/seotags/default.htm b/components/seotags/default.htm index e73b5d1..4c087a9 100644 --- a/components/seotags/default.htm +++ b/components/seotags/default.htm @@ -1,9 +1,13 @@ {# tags #} {% for tagName, tagContent in __SELF__.getMetaTags() %} - + {% if tagName == 'title' %} + {{ tagContent }} + {% else %} + + {% endif %} {% endfor %} {# tags #} {% for tagName, tagContent in __SELF__.getLinkTags() %} - + {% endfor %} diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index eab78e5..7cf51b5 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -4,12 +4,76 @@ tabs: icons: + 'Global': icon-globe + 'Favicon': icon-image winter.seo::lang.models.meta.label_plural: icon-tag winter.seo::lang.models.link.label_plural: icon-link winter.seo::lang.models.settings.humans_txt: icon-user-group winter.seo::lang.models.settings.robots_txt: icon-robot winter.seo::lang.models.settings.security_txt: icon-lock fields: + global_enable: + tab: 'Global' + label: 'Enable global meta tags (not overrides page settings)' + span: auto + type: switch + default: 1 + global_minify_html: + tab: 'Global' + label: 'Minify HTML automatically' + span: auto + type: switch + default: 0 + global_app_name: + tab: 'Global' + label: 'App name' + span: auto + placeholder: '' + type: text + default: '' + comment: 'App name can be visible in the title meta tag' + global_separator: + tab: 'Global' + label: 'App name separator' + span: auto + placeholder: '' + type: text + default: '|' + comment: 'Sybmol to separate title from app name: {app name} {sepratator} {title}' + global_app_name_pos: + tab: 'Global' + label: 'Select how the app name should appear in the title' + options: + hide: 'Hide' + prefix: 'Prefix' + suffix: 'Suffix' + span: auto + type: balloon-selector + global_app_title: + tab: 'Global' + label: 'App title' + size: tiny + span: full + placeholder: 'Your app global title' + type: text + comment: '[data-counter]' + attributes: + data-counter: 1 + data-min: 50 + data-max: 60 + global_app_description: + tab: 'Global' + label: 'App description' + size: tiny + span: full + placeholder: 'Your app global description' + type: textarea + comment: '[data-counter]' + attributes: + data-counter: 1 + data-min: 100 + data-max: 160 + meta_tags: tab: winter.seo::lang.models.meta.label_plural commentAbove: winter.seo::lang.models.meta.comment @@ -50,16 +114,46 @@ tabs: type: textarea size: tiny label: winter.seo::lang.models.link.description + enable_favicon: + tab: 'Favicon' + label: 'Enable icons' + span: full + type: switch + default: 1 + favicon: + tab: 'Favicon' + commentAbove: 'Global favicon' + type: mediafinder + mode: image + span: left + enable_humans_txt: + tab: winter.seo::lang.models.settings.humans_txt + label: 'Enable humans.txt' + span: full + type: switch + default: 1 humans_txt: tab: winter.seo::lang.models.settings.humans_txt commentAbove: winter.seo::lang.models.settings.humans_txt_comment type: textarea size: small + enable_robots_txt: + tab: winter.seo::lang.models.settings.robots_txt + label: 'Enable robots.txt' + span: full + type: switch + default: 1 robots_txt: tab: winter.seo::lang.models.settings.robots_txt commentAbove: winter.seo::lang.models.settings.robots_txt_comment type: textarea size: small + enable_security_txt: + tab: winter.seo::lang.models.settings.security_txt + label: 'Enable security.txt' + span: full + type: switch + default: 1 security_txt: tab: winter.seo::lang.models.settings.security_txt commentAbove: winter.seo::lang.models.settings.security_txt_comment diff --git a/routes.php b/routes.php index 4e69f34..d8efad2 100644 --- a/routes.php +++ b/routes.php @@ -1,20 +1,52 @@ addJs('/plugins/winter/seo/assets/counter.js'); +}); Event::listen('system.beforeRoute', function () { + $notFoundError = function () { + return Response::make((new \Cms\Classes\Controller())->run('404'), 404); + }; + $txtResponse = function ($key) { $contents = Settings::get($key); - if (empty($contents)) { - return Response::make((new \Cms\Classes\Controller())->run('404'), 404); + return $notFoundError(); } - return Response::make($contents, 200, ['Content-Type' => 'text/plain']); }; - Route::get('/humans.txt', fn() => $txtResponse('humans_txt')); - Route::get('/robots.txt', fn() => $txtResponse('robots_txt')); - Route::get('/security.txt', fn() => $txtResponse('security_txt')); - Route::get('/.well-known/security.txt', fn() => $txtResponse('security_txt')); + if(Settings::get('enable_favicon') == 1) { + Route::get('favicon.ico', function() use ($notFoundError) { + $favicon = Settings::get('favicon'); + $faviconPath = base_path().media_path($favicon); + $outputPath = base_path().media_path('favicon/' . basename($favicon)); + if(!strlen($favicon) || !file_exists($faviconPath)) { + return $notFoundError(); + } + try { + if (!file_exists($outputPath)) { + Resizer::open($faviconPath)->resize(16, 16)->save($outputPath); + } + } catch(Exception $e) { + $outputPath = $faviconPath; + } + return response()->file($outputPath, [ 'Content-Type'=> 'image/x-icon' ]); + }); + } + if(Settings::get('enable_humans_txt') == 1) { + Route::get('/humans.txt', fn() => $txtResponse('humans_txt')); + } + if(Settings::get('enable_robots_txt') == 1) { + Route::get('/robots.txt', fn() => $txtResponse('robots_txt')); + } + if(Settings::get('enable_security_txt') == 1) { + Route::get('/security.txt', fn() => $txtResponse('security_txt')); + Route::get('/.well-known/security.txt', fn() => $txtResponse('security_txt')); + } }); From fbaa2b123586fb53a79a35c47d74e7cfb37f9032 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Mon, 10 Apr 2023 16:14:02 +0300 Subject: [PATCH 03/24] Meta tags now sets only if empty --- components/SEOTags.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/components/SEOTags.php b/components/SEOTags.php index abc403a..60cfa17 100644 --- a/components/SEOTags.php +++ b/components/SEOTags.php @@ -70,12 +70,14 @@ protected function processPageMeta() } // Set the page title - // No need to check empty since `tag missing or empty` has same effects for audit - Meta::set('title', $page->meta_title); + if(empty(trim(Meta::get('title')))) { + Meta::set('title', $page->meta_title); + } // Set the page descriptioe - // No need to check empty since `tag missing or empty` has same effects for audit - Meta::set('description', $page->meta_description); + if(empty(trim(Meta::get('description')))) { + Meta::set('description', $page->meta_description); + } // Set the cannonical URL if (empty(Link::get('canonical'))) { From 50dae0b06809763349b7cd8b79622f51137c9e08 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Mon, 10 Apr 2023 19:13:17 +0300 Subject: [PATCH 04/24] + support of external model metadata by metadata_from public property --- Plugin.php | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 6 deletions(-) diff --git a/Plugin.php b/Plugin.php index 10226ce..7ef8a33 100644 --- a/Plugin.php +++ b/Plugin.php @@ -8,6 +8,66 @@ use Winter\SEO\Models\Settings; use Yaml; +/** + * SEO Plugin Information File + * @TODO: + * - Support for robots.txt, humans.txt, and security.txt + * - humans.txt prepopulate from project composer.json? + * - Twig parsing in .txt file definitions + * - When adding support for twig parsing also add heavy caching by default for parsed results + * - Support for Model Behavior that creates meta data in separate DB + * - check for extended with in the form event listener + * - Support templated strings for meta data managed by plugin (i.e. {{ record.name }} - {{ record.category }}) + * - Support for Winter.Translate + * - Automatic extension of Winter.Blog + * - Recommended plugins: Winter.Sitemap, Winter.Pages, Winter.Blog, Winter.Redirect + * - Documentation + * - All settings should be backed / default to config values for file based configuration + * - Title prefix / suffix; includeTitle default true in component + * - Comments / help text for all provided fields ideally with references on how they should be used + * - Rename "Meta" tab to "SEO" tab + * - Character limit on description field + * - Full support for common robots settings via meta tags and site wide defaults + * - Default addition of generator meta tag in config file + * - Support for "Social Media" quick connects (FB / Twitter IDs / accounts) (maybe just via global tags) + * - Support for more sources of og:image (i.e. manual URL, fileupload, mediafinder, etc) + * - Favicon, webmanifest, browserconfig, apple touch icons, etc + * - Previews of FB / Twitter / Google + * - Support for structured data + * - Winter.Redirect + * - Model behavior to setup automatic redirects when URL attribute changes + * - Support for Winter.Builder pages + * - Support for twig parsing of meta tag values? + * - og:locale / other relevant locale flags, perhaps provide from Winter.Translate plugin? Or just from this plugin + * - Contextual settings for adding records to the sitemap rather than having to add everything in the sitemap main section? Perhaps implement in Winter.Sitemap + * + * @Related: + * - https://github.com/wintercms/wn-redirect-plugin + * - https://github.com/wintercms/wn-sitemap-plugin + * - https://github.com/wintercms/wn-pages-plugin + * - https://github.com/wintercms/wn-blog-plugin + * - https://github.com/wintercms/wn-translate-plugin + * + * @See also: + * - https://github.com/bennothommo/wn-meta-plugin + * - https://github.com/mjauvin/wn-mlsitemap-plugin + * - https://github.com/josephcrowell/wn-sitemappretty-plugin + * - https://github.com/studiobosco/wn-seo-extension + * - https://github.com/WebVPF/wn-robots-plugin + * - https://octobercms.com/plugin/renatio-seomanager + * - https://octobercms.com/plugin/lovata-mightyseo + * - https://octobercms.com/plugin/initbiz-seostorm + * - https://octobercms.com/plugin/zen-robots + * - https://octobercms.com/plugin/utopigs-seo + * - https://octobercms.com/plugin/mohsin-txt + * - https://octobercms.com/plugin/linkonoid-schemamarkup + * - https://octobercms.com/plugin/vdlp-schemaorg + * - https://octobercms.com/plugin/egalhtn-localbusiness + * - https://octobercms.com/plugin/dynamedia-posts + * - https://octobercms.com/plugin/eugene3993-seo + * - https://octobercms.com/plugin/magiczne-seotweaker + * - https://octobercms.com/plugin/eugene3993-seolight + */ class Plugin extends PluginBase { /** @@ -29,6 +89,7 @@ public function pluginDetails(): array public function boot(): void { $this->extendPagesForms(); + $this->extendExternalPlugins(); $this->populateGlobalTags(); } @@ -38,12 +99,8 @@ public function boot(): void protected function extendPagesForms() { $controllerModels = [ - \Cms\Controllers\Index::class => [ - \Cms\Classes\Page::class, - ], - \Winter\Pages\Controllers\Index::class => [ - \Winter\Pages\Classes\Page::class, - ], + \Cms\Controllers\Index::class => [ \Cms\Classes\Page::class ], + \Winter\Pages\Controllers\Index::class => [ \Winter\Pages\Classes\Page::class ], ]; Event::listen('backend.form.extendFieldsBefore', function (\Backend\Widgets\Form $widget) use ($controllerModels) { @@ -119,6 +176,57 @@ protected function populateGlobalTags() } }); + } + + /** + * Extends external plugins with support for SEO form fields + */ + protected function extendExternalPlugins(): void + { + // TODO + // Maybe move metadata field to single pivot table like [id, model, model_id, metadata] + $controllerModels = [ + \Winter\Blog\Controllers\Posts::class => [ + \Winter\Blog\Models\Post::class, + ], + \Winter\Blog\Controllers\Categories::class => [ + \Winter\Blog\Models\Category::class, + ], + ]; + + Event::listen('backend.form.extendFieldsBefore', function (\Backend\Widgets\Form $widget) use ($controllerModels) { + if($widget->isNested) return; + $controller = $widget->getController(); + $controllerClass = get_class($controller); + $model = $widget->model; + $modelClass = get_class($model); + $shouldExtend = property_exists($model, 'metadata_from') || + (isset($controllerModels[$controllerClass]) && + in_array($modelClass, $controllerModels[$controllerClass])); + + if(!$shouldExtend) return; + + // Create link to proper widget tabs container + if($widget->secondaryTabs) { + $tabs = &$widget->secondaryTabs; + } else { + $tabs = &$widget->tabs; + } + + // Model's metadata column can be set explicitly `public $metadata_from = 'column_name';` + $metadataField = strlen(trim($model->metadata_from)) ? $model->metadata_from : 'metadata'; + + $form = Yaml::parseFile(plugins_path('winter/seo/models/meta/fields.yaml')); + $tab = 'winter.seo::lang.models.meta.label'; + $fields = []; + foreach ($form['fields'] as $name => $config) { + $config['tab'] = $tab; + $fields["{$metadataField}[seo][{$name}]"] = $config; + } + $tabs['paneCssClass'][$tab] = 'padded-pane'; + $tabs['icons'][$tab] = 'icon-magnifying-glass'; + $tabs['fields'] = array_merge($tabs['fields'], $fields); + }); } /** From 4b993e87e1cc18aa7af3b729a2ef58d223660952 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Mon, 10 Apr 2023 20:04:09 +0300 Subject: [PATCH 05/24] + output external model's metadata support, json field names prefixed with 'meta_' to avoid excessive loop --- components/SEOTags.php | 29 +++++++++++++++++++++++++---- models/meta/fields.yaml | 8 ++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/components/SEOTags.php b/components/SEOTags.php index 60cfa17..87566b6 100644 --- a/components/SEOTags.php +++ b/components/SEOTags.php @@ -38,11 +38,13 @@ public function defineProperties() /** * Processes the meta tags for CMS pages and Winter.Pages static pages */ - protected function processPageMeta() + protected function processPageMeta(object $page = null) { // Store page settings in order to substitute with model settings if needed - $page = (object) $this->page->settings; + if(!$page) { + $page = (object) $this->page->settings; + } // Handle global settings if(Settings::get('global_enable') == 1) { @@ -222,10 +224,29 @@ protected function processFavicon(): void } } + /** + * Processes external model's metadata by calling it from controller + */ + public function useMetadataModel(\Model $model): void + { + // Im not sure about try/catch, but just logging seems to be not bad option + try { + $metadataField = strlen(trim($model->metadata_from)) ? $model->metadata_from : 'metadata'; + $metadata = $model->{$metadataField}; + if(!is_array($metadata)) { + $metadata = json_decode($metadata, true); + } + if(!isset($metadata['seo']) || empty($metadata['seo'])) { + return; + } + $this->processPageMeta((object)$metadata['seo']); + } catch(Exception $e) { + Log::error($e->getMessage()); + } + } + public function getMetaTags(): array { - // TODO - // Settings::global_minify_html $this->processFavicon(); $this->processPageMeta(); $this->processOgImage(); diff --git a/models/meta/fields.yaml b/models/meta/fields.yaml index 605eee4..0b51ccb 100644 --- a/models/meta/fields.yaml +++ b/models/meta/fields.yaml @@ -4,23 +4,23 @@ fields: type: section comment: winter.seo::lang.models.meta.instructions permissions: winter.seo.manage_meta - image: + meta_image: label: winter.seo::lang.models.meta.fields.image type: mediafinder mode: image span: left permissions: winter.seo.manage_meta - title: + meta_title: label: winter.seo::lang.models.meta.fields.title span: right permissions: winter.seo.manage_meta - description: + meta_description: label: winter.seo::lang.models.meta.fields.description span: right type: textarea size: tiny permissions: winter.seo.manage_meta - nofollow: + meta_nofollow: label: winter.seo::lang.models.meta.fields.nofollow type: switch permissions: winter.seo.manage_meta From a50e69fa4fb36a094b6aa9ae4b2ffb7a28c4fe9d Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Tue, 11 Apr 2023 15:08:30 +0300 Subject: [PATCH 06/24] + support of HTML compression --- middleware/CompressHTML.php | 43 +++++++++++++++++++++++++++++++++++++ models/settings/fields.yaml | 2 +- routes.php | 8 ++++++- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 middleware/CompressHTML.php diff --git a/middleware/CompressHTML.php b/middleware/CompressHTML.php new file mode 100644 index 0000000..ed80630 --- /dev/null +++ b/middleware/CompressHTML.php @@ -0,0 +1,43 @@ +getRequestUri(); + // Time to live in seconds + $cacheTTL = 3600; + // Get cached HTML or compress and save if cache not exists + $content = Cache::remember($cacheKey, $cacheTTL, function() use ($request, $next) { + return $this->compress($next($request)->getContent()); + }); + return response($content); + } + + protected function compress($buffer) + { + $replace = [ + // Remove HTML whitespaces + "/\n([\S])/" => '$1', + "/\r/" => '', + "/\n/" => '', + "/\t/" => '', + "/ +/" => ' ', + "/> + '><', + // Remove HTML comments + '//s' => '', + // Remove unnecessary url parts + '/https:/' => '', + '/http:/' => '', + // Replace attributes with short notation + '/ method=("get"|get)/' => '', + '/ disabled=[^ >]*(.*?)/' => ' disabled', + '/ selected=[^ >]*(.*?)/' => ' selected', + ]; + return preg_replace(array_keys($replace), array_values($replace), $buffer); + } +} \ No newline at end of file diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index 7cf51b5..9aea19c 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -20,7 +20,7 @@ tabs: default: 1 global_minify_html: tab: 'Global' - label: 'Minify HTML automatically' + label: 'Minify HTML (whitespaces and comments will be removed)' span: auto type: switch default: 0 diff --git a/routes.php b/routes.php index d8efad2..256c224 100644 --- a/routes.php +++ b/routes.php @@ -1,9 +1,15 @@ middleware('Winter\SEO\Middleware\CompressHTML'); + } +}); + Event::listen('backend.page.beforeDisplay', function($controller, $action, $params) { $controller->addJs('/plugins/winter/seo/assets/counter.js'); }); From 9a0a9fa07579850a73a97ad676ff2d78f93c52ce Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Wed, 12 Apr 2023 00:11:25 +0300 Subject: [PATCH 07/24] PR preparations attempt #1 --- Plugin.php | 8 ++++++-- README.md | 11 ----------- components/SEOTags.php | 23 +++++++++++++++-------- composer.json | 7 ++----- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/Plugin.php b/Plugin.php index 7ef8a33..6971dc4 100644 --- a/Plugin.php +++ b/Plugin.php @@ -99,8 +99,12 @@ public function boot(): void protected function extendPagesForms() { $controllerModels = [ - \Cms\Controllers\Index::class => [ \Cms\Classes\Page::class ], - \Winter\Pages\Controllers\Index::class => [ \Winter\Pages\Classes\Page::class ], + \Cms\Controllers\Index::class => [ + \Cms\Classes\Page::class + ], + \Winter\Pages\Controllers\Index::class => [ + \Winter\Pages\Classes\Page::class + ], ]; Event::listen('backend.form.extendFieldsBefore', function (\Backend\Widgets\Form $widget) use ($controllerModels) { diff --git a/README.md b/README.md index f4c3b77..38391c7 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,5 @@ # SEO Plugin -### Installation -#### Before installation add this to your `composer.json` so plugin will act like original one -```json -"extra": { - "installer-paths": { - "./plugins/winter/{$name}/": ["8black/wn-seo-plugin", "8black/seo"] - } -} -``` -#### Run `composer require 8black/wn-seo-plugin` - WIP plugin for managing SEO content in Winter CMS. Inspired by https://github.com/bennothommo/wn-meta-plugin. Future plans including support for easily generating structured data and automatically attaching SEO meta fields to CMS pages, Winter.Pages pages, & generically to any Winter CMS model. diff --git a/components/SEOTags.php b/components/SEOTags.php index 87566b6..91333cd 100644 --- a/components/SEOTags.php +++ b/components/SEOTags.php @@ -23,7 +23,7 @@ public function componentDetails() { return [ 'name' => 'SEOTags', - 'description' => 'Outputs specified tags to the page' + 'description' => 'Outputs meta tags to the page' ]; } @@ -40,6 +40,8 @@ public function defineProperties() */ protected function processPageMeta(object $page = null) { + // $this['page_title'] = $this->page->title ?? Meta::get('og:title') ?? ''; + // $this['app_name'] = BrandSetting::get('app_name'); // Store page settings in order to substitute with model settings if needed if(!$page) { @@ -111,8 +113,11 @@ protected function processPageMeta(object $page = null) foreach ($metaMap as $class => $map) { foreach ($map as $name => $pageProp) { - if (empty($class::get($name)) && !empty($page->{$pageProp})) { - $class::set($name, $page->{$pageProp}); + if ( + empty($class::get($name)) + && !empty($this->page->{$pageProp}) + ) { + $class::set($name, $this->page->{$pageProp}); } } } @@ -181,6 +186,8 @@ protected function processOgDescription(): void { if (!empty(Meta::get('description')) && empty(Meta::get('og:description'))) { Meta::set('og:description', Meta::get('description')); + } elseif (!empty(Meta::get('og:description')) && empty(Meta::get('description'))) { + Meta::set('description', Meta::get('og:description')); } } @@ -263,13 +270,13 @@ public function getLinkTags(): array return Link::all(); } -// dd(Meta::all(), Link::all(), __LINE__, __FILE__); + // dd(Meta::all(), Link::all(), __LINE__, __FILE__); -// Meta::set('og:title', $meta['title']); -// Meta::set('og:description', $meta['description']); -// Meta::set('og:image', \System\Classes\MediaLibrary::url($meta['image'])); -// Link::set('canonical', $meta['canonical_url']); + // Meta::set('og:title', $meta['title']); + // Meta::set('og:description', $meta['description']); + // Meta::set('og:image', \System\Classes\MediaLibrary::url($meta['image'])); + // Link::set('canonical', $meta['canonical_url']); // {# Pagination Links #} diff --git a/composer.json b/composer.json index 9c5bd9b..b702d51 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { - "name": "8black/wn-seo-plugin", + "name": "winter/wn-seo-plugin", "type": "winter-plugin", "description": "Winter CMS plugin for managing SEO tags", - "homepage": "https://github.com/8blvck/wn-seo-plugin", + "homepage": "https://github.com/wintercms/wn-seo-plugin", "keywords": ["cms", "wintercms", "seo"], "license": "MIT", "authors": [ @@ -24,8 +24,5 @@ "require": { "php": ">=7.2", "composer/installers": "~1.11" - }, - "extra": { - "installer-name": "seo" } } From e8925cc3a95aa25d3853385baa3087793e2ffba9 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Wed, 12 Apr 2023 00:16:43 +0300 Subject: [PATCH 08/24] PR preparations attempt 2 --- Plugin.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Plugin.php b/Plugin.php index 6971dc4..dd54062 100644 --- a/Plugin.php +++ b/Plugin.php @@ -99,11 +99,11 @@ public function boot(): void protected function extendPagesForms() { $controllerModels = [ - \Cms\Controllers\Index::class => [ - \Cms\Classes\Page::class + \Cms\Controllers\Index::class => [ + \Cms\Classes\Page::class, ], - \Winter\Pages\Controllers\Index::class => [ - \Winter\Pages\Classes\Page::class + \Winter\Pages\Controllers\Index::class => [ + \Winter\Pages\Classes\Page::class, ], ]; From 98287575b23b62b62b83991311e2ec87dea4537f Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Wed, 12 Apr 2023 01:36:32 +0300 Subject: [PATCH 09/24] PR preparation attempt 3. Config values added, now settings defaults to config --- Plugin.php | 4 +--- components/SEOTags.php | 16 +++++++++------- config/config.php | 23 ++++++++++++++++++++++- models/Settings.php | 5 +++++ models/meta/fields.yaml | 8 ++++---- models/settings/fields.yaml | 2 +- routes.php | 10 +++++----- 7 files changed, 47 insertions(+), 21 deletions(-) diff --git a/Plugin.php b/Plugin.php index dd54062..041d41f 100644 --- a/Plugin.php +++ b/Plugin.php @@ -187,8 +187,6 @@ protected function populateGlobalTags() */ protected function extendExternalPlugins(): void { - // TODO - // Maybe move metadata field to single pivot table like [id, model, model_id, metadata] $controllerModels = [ \Winter\Blog\Controllers\Posts::class => [ \Winter\Blog\Models\Post::class, @@ -225,7 +223,7 @@ protected function extendExternalPlugins(): void $fields = []; foreach ($form['fields'] as $name => $config) { $config['tab'] = $tab; - $fields["{$metadataField}[seo][{$name}]"] = $config; + $fields["{$metadataField}[seo][{meta_$name}]"] = $config; } $tabs['paneCssClass'][$tab] = 'padded-pane'; $tabs['icons'][$tab] = 'icon-magnifying-glass'; diff --git a/components/SEOTags.php b/components/SEOTags.php index 91333cd..35168e0 100644 --- a/components/SEOTags.php +++ b/components/SEOTags.php @@ -48,20 +48,22 @@ protected function processPageMeta(object $page = null) $page = (object) $this->page->settings; } + dd($this->page->settings); + // Handle global settings - if(Settings::get('global_enable') == 1) { - $name = Settings::get('global_app_name'); - $position = Settings::get('global_app_name_pos'); - $separator = Settings::get('global_separator'); + if(Settings::getOrDefault('global_enable_tags')) { + $name = Settings::getOrDefault('global_app_name'); + $position = Settings::getOrDefault('global_app_name_pos'); + $separator = Settings::getOrDefault('global_separator'); // Substitute empty title by global setting if(empty(trim($page->meta_title))) { - $page->meta_title = Settings::get('global_app_title'); + $page->meta_title = Settings::getOrDefault('global_app_title'); } // Substitute empty description by global setting if(empty(trim($page->meta_description))) { - $page->meta_description = Settings::get('global_app_description'); + $page->meta_description = Settings::getOrDefault('global_app_description'); } if(empty($name)) { @@ -226,7 +228,7 @@ protected function processOgSiteName(): void */ protected function processFavicon(): void { - if(Settings::get('enable_favicon') == 1) { + if(Settings::getOrDefault('enable_favicon')) { Link::set('icon', '/favicon.ico'); } } diff --git a/config/config.php b/config/config.php index 9cfef9d..2ee06fe 100644 --- a/config/config.php +++ b/config/config.php @@ -2,17 +2,38 @@ return [ 'default_social_image' => null, + + 'enable_humans_txt' => true, 'humans_txt' => [ 'path' => base_path('humans.txt'), ], + + 'enable_robots_txt' => true, 'robots_txt' => [ 'path' => base_path('robots.txt'), ], + 'enable_security_txt' => true, + 'security_txt' => [ 'path' => base_path('security.txt'), ], + + 'enable_favicon' => false, -]; \ No newline at end of file + 'global_enable_tags' => false, + + 'global_minify_html' => true, + + 'global_app_name' => null, + + 'global_app_name_pos' => null, + + 'global_separator' => null, + + 'global_app_title' => null, + + 'global_app_description' => null, +]; diff --git a/models/Settings.php b/models/Settings.php index 7af75df..6384228 100644 --- a/models/Settings.php +++ b/models/Settings.php @@ -52,4 +52,9 @@ public function initSettingsData(): void $this->robots_txt = $contentsFromConfig('robots_txt'); $this->security_txt = $contentsFromConfig('security_txt'); } + + public static function getOrDefault($prop) { + return self::get($prop, Config::get("winter.seo::{$prop}", null)); + } + } diff --git a/models/meta/fields.yaml b/models/meta/fields.yaml index 0b51ccb..605eee4 100644 --- a/models/meta/fields.yaml +++ b/models/meta/fields.yaml @@ -4,23 +4,23 @@ fields: type: section comment: winter.seo::lang.models.meta.instructions permissions: winter.seo.manage_meta - meta_image: + image: label: winter.seo::lang.models.meta.fields.image type: mediafinder mode: image span: left permissions: winter.seo.manage_meta - meta_title: + title: label: winter.seo::lang.models.meta.fields.title span: right permissions: winter.seo.manage_meta - meta_description: + description: label: winter.seo::lang.models.meta.fields.description span: right type: textarea size: tiny permissions: winter.seo.manage_meta - meta_nofollow: + nofollow: label: winter.seo::lang.models.meta.fields.nofollow type: switch permissions: winter.seo.manage_meta diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index 9aea19c..a077fda 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -12,7 +12,7 @@ tabs: winter.seo::lang.models.settings.robots_txt: icon-robot winter.seo::lang.models.settings.security_txt: icon-lock fields: - global_enable: + global_enable_tags: tab: 'Global' label: 'Enable global meta tags (not overrides page settings)' span: auto diff --git a/routes.php b/routes.php index 256c224..e3166e5 100644 --- a/routes.php +++ b/routes.php @@ -5,7 +5,7 @@ use File as FileManager; CmsController::extend(function($controller) { - if(Settings::get('global_minify_html') == 1) { + if(Settings::getOrDefault('global_minify_html')) { $controller->middleware('Winter\SEO\Middleware\CompressHTML'); } }); @@ -27,7 +27,7 @@ return Response::make($contents, 200, ['Content-Type' => 'text/plain']); }; - if(Settings::get('enable_favicon') == 1) { + if(Settings::getOrDefault('enable_favicon')) { Route::get('favicon.ico', function() use ($notFoundError) { $favicon = Settings::get('favicon'); $faviconPath = base_path().media_path($favicon); @@ -45,13 +45,13 @@ return response()->file($outputPath, [ 'Content-Type'=> 'image/x-icon' ]); }); } - if(Settings::get('enable_humans_txt') == 1) { + if(Settings::getOrDefault('enable_humans_txt')) { Route::get('/humans.txt', fn() => $txtResponse('humans_txt')); } - if(Settings::get('enable_robots_txt') == 1) { + if(Settings::getOrDefault('enable_robots_txt')) { Route::get('/robots.txt', fn() => $txtResponse('robots_txt')); } - if(Settings::get('enable_security_txt') == 1) { + if(Settings::getOrDefault('enable_security_txt')) { Route::get('/security.txt', fn() => $txtResponse('security_txt')); Route::get('/.well-known/security.txt', fn() => $txtResponse('security_txt')); } From 9c8835e570b99f7db7dbdac2bc9e484703139d15 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Wed, 12 Apr 2023 01:45:14 +0300 Subject: [PATCH 10/24] PR preparation attempt 4. Indentetion fixes --- Plugin.php | 2 +- assets/counter.js | 112 ++++++++++++++++----------------- components/SEOTags.php | 6 +- components/seotags/default.htm | 12 ++-- config/config.php | 2 +- models/settings/fields.yaml | 4 +- 6 files changed, 69 insertions(+), 69 deletions(-) diff --git a/Plugin.php b/Plugin.php index 041d41f..2252432 100644 --- a/Plugin.php +++ b/Plugin.php @@ -223,7 +223,7 @@ protected function extendExternalPlugins(): void $fields = []; foreach ($form['fields'] as $name => $config) { $config['tab'] = $tab; - $fields["{$metadataField}[seo][{meta_$name}]"] = $config; + $fields["{$metadataField}[seo][meta_{$name}]"] = $config; } $tabs['paneCssClass'][$tab] = 'padded-pane'; $tabs['icons'][$tab] = 'icon-magnifying-glass'; diff --git a/assets/counter.js b/assets/counter.js index e0f0f66..f7ebedc 100644 --- a/assets/counter.js +++ b/assets/counter.js @@ -1,62 +1,62 @@ (function (win, doc) { - counter = win.counter || {} - counter.seo = { - charCountHandler: function(target) { - $target = $(target); - let $helpBlock = $target.next('.help-block'); - let isTwig = /\{{2}.*\}{2}/.test($target.val()); - let count = isTwig ? 'unrechable' : $target.val().length; - let min = $target.data('min'); - let max = $target.data('max'); - $helpBlock.html(`Symbols: ${count} of ${min} - ${max} optimal`); - let $number = $helpBlock.find('b'); - if (count < max && count > min) { - $number.css({color: 'lime'}); - } else if (isTwig) { - $number.css({color: 'coral'}); - } else { - $number.css({color: 'salmon'}); - } - } - } - var listeners = [], - doc = win.document, - MutationObserver = win.MutationObserver || win.WebKitMutationObserver, - observer; - - function ready(selector, fn) { - listeners.push({ - selector: selector, - fn: fn - }); - if (!observer) { - observer = new MutationObserver(check); - observer.observe(doc.documentElement, { - childList: true, - subtree: true - }); + counter = win.counter || {} + counter.seo = { + charCountHandler: function(target) { + $target = $(target); + let $helpBlock = $target.next('.help-block'); + let isTwig = /\{{2}.*\}{2}/.test($target.val()); + let count = isTwig ? 'unrechable' : $target.val().length; + let min = $target.data('min'); + let max = $target.data('max'); + $helpBlock.html(`Symbols: ${count} of ${min} - ${max} optimal`); + let $number = $helpBlock.find('b'); + if (count < max && count > min) { + $number.css({color: 'lime'}); + } else if (isTwig) { + $number.css({color: 'coral'}); + } else { + $number.css({color: 'salmon'}); + } + } + } + var listeners = [], + doc = win.document, + MutationObserver = win.MutationObserver || win.WebKitMutationObserver, + observer; + + function ready(selector, fn) { + listeners.push({ + selector: selector, + fn: fn + }); + if (!observer) { + observer = new MutationObserver(check); + observer.observe(doc.documentElement, { + childList: true, + subtree: true + }); + } + check(); } - check(); - } - - function check() { - for (var i = 0, len = listeners.length, listener, elements; i < len; i++) { - listener = listeners[i]; - elements = doc.querySelectorAll(listener.selector); - for (var j = 0, jLen = elements.length, element; j < jLen; j++) { - element = elements[j]; - if (!element.ready) { - element.ready = true; - listener.fn.call(element, element); + + function check() { + for (var i = 0, len = listeners.length, listener, elements; i < len; i++) { + listener = listeners[i]; + elements = doc.querySelectorAll(listener.selector); + for (var j = 0, jLen = elements.length, element; j < jLen; j++) { + element = elements[j]; + if (!element.ready) { + element.ready = true; + listener.fn.call(element, element); + } + } } - } } - } - win.ready = ready; - win.counter = counter; - win.ready('[data-counter]', (el) => { - counter.seo.charCountHandler(el) - el.oninput = event => counter.seo.charCountHandler(el); - }); + win.ready = ready; + win.counter = counter; + win.ready('[data-counter]', (el) => { + counter.seo.charCountHandler(el) + el.oninput = event => counter.seo.charCountHandler(el); + }); })(window, document); \ No newline at end of file diff --git a/components/SEOTags.php b/components/SEOTags.php index 35168e0..d4cb28a 100644 --- a/components/SEOTags.php +++ b/components/SEOTags.php @@ -182,9 +182,9 @@ protected function processOgImage(): void } /** - * Processes the og:description meta tags + * Processes the og:description / description meta tags */ - protected function processOgDescription(): void + protected function processDescription(): void { if (!empty(Meta::get('description')) && empty(Meta::get('og:description'))) { Meta::set('og:description', Meta::get('description')); @@ -259,7 +259,7 @@ public function getMetaTags(): array $this->processFavicon(); $this->processPageMeta(); $this->processOgImage(); - $this->processOgDescription(); + $this->processDescription(); $this->processOgUrl(); $this->processOgType(); $this->processOgSiteName(); diff --git a/components/seotags/default.htm b/components/seotags/default.htm index 4c087a9..30456db 100644 --- a/components/seotags/default.htm +++ b/components/seotags/default.htm @@ -1,13 +1,13 @@ {# tags #} {% for tagName, tagContent in __SELF__.getMetaTags() %} - {% if tagName == 'title' %} - {{ tagContent }} - {% else %} - - {% endif %} + {% if tagName == 'title' %} + {{ tagContent }} + {% else %} + + {% endif %} {% endfor %} {# tags #} {% for tagName, tagContent in __SELF__.getLinkTags() %} - + {% endfor %} diff --git a/config/config.php b/config/config.php index 2ee06fe..0738880 100644 --- a/config/config.php +++ b/config/config.php @@ -25,7 +25,7 @@ 'global_enable_tags' => false, - 'global_minify_html' => true, + 'global_minify_html' => false, 'global_app_name' => null, diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index a077fda..40941d5 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -17,7 +17,7 @@ tabs: label: 'Enable global meta tags (not overrides page settings)' span: auto type: switch - default: 1 + default: 0 global_minify_html: tab: 'Global' label: 'Minify HTML (whitespaces and comments will be removed)' @@ -119,7 +119,7 @@ tabs: label: 'Enable icons' span: full type: switch - default: 1 + default: 0 favicon: tab: 'Favicon' commentAbove: 'Global favicon' From 217617c1fa835329409e19f99064e8ca4bbdd882 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Wed, 12 Apr 2023 01:48:14 +0300 Subject: [PATCH 11/24] PR preparation attempt 5. New lines fixes --- assets/counter.js | 2 +- config/config.php | 1 + routes.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/counter.js b/assets/counter.js index f7ebedc..8f094bc 100644 --- a/assets/counter.js +++ b/assets/counter.js @@ -59,4 +59,4 @@ el.oninput = event => counter.seo.charCountHandler(el); }); })(window, document); - \ No newline at end of file + diff --git a/config/config.php b/config/config.php index 0738880..bb15357 100644 --- a/config/config.php +++ b/config/config.php @@ -37,3 +37,4 @@ 'global_app_description' => null, ]; + diff --git a/routes.php b/routes.php index e3166e5..c44aaa3 100644 --- a/routes.php +++ b/routes.php @@ -1,4 +1,5 @@ Date: Wed, 12 Apr 2023 01:49:53 +0300 Subject: [PATCH 12/24] PR preparation attempt 6. New lines fixex --- config/config.php | 1 - middleware/CompressHTML.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/config/config.php b/config/config.php index bb15357..0738880 100644 --- a/config/config.php +++ b/config/config.php @@ -37,4 +37,3 @@ 'global_app_description' => null, ]; - diff --git a/middleware/CompressHTML.php b/middleware/CompressHTML.php index ed80630..6b389dc 100644 --- a/middleware/CompressHTML.php +++ b/middleware/CompressHTML.php @@ -40,4 +40,4 @@ protected function compress($buffer) ]; return preg_replace(array_keys($replace), array_values($replace), $buffer); } -} \ No newline at end of file +} From e002022645b8a59a353f334b9d5830e8c015f804 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Wed, 12 Apr 2023 02:15:09 +0300 Subject: [PATCH 13/24] debug removed --- components/SEOTags.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/SEOTags.php b/components/SEOTags.php index d4cb28a..eeafe7f 100644 --- a/components/SEOTags.php +++ b/components/SEOTags.php @@ -47,8 +47,6 @@ protected function processPageMeta(object $page = null) if(!$page) { $page = (object) $this->page->settings; } - - dd($this->page->settings); // Handle global settings if(Settings::getOrDefault('global_enable_tags')) { From 05838182438d51249bbdba6a27e125f057acfe26 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Wed, 12 Apr 2023 02:42:15 +0300 Subject: [PATCH 14/24] meta_title & meta_description empty check returned --- components/SEOTags.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/SEOTags.php b/components/SEOTags.php index eeafe7f..0067184 100644 --- a/components/SEOTags.php +++ b/components/SEOTags.php @@ -74,12 +74,12 @@ protected function processPageMeta(object $page = null) } // Set the page title - if(empty(trim(Meta::get('title')))) { + if(!empty($page->meta_title) && empty(trim(Meta::get('title')))) { Meta::set('title', $page->meta_title); } - // Set the page descriptioe - if(empty(trim(Meta::get('description')))) { + // Set the page description + if(!empty($page->meta_description) && empty(trim(Meta::get('description')))) { Meta::set('description', $page->meta_description); } From 0128098949d136ca495f61bcd8642148fa082655 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Thu, 13 Apr 2023 15:01:07 +0300 Subject: [PATCH 15/24] * counter script update --- assets/counter.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/assets/counter.js b/assets/counter.js index 8f094bc..21c8f04 100644 --- a/assets/counter.js +++ b/assets/counter.js @@ -4,18 +4,17 @@ charCountHandler: function(target) { $target = $(target); let $helpBlock = $target.next('.help-block'); - let isTwig = /\{{2}.*\}{2}/.test($target.val()); - let count = isTwig ? 'unrechable' : $target.val().length; let min = $target.data('min'); let max = $target.data('max'); + let count = $target.val().replace(/[{}]/g, "").length; $helpBlock.html(`Symbols: ${count} of ${min} - ${max} optimal`); let $number = $helpBlock.find('b'); if (count < max && count > min) { $number.css({color: 'lime'}); - } else if (isTwig) { - $number.css({color: 'coral'}); + } else if(count < min) { + $number.css({color: 'darkred'}); } else { - $number.css({color: 'salmon'}); + $number.css({color: 'coral'}); } } } @@ -24,7 +23,7 @@ MutationObserver = win.MutationObserver || win.WebKitMutationObserver, observer; - function ready(selector, fn) { + const ready = (selector, fn) => { listeners.push({ selector: selector, fn: fn @@ -39,7 +38,7 @@ check(); } - function check() { + const check = () => { for (var i = 0, len = listeners.length, listener, elements; i < len; i++) { listener = listeners[i]; elements = doc.querySelectorAll(listener.selector); From 784eb9ff328f6498c98188d8b0a9dfcd8b97fa1a Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Thu, 13 Apr 2023 17:09:18 +0300 Subject: [PATCH 16/24] * favicon resizing improved --- assets/counter.js | 1 - routes.php | 40 +++++++++++++++------------------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/assets/counter.js b/assets/counter.js index 21c8f04..3ddebba 100644 --- a/assets/counter.js +++ b/assets/counter.js @@ -58,4 +58,3 @@ el.oninput = event => counter.seo.charCountHandler(el); }); })(window, document); - diff --git a/routes.php b/routes.php index c44aaa3..888f4a1 100644 --- a/routes.php +++ b/routes.php @@ -1,7 +1,7 @@ run('404'), 404); - }; - $txtResponse = function ($key) { $contents = Settings::get($key); if (empty($contents)) { - return $notFoundError(); + return Response::make((new \Cms\Classes\Controller())->run('404'), 404); } return Response::make($contents, 200, ['Content-Type' => 'text/plain']); }; - - if(Settings::getOrDefault('enable_favicon')) { - Route::get('favicon.ico', function() use ($notFoundError) { - $favicon = Settings::get('favicon'); - $faviconPath = base_path().media_path($favicon); - $outputPath = base_path().media_path('favicon/' . basename($favicon)); - if(!strlen($favicon) || !file_exists($faviconPath)) { - return $notFoundError(); - } - try { - if (!file_exists($outputPath)) { - Resizer::open($faviconPath)->resize(16, 16)->save($outputPath); - } - } catch(Exception $e) { - $outputPath = $faviconPath; - } - return response()->file($outputPath, [ 'Content-Type'=> 'image/x-icon' ]); - }); - } if(Settings::getOrDefault('enable_humans_txt')) { Route::get('/humans.txt', fn() => $txtResponse('humans_txt')); } @@ -56,4 +33,17 @@ Route::get('/security.txt', fn() => $txtResponse('security_txt')); Route::get('/.well-known/security.txt', fn() => $txtResponse('security_txt')); } + if(Settings::getOrDefault('enable_favicon')) { + Route::get('favicon.ico', function() { + $faviconPath = media_path(Settings::get('favicon')); + try { + $favicon = new ImageResizer($faviconPath, 16, 16, ['mode'=>'crop']); + $favicon->resize(); + $outputPath = storage_path('app/'.$favicon->getPathToResizedImage()); + } catch(Exception $e) { + $outputPath = base_path($faviconPath); + } + return response()->file($outputPath, [ 'Content-Type'=> 'image/x-icon' ]); + }); + } }); From 69b50371e783919ca8aff35191be1c60342286a9 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Thu, 13 Apr 2023 17:56:04 +0300 Subject: [PATCH 17/24] * global_ config became nested --- components/SEOTags.php | 24 +++++++++---------- config/config.php | 46 ++++++++++++++++++++----------------- models/settings/fields.yaml | 30 ++++++++++++------------ routes.php | 12 +++++----- 4 files changed, 58 insertions(+), 54 deletions(-) diff --git a/components/SEOTags.php b/components/SEOTags.php index 0067184..cf3ad95 100644 --- a/components/SEOTags.php +++ b/components/SEOTags.php @@ -44,24 +44,24 @@ protected function processPageMeta(object $page = null) // $this['app_name'] = BrandSetting::get('app_name'); // Store page settings in order to substitute with model settings if needed - if(!$page) { - $page = (object) $this->page->settings; + if (!$page) { + $page = $this->page; } - + // Handle global settings - if(Settings::getOrDefault('global_enable_tags')) { - $name = Settings::getOrDefault('global_app_name'); - $position = Settings::getOrDefault('global_app_name_pos'); - $separator = Settings::getOrDefault('global_separator'); + if (Settings::getOrDefault('global.enable_tags')) { + $name = Settings::getOrDefault('global.app_name'); + $position = Settings::getOrDefault('global.app_name_pos'); + $separator = Settings::getOrDefault('global.separator'); // Substitute empty title by global setting if(empty(trim($page->meta_title))) { - $page->meta_title = Settings::getOrDefault('global_app_title'); + $page->meta_title = Settings::getOrDefault('global.app_title'); } // Substitute empty description by global setting if(empty(trim($page->meta_description))) { - $page->meta_description = Settings::getOrDefault('global_app_description'); + $page->meta_description = Settings::getOrDefault('global.app_description'); } if(empty($name)) { @@ -74,12 +74,12 @@ protected function processPageMeta(object $page = null) } // Set the page title - if(!empty($page->meta_title) && empty(trim(Meta::get('title')))) { + if (!empty($page->meta_title) && empty(trim(Meta::get('title')))) { Meta::set('title', $page->meta_title); } // Set the page description - if(!empty($page->meta_description) && empty(trim(Meta::get('description')))) { + if (!empty($page->meta_description) && empty(trim(Meta::get('description')))) { Meta::set('description', $page->meta_description); } @@ -226,7 +226,7 @@ protected function processOgSiteName(): void */ protected function processFavicon(): void { - if(Settings::getOrDefault('enable_favicon')) { + if(Settings::getOrDefault('enable_favicon.enabled')) { Link::set('icon', '/favicon.ico'); } } diff --git a/config/config.php b/config/config.php index 0738880..545f7e8 100644 --- a/config/config.php +++ b/config/config.php @@ -3,37 +3,41 @@ return [ 'default_social_image' => null, - 'enable_humans_txt' => true, - 'humans_txt' => [ 'path' => base_path('humans.txt'), + 'enabled' => true, ], - - 'enable_robots_txt' => true, 'robots_txt' => [ 'path' => base_path('robots.txt'), + 'enabled' => true, ], - 'enable_security_txt' => true, - 'security_txt' => [ 'path' => base_path('security.txt'), + 'enabled' => true, ], - - 'enable_favicon' => false, - 'global_enable_tags' => false, - - 'global_minify_html' => false, - - 'global_app_name' => null, - - 'global_app_name_pos' => null, - - 'global_separator' => null, - - 'global_app_title' => null, - - 'global_app_description' => null, + 'favicon' => [ + 'path' => base_path('favicon.ico'), + 'enabled' => false, + ], + + 'blobal' => [ + + 'enable_tags' => false, + + 'minify_html' => false, + + 'app_name' => null, + + 'app_name_pos' => null, + + 'separator' => null, + + 'app_title' => null, + + 'app_description' => null, + + ], ]; diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index 40941d5..ef00930 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -12,19 +12,19 @@ tabs: winter.seo::lang.models.settings.robots_txt: icon-robot winter.seo::lang.models.settings.security_txt: icon-lock fields: - global_enable_tags: + global.enable_tags: tab: 'Global' label: 'Enable global meta tags (not overrides page settings)' span: auto type: switch default: 0 - global_minify_html: + global.minify_html: tab: 'Global' label: 'Minify HTML (whitespaces and comments will be removed)' span: auto type: switch default: 0 - global_app_name: + global.app_name: tab: 'Global' label: 'App name' span: auto @@ -32,7 +32,7 @@ tabs: type: text default: '' comment: 'App name can be visible in the title meta tag' - global_separator: + global.separator: tab: 'Global' label: 'App name separator' span: auto @@ -40,7 +40,7 @@ tabs: type: text default: '|' comment: 'Sybmol to separate title from app name: {app name} {sepratator} {title}' - global_app_name_pos: + global.app_name_pos: tab: 'Global' label: 'Select how the app name should appear in the title' options: @@ -49,9 +49,9 @@ tabs: suffix: 'Suffix' span: auto type: balloon-selector - global_app_title: + global.app_title: tab: 'Global' - label: 'App title' + label: 'Default meta title' size: tiny span: full placeholder: 'Your app global title' @@ -61,9 +61,9 @@ tabs: data-counter: 1 data-min: 50 data-max: 60 - global_app_description: + global.app_description: tab: 'Global' - label: 'App description' + label: 'Default meta desciption' size: tiny span: full placeholder: 'Your app global description' @@ -114,19 +114,19 @@ tabs: type: textarea size: tiny label: winter.seo::lang.models.link.description - enable_favicon: + favicon.enabled: tab: 'Favicon' - label: 'Enable icons' + label: 'Enable favicon' span: full type: switch default: 0 - favicon: + favicon.path: tab: 'Favicon' commentAbove: 'Global favicon' type: mediafinder mode: image span: left - enable_humans_txt: + humans_txt.enabled: tab: winter.seo::lang.models.settings.humans_txt label: 'Enable humans.txt' span: full @@ -137,7 +137,7 @@ tabs: commentAbove: winter.seo::lang.models.settings.humans_txt_comment type: textarea size: small - enable_robots_txt: + robots_txt.enabled: tab: winter.seo::lang.models.settings.robots_txt label: 'Enable robots.txt' span: full @@ -148,7 +148,7 @@ tabs: commentAbove: winter.seo::lang.models.settings.robots_txt_comment type: textarea size: small - enable_security_txt: + security_txt.enabled: tab: winter.seo::lang.models.settings.security_txt label: 'Enable security.txt' span: full diff --git a/routes.php b/routes.php index 888f4a1..bfc1f85 100644 --- a/routes.php +++ b/routes.php @@ -6,7 +6,7 @@ use File as FileManager; CmsController::extend(function($controller) { - if(Settings::getOrDefault('global_minify_html')) { + if(Settings::getOrDefault('global.minify_html')) { $controller->middleware('Winter\SEO\Middleware\CompressHTML'); } }); @@ -23,19 +23,19 @@ } return Response::make($contents, 200, ['Content-Type' => 'text/plain']); }; - if(Settings::getOrDefault('enable_humans_txt')) { + if(Settings::getOrDefault('humans_txt.enabled')) { Route::get('/humans.txt', fn() => $txtResponse('humans_txt')); } - if(Settings::getOrDefault('enable_robots_txt')) { + if(Settings::getOrDefault('robots_txt.enabled')) { Route::get('/robots.txt', fn() => $txtResponse('robots_txt')); } - if(Settings::getOrDefault('enable_security_txt')) { + if(Settings::getOrDefault('security_txt.enabled')) { Route::get('/security.txt', fn() => $txtResponse('security_txt')); Route::get('/.well-known/security.txt', fn() => $txtResponse('security_txt')); } - if(Settings::getOrDefault('enable_favicon')) { + if(Settings::getOrDefault('favicon.enabled')) { Route::get('favicon.ico', function() { - $faviconPath = media_path(Settings::get('favicon')); + $faviconPath = media_path(Settings::get('favicon.path')); try { $favicon = new ImageResizer($faviconPath, 16, 16, ['mode'=>'crop']); $favicon->resize(); From 8f16b01113f0ba0eeacfaed044956fc6d5569bbf Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Fri, 14 Apr 2023 00:55:36 +0300 Subject: [PATCH 18/24] * fields.yml texts update --- config/config.php | 2 +- models/settings/fields.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/config.php b/config/config.php index 545f7e8..530b73d 100644 --- a/config/config.php +++ b/config/config.php @@ -2,7 +2,7 @@ return [ 'default_social_image' => null, - + 'humans_txt' => [ 'path' => base_path('humans.txt'), 'enabled' => true, diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index ef00930..f4f131d 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -54,7 +54,7 @@ tabs: label: 'Default meta title' size: tiny span: full - placeholder: 'Your app global title' + placeholder: 'Meta description that is used for the page when one isn''t set already.' type: text comment: '[data-counter]' attributes: @@ -66,7 +66,7 @@ tabs: label: 'Default meta desciption' size: tiny span: full - placeholder: 'Your app global description' + placeholder: 'Meta description that is used for the page when one isn''t set already.' type: textarea comment: '[data-counter]' attributes: From c38dd8cc4bc81ba82a4d2807ae005210e9265ddd Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Fri, 14 Apr 2023 01:01:11 +0300 Subject: [PATCH 19/24] * minor texts fixes --- config/config.php | 2 +- models/settings/fields.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.php b/config/config.php index 530b73d..ef0e288 100644 --- a/config/config.php +++ b/config/config.php @@ -23,7 +23,7 @@ 'enabled' => false, ], - 'blobal' => [ + 'global' => [ 'enable_tags' => false, diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index f4f131d..e08aa05 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -122,7 +122,7 @@ tabs: default: 0 favicon.path: tab: 'Favicon' - commentAbove: 'Global favicon' + commentAbove: 'Shortcut icon of your app' type: mediafinder mode: image span: left From bc1fe29bf32d7d5de0a703b20002f7d896fef8cc Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Fri, 14 Apr 2023 01:16:55 +0300 Subject: [PATCH 20/24] * yaml indentation fix --- models/meta/fields.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/models/meta/fields.yaml b/models/meta/fields.yaml index 605eee4..528b12f 100644 --- a/models/meta/fields.yaml +++ b/models/meta/fields.yaml @@ -14,12 +14,22 @@ fields: label: winter.seo::lang.models.meta.fields.title span: right permissions: winter.seo.manage_meta + comment: '[data-counter]' + attributes: + data-counter: 1 + data-min: 50 + data-max: 60 description: label: winter.seo::lang.models.meta.fields.description span: right type: textarea size: tiny permissions: winter.seo.manage_meta + comment: '[data-counter]' + attributes: + data-counter: 1 + data-min: 100 + data-max: 160 nofollow: label: winter.seo::lang.models.meta.fields.nofollow type: switch From 7d691463fa34c88c28ed42b038812ad7069f74c4 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Fri, 14 Apr 2023 02:34:46 +0300 Subject: [PATCH 21/24] * favicon now uses fileupload instead media --- components/SEOTags.php | 2 +- config/config.php | 1 - models/Settings.php | 4 ++++ models/settings/fields.yaml | 15 +++++++++------ routes.php | 12 +++--------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/components/SEOTags.php b/components/SEOTags.php index cf3ad95..4b7a2b5 100644 --- a/components/SEOTags.php +++ b/components/SEOTags.php @@ -226,7 +226,7 @@ protected function processOgSiteName(): void */ protected function processFavicon(): void { - if(Settings::getOrDefault('enable_favicon.enabled')) { + if(Settings::getOrDefault('favicon.enabled') && Settings::instance()->app_favicon) { Link::set('icon', '/favicon.ico'); } } diff --git a/config/config.php b/config/config.php index ef0e288..76e38b3 100644 --- a/config/config.php +++ b/config/config.php @@ -19,7 +19,6 @@ ], 'favicon' => [ - 'path' => base_path('favicon.ico'), 'enabled' => false, ], diff --git a/models/Settings.php b/models/Settings.php index 6384228..414467c 100644 --- a/models/Settings.php +++ b/models/Settings.php @@ -30,6 +30,10 @@ class Settings extends Model */ public $rules = []; + public $attachOne = [ + 'app_favicon' => 'System\Models\File' + ]; + /** * Initialize the seed data for this model. This only executes when the * model is first created or reset to default. diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index e08aa05..3935928 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -73,7 +73,6 @@ tabs: data-counter: 1 data-min: 100 data-max: 160 - meta_tags: tab: winter.seo::lang.models.meta.label_plural commentAbove: winter.seo::lang.models.meta.comment @@ -119,13 +118,17 @@ tabs: label: 'Enable favicon' span: full type: switch - default: 0 - favicon.path: + default: 1 + app_favicon: tab: 'Favicon' - commentAbove: 'Shortcut icon of your app' - type: mediafinder + label: 'Favicon' + comment: 'Shortcut icon of your website' + type: fileupload mode: image - span: left + fileTypes: png,gif,svg,ico + useCaption: true + imageHeight: 32 + imageWidth: 32 humans_txt.enabled: tab: winter.seo::lang.models.settings.humans_txt label: 'Enable humans.txt' diff --git a/routes.php b/routes.php index bfc1f85..da1dde9 100644 --- a/routes.php +++ b/routes.php @@ -23,6 +23,7 @@ } return Response::make($contents, 200, ['Content-Type' => 'text/plain']); }; + $settings = Settings::instance(); if(Settings::getOrDefault('humans_txt.enabled')) { Route::get('/humans.txt', fn() => $txtResponse('humans_txt')); } @@ -33,16 +34,9 @@ Route::get('/security.txt', fn() => $txtResponse('security_txt')); Route::get('/.well-known/security.txt', fn() => $txtResponse('security_txt')); } - if(Settings::getOrDefault('favicon.enabled')) { + if(Settings::getOrDefault('favicon.enabled') && $settings->app_favicon) { Route::get('favicon.ico', function() { - $faviconPath = media_path(Settings::get('favicon.path')); - try { - $favicon = new ImageResizer($faviconPath, 16, 16, ['mode'=>'crop']); - $favicon->resize(); - $outputPath = storage_path('app/'.$favicon->getPathToResizedImage()); - } catch(Exception $e) { - $outputPath = base_path($faviconPath); - } + $outputPath = $settings->app_favicon->getLocalPath(); return response()->file($outputPath, [ 'Content-Type'=> 'image/x-icon' ]); }); } From 44ab26c7754ab1f9120b46ed89b7accd6c48fe23 Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Fri, 14 Apr 2023 02:45:53 +0300 Subject: [PATCH 22/24] * removed from SEOTags component --- components/seotags/default.htm | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/components/seotags/default.htm b/components/seotags/default.htm index 30456db..3566341 100644 --- a/components/seotags/default.htm +++ b/components/seotags/default.htm @@ -1,10 +1,7 @@ {# <meta> tags #} {% for tagName, tagContent in __SELF__.getMetaTags() %} - {% if tagName == 'title' %} - <title>{{ tagContent }} - {% else %} - - {% endif %} + {# TODO: print title tag if not exists in page layout #} + {% endfor %} {# tags #} From 833e50f9814e57e61f36312e1df84c72de1b4b5a Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Fri, 14 Apr 2023 16:46:04 +0300 Subject: [PATCH 23/24] * custom models configuration added to config.php --- Plugin.php | 45 +++++++++++++++++++++++---------------------- config/config.php | 5 +++++ models/Settings.php | 3 +++ routes.php | 6 ++++-- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/Plugin.php b/Plugin.php index 2252432..1891a5c 100644 --- a/Plugin.php +++ b/Plugin.php @@ -7,6 +7,7 @@ use Winter\SEO\Classes\Meta; use Winter\SEO\Models\Settings; use Yaml; +use Config; /** * SEO Plugin Information File @@ -183,40 +184,40 @@ protected function populateGlobalTags() } /** - * Extends external plugins with support for SEO form fields + * Extends external plugins with the SEO form fields */ protected function extendExternalPlugins(): void { - $controllerModels = [ - \Winter\Blog\Controllers\Posts::class => [ - \Winter\Blog\Models\Post::class, - ], - \Winter\Blog\Controllers\Categories::class => [ - \Winter\Blog\Models\Category::class, - ], - ]; + $modelsToAttach = Config::get('winter.seo::models_to_attach', []); - Event::listen('backend.form.extendFieldsBefore', function (\Backend\Widgets\Form $widget) use ($controllerModels) { + Event::listen('backend.form.extendFieldsBefore', function (\Backend\Widgets\Form $widget) use ($modelsToAttach) { if($widget->isNested) return; - $controller = $widget->getController(); - $controllerClass = get_class($controller); $model = $widget->model; $modelClass = get_class($model); - $shouldExtend = property_exists($model, 'metadata_from') || - (isset($controllerModels[$controllerClass]) && - in_array($modelClass, $controllerModels[$controllerClass])); - + $shouldExtend = isset($modelsToAttach[$modelClass]) + || in_array($modelClass, $modelsToAttach) + || property_exists($model, 'metadata_from'); if(!$shouldExtend) return; - // Create link to proper widget tabs container - if($widget->secondaryTabs) { - $tabs = &$widget->secondaryTabs; + // Read config or fallback to default + if($model->metadata_from) { + $config = ['metadata_field' => $model->metadata_from]; } else { - $tabs = &$widget->tabs; + $config = is_array($modelsToAttach[$modelClass] ?? null) ? $modelsToAttach[$modelClass] : []; + } + $config = array_merge(['metadata_field' => 'metadata'], $config); + + // Reference to proper widget tabs container + if($widget->tabs || !$widget->secondaryTabs) { + $tabs = &$widget->tabs; + } else { + $tabs = &$widget->secondaryTabs; } // Model's metadata column can be set explicitly `public $metadata_from = 'column_name';` - $metadataField = strlen(trim($model->metadata_from)) ? $model->metadata_from : 'metadata'; + $metadataField = $config['metadata_field']; + + // TODO: not we gotta decide how to deal with the models that has no metadata field, pivot table? $form = Yaml::parseFile(plugins_path('winter/seo/models/meta/fields.yaml')); $tab = 'winter.seo::lang.models.meta.label'; @@ -227,7 +228,7 @@ protected function extendExternalPlugins(): void } $tabs['paneCssClass'][$tab] = 'padded-pane'; $tabs['icons'][$tab] = 'icon-magnifying-glass'; - $tabs['fields'] = array_merge($tabs['fields'], $fields); + $tabs['fields'] = array_merge($tabs['fields'] ?? [], $fields); }); } diff --git a/config/config.php b/config/config.php index 76e38b3..2dd4161 100644 --- a/config/config.php +++ b/config/config.php @@ -39,4 +39,9 @@ 'app_description' => null, ], + + 'models_to_attach' => [ + \Winter\Blog\Models\Post::class, + \Winter\Blog\Models\Category::class, + ], ]; diff --git a/models/Settings.php b/models/Settings.php index 414467c..69bc242 100644 --- a/models/Settings.php +++ b/models/Settings.php @@ -30,6 +30,9 @@ class Settings extends Model */ public $rules = []; + /** + * @var array One-to-one relations + */ public $attachOne = [ 'app_favicon' => 'System\Models\File' ]; diff --git a/routes.php b/routes.php index da1dde9..5bd2f83 100644 --- a/routes.php +++ b/routes.php @@ -18,12 +18,13 @@ Event::listen('system.beforeRoute', function () { $txtResponse = function ($key) { $contents = Settings::get($key); + if (empty($contents)) { - return Response::make((new \Cms\Classes\Controller())->run('404'), 404); + return Response::make((new \Cms\Classes\Controller())->run('404'), 404); } + return Response::make($contents, 200, ['Content-Type' => 'text/plain']); }; - $settings = Settings::instance(); if(Settings::getOrDefault('humans_txt.enabled')) { Route::get('/humans.txt', fn() => $txtResponse('humans_txt')); } @@ -34,6 +35,7 @@ Route::get('/security.txt', fn() => $txtResponse('security_txt')); Route::get('/.well-known/security.txt', fn() => $txtResponse('security_txt')); } + $settings = Settings::instance(); if(Settings::getOrDefault('favicon.enabled') && $settings->app_favicon) { Route::get('favicon.ico', function() { $outputPath = $settings->app_favicon->getLocalPath(); From a5cc6d69bb5380fd1b258064255234f95388629e Mon Sep 17 00:00:00 2001 From: Vadia Bezdushny Date: Fri, 14 Apr 2023 22:54:17 +0300 Subject: [PATCH 24/24] * favicon closure fix --- routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes.php b/routes.php index 5bd2f83..faac801 100644 --- a/routes.php +++ b/routes.php @@ -37,7 +37,7 @@ } $settings = Settings::instance(); if(Settings::getOrDefault('favicon.enabled') && $settings->app_favicon) { - Route::get('favicon.ico', function() { + Route::get('favicon.ico', function() use ($settings) { $outputPath = $settings->app_favicon->getLocalPath(); return response()->file($outputPath, [ 'Content-Type'=> 'image/x-icon' ]); });