From 5971f772fa5c5689c9c61815a287aa9dc338a6ac Mon Sep 17 00:00:00 2001 From: Jonathan Carey Date: Fri, 15 Mar 2024 17:14:19 +0000 Subject: [PATCH 1/4] Fix localisation sync for Meta Title in multi-site setups. #60 --- .../fieldtypes/GooglePreviewFieldtype.vue | 14 +++- .../fieldtypes/MetaTitleFieldtype.vue | 27 ++++++- .../AardvarkSeoGooglePreviewFieldtype.php | 1 + .../AardvarkSeoMetaTitleFieldtype.php | 4 +- src/Parsers/PageDataParser.php | 72 +++++++++++++++++-- 5 files changed, 108 insertions(+), 10 deletions(-) diff --git a/resources/js/components/fieldtypes/GooglePreviewFieldtype.vue b/resources/js/components/fieldtypes/GooglePreviewFieldtype.vue index 8f97718..6f9cbb7 100644 --- a/resources/js/components/fieldtypes/GooglePreviewFieldtype.vue +++ b/resources/js/components/fieldtypes/GooglePreviewFieldtype.vue @@ -16,21 +16,33 @@ computed: { previewParts() { + const state = this.$store.state.publish[this.storeName]; + const { meta_title, meta_description, slug, title, } = state.values; + const { site_name, site_url, title_separator, + default_locale, } = this.meta; + // Initialise pageTitle with meta_title if available, otherwise combine title with site name and separator. + let pageTitle = meta_title || `${title}${site_name ? ` ${title_separator} ${site_name}` : ''}`; + + // Override pageTitle for non-default locales without a localised meta_title, using title and optionally site name and separator. + if (state && state.localizedFields && default_locale !== state.site && !state.localizedFields.includes('meta_title')) { + pageTitle = `${title}${site_name ? ` ${title_separator} ${site_name}` : ''}`; + } + return { - title: meta_title || `${title} ${title_separator} ${site_name}`, + title: pageTitle, url: `${site_url}/${slug}`, description: meta_description } diff --git a/resources/js/components/fieldtypes/MetaTitleFieldtype.vue b/resources/js/components/fieldtypes/MetaTitleFieldtype.vue index e2b8fbe..cbbbf3d 100644 --- a/resources/js/components/fieldtypes/MetaTitleFieldtype.vue +++ b/resources/js/components/fieldtypes/MetaTitleFieldtype.vue @@ -2,7 +2,7 @@
- +
@@ -21,7 +21,30 @@ methods: { generatePlaceholder() { const state = this.$store.state.publish[this.storeName]; - return `${state.values.title || ''} ${this.meta.title_separator} ${this.meta.site_name}` + return this.meta.site_name + ? `${state.values.title || ''} ${this.meta.title_separator} ${this.meta.site_name}` + : state.values.title || ''; + }, + /** + * Generates the title based on localisation fields. + * If the current site is not the default locale and the 'meta_title' is not localised, + * it returns an empty string; otherwise, it returns the provided value. + * + * @param {string} value - The original title value to potentially return. + * @return {string} - The localised title or an empty string. + */ + generateTitle(value) { + // Access the publish state from the Vuex store + const state = this.$store.state.publish[this.storeName]; + + // Check if the state exists, has localizedFields, and if the current site is not the default locale + if (state && state.localizedFields && this.meta.default_locale !== state.site) { + // Return the value only if 'meta_title' is a localised field + return state.localizedFields.includes('meta_title') ? value : ''; + } + + // Return the provided value as default + return value; }, validateMeta(length) { let validation; diff --git a/src/Fieldtypes/AardvarkSeoGooglePreviewFieldtype.php b/src/Fieldtypes/AardvarkSeoGooglePreviewFieldtype.php index 4759657..a159590 100644 --- a/src/Fieldtypes/AardvarkSeoGooglePreviewFieldtype.php +++ b/src/Fieldtypes/AardvarkSeoGooglePreviewFieldtype.php @@ -21,6 +21,7 @@ public function preload() 'site_name' => $data->get('site_name', ''), 'site_url' => $site->absoluteUrl(), 'title_separator' => $data->get('title_separator', '|'), + 'default_locale' => Site::default()->handle(), ]; } } diff --git a/src/Fieldtypes/AardvarkSeoMetaTitleFieldtype.php b/src/Fieldtypes/AardvarkSeoMetaTitleFieldtype.php index 989944a..229d510 100644 --- a/src/Fieldtypes/AardvarkSeoMetaTitleFieldtype.php +++ b/src/Fieldtypes/AardvarkSeoMetaTitleFieldtype.php @@ -13,13 +13,15 @@ class AardvarkSeoMetaTitleFieldtype extends Fieldtype /** * Load the global seo settings from storage */ - public function preload() + public function preload(): array { $site = Site::selected(); $data = AardvarkStorage::getYaml('general', $site, true); + return [ 'site_name' => $data->get('site_name', ''), 'title_separator' => $data->get('title_separator', '|'), + 'default_locale' => Site::default()->handle(), ]; } } diff --git a/src/Parsers/PageDataParser.php b/src/Parsers/PageDataParser.php index f0d14f4..c28a3f2 100644 --- a/src/Parsers/PageDataParser.php +++ b/src/Parsers/PageDataParser.php @@ -11,6 +11,7 @@ use WithCandour\AardvarkSeo\Blueprints\CP\SocialSettingsBlueprint; use WithCandour\AardvarkSeo\Blueprints\CP\OnPageSeoBlueprint; use WithCandour\AardvarkSeo\Facades\AardvarkStorage; +use Statamic\Facades\Entry; /** * Helper class for parsing on-page data @@ -158,21 +159,80 @@ public static function getSettingsBlueprintWithValues($ctx, $type, $blueprint_cl */ public static function generatePageTitle($data, $ctx) { - if ($data->get('meta_title') && $data->get('meta_title')->raw()) { + // Check if an ID exists in the provided data. + if ($data->get('id')) { + + // Retrieve the default site's configuration to compare locales. + $defaultSite = Site::default(); + // Get the handle (identifier) of the default site. + $defaultLocale = $defaultSite->handle(); + // Find the entry associated with the given ID. + $entry = Entry::find($data->get('id')->raw()); + + // Ensure the entry exists and its locale does not match the default site's locale. + if ($entry && method_exists($entry, 'locale') && $defaultLocale != $entry->locale()) { + + // Check if the entry has a 'meta_title' set. + if ($entry->has('meta_title') && !empty ($entry->get('meta_title'))) { + // If 'meta_title' exists, parse it with context and return. + return Parse::template($entry->get('meta_title'), $ctx); + } + + // If there's no 'meta_title', check for a regular 'title' field as a fallback. + if ($entry->has('title') && !empty ($entry->get('title'))) { + + $title = self::constructPageTitle($ctx, $entry->get('title')); + + // Parse and return the 'title' with context. + return Parse::template($title, $ctx); + } + + // If the localized entry lacks a 'title' or 'meta_title', fall back to the 'title' from the original data. + if ($data->has('title') && !empty ($data->get('title'))) { + + // Construct the page title by combining the entry title with the site name and title separator, only if the site name is set. + $title = self::constructPageTitle($ctx, $data->get('title')); + + // Parse and return the 'title' with context. + return Parse::template($title, $ctx); + } + } + } + + if ($data->has('meta_title') && !empty ($data->get('meta_title')->raw())) { return Parse::template($data->get('meta_title'), $ctx); } if ($data->get('response_code') === 404) { $data->put('title', '404'); } + $title = self::constructPageTitle($ctx, $data->get('title')); + + return $title; + } + + /** + * Constructs the page title by combining the entry title with the site name and title separator. + * + * @param string $title The primary title part. + * @param Illuminate\Support\Collection $storage The storage collection containing site configuration. + * @return string The constructed page title. + */ + protected static function constructPageTitle($ctx, $title): string { $storage = self::getSettingsBlueprintWithValues($ctx, 'general', new GeneralSettingsBlueprint()); + $titleParts = [$title]; + + $siteName = $storage->get('site_name'); + if ($siteName !== null) { + $siteNameValue = $siteName->raw(); + if (!empty($siteNameValue)) { + $titleParts[] = $storage->get('title_separator'); + $titleParts[] = $siteNameValue; + } + } - return implode(' ', [ - $data->get('title'), - $storage->get('title_separator'), - $storage->get('site_name'), - ]); + return implode(' ', array_filter($titleParts)); } /** From 46c728ad0fc6e452afef026648b3dec2dad78f81 Mon Sep 17 00:00:00 2001 From: Jonathan Carey Date: Sun, 17 Mar 2024 06:59:28 +0000 Subject: [PATCH 2/4] Working prototype. #60 --- .../fieldtypes/MetaTitleFieldtype.vue | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/resources/js/components/fieldtypes/MetaTitleFieldtype.vue b/resources/js/components/fieldtypes/MetaTitleFieldtype.vue index cbbbf3d..c5b2164 100644 --- a/resources/js/components/fieldtypes/MetaTitleFieldtype.vue +++ b/resources/js/components/fieldtypes/MetaTitleFieldtype.vue @@ -2,7 +2,7 @@
- +
@@ -14,11 +14,53 @@ import MetaDataAnalyser from './mixins/MetaDataAnalyser'; export default { + mixins: [Fieldtype, MetaDataAnalyser], inject: ['storeName'], + data() { + return { + isFocussed: false, + hasSyncedJustChanged: false, + }; + }, + + created() { + this.processed = 0; // This is a plain JavaScript property, not a reactive data property + }, + + computed: { + /** + * Computes the synchronization state based on whether the current handle + * is included in the localizedFields array from the Vuex store state. + * @returns {Boolean} The sync state. + */ + isSynced() { + const state = this.$store.state.publish[this.storeName]; + if (state && state.localizedFields) { + return !state.localizedFields.includes(this.config.handle); + } + return false; + }, + }, + + watch: { + /** + * Watches for changes in the `isSynced` computed property to perform + * actions or log its changes. + */ + isSynced(newVal, oldVal) { + if (newVal !== oldVal) { + this.hasSyncedJustChanged = true; + } + }, + }, + methods: { + toggleFocus(focusState) { + this.isFocussed = focusState; + }, generatePlaceholder() { const state = this.$store.state.publish[this.storeName]; return this.meta.site_name @@ -34,11 +76,28 @@ * @return {string} - The localised title or an empty string. */ generateTitle(value) { + + this.processed++; + // Access the publish state from the Vuex store const state = this.$store.state.publish[this.storeName]; - // Check if the state exists, has localizedFields, and if the current site is not the default locale - if (state && state.localizedFields && this.meta.default_locale !== state.site) { + if(state && state.localizedFields && this.meta.default_locale !== state.site) { + + if (!this.isSynced && this.hasSyncedJustChanged && !this.isFocussed) { + this.hasSyncedJustChanged = false; + state.values.meta_title = state.values.title; + return state.values.title; + } + + if(this.isSynced && this.hasSyncedJustChanged && !this.isFocussed && this.processed > 0) { + state.values.meta_title = ''; + } + + if(this.isSynced && !this.hasSyncedJustChanged) { + state.values.meta_title = ''; + } + // Return the value only if 'meta_title' is a localised field return state.localizedFields.includes('meta_title') ? value : ''; } From 96afaf2ae4db3b86c265629e1623264c8ddd7237 Mon Sep 17 00:00:00 2001 From: Jonathan Carey Date: Sun, 17 Mar 2024 07:07:18 +0000 Subject: [PATCH 3/4] Removed processed count. #60. --- .../components/fieldtypes/MetaTitleFieldtype.vue | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/resources/js/components/fieldtypes/MetaTitleFieldtype.vue b/resources/js/components/fieldtypes/MetaTitleFieldtype.vue index c5b2164..7405e20 100644 --- a/resources/js/components/fieldtypes/MetaTitleFieldtype.vue +++ b/resources/js/components/fieldtypes/MetaTitleFieldtype.vue @@ -21,15 +21,11 @@ data() { return { - isFocussed: false, + isFocused: false, hasSyncedJustChanged: false, }; }, - created() { - this.processed = 0; // This is a plain JavaScript property, not a reactive data property - }, - computed: { /** * Computes the synchronization state based on whether the current handle @@ -59,7 +55,7 @@ methods: { toggleFocus(focusState) { - this.isFocussed = focusState; + this.isFocused = focusState; }, generatePlaceholder() { const state = this.$store.state.publish[this.storeName]; @@ -77,20 +73,18 @@ */ generateTitle(value) { - this.processed++; - // Access the publish state from the Vuex store const state = this.$store.state.publish[this.storeName]; if(state && state.localizedFields && this.meta.default_locale !== state.site) { - if (!this.isSynced && this.hasSyncedJustChanged && !this.isFocussed) { + if (!this.isSynced && this.hasSyncedJustChanged && !this.isFocused) { this.hasSyncedJustChanged = false; state.values.meta_title = state.values.title; return state.values.title; } - if(this.isSynced && this.hasSyncedJustChanged && !this.isFocussed && this.processed > 0) { + if(this.isSynced && this.hasSyncedJustChanged && !this.isFocused) { state.values.meta_title = ''; } From d9a103c99eff9b77c00073905448650c766816c0 Mon Sep 17 00:00:00 2001 From: Jonathan Carey Date: Sun, 17 Mar 2024 07:34:27 +0000 Subject: [PATCH 4/4] updated and added new comments. #60 --- .../components/fieldtypes/MetaTitleFieldtype.vue | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/resources/js/components/fieldtypes/MetaTitleFieldtype.vue b/resources/js/components/fieldtypes/MetaTitleFieldtype.vue index 7405e20..14a7ff8 100644 --- a/resources/js/components/fieldtypes/MetaTitleFieldtype.vue +++ b/resources/js/components/fieldtypes/MetaTitleFieldtype.vue @@ -66,29 +66,37 @@ /** * Generates the title based on localisation fields. * If the current site is not the default locale and the 'meta_title' is not localised, - * it returns an empty string; otherwise, it returns the provided value. + * it returns an empty string; otherwise, it returns the provided value. It also handles the sync logic by updating + * the meta title based on the fields current sync state. * * @param {string} value - The original title value to potentially return. - * @return {string} - The localised title or an empty string. + * @return {string} - The localised title based on the current site's locale and field's sync state, or an empty string. */ generateTitle(value) { - // Access the publish state from the Vuex store const state = this.$store.state.publish[this.storeName]; + // Check if state is defined, and if the current site has localizedFields and is not the default locale. if(state && state.localizedFields && this.meta.default_locale !== state.site) { + // If the field is not synced and has just been changed and is not currently focused, + // reset the hasSyncedJustChanged flag and update the meta title to match the page title. if (!this.isSynced && this.hasSyncedJustChanged && !this.isFocused) { this.hasSyncedJustChanged = false; + // ToDo: Best practice: dispatch an action or commit a mutation instead state.values.meta_title = state.values.title; return state.values.title; } + // If the field is synced and has just been changed and is not currently focused, clear the meta title. if(this.isSynced && this.hasSyncedJustChanged && !this.isFocused) { + // ToDo: Best practice: dispatch an action or commit a mutation instead state.values.meta_title = ''; } + // If the field is synced and has not just changed, ensure the meta title remains cleared. if(this.isSynced && !this.hasSyncedJustChanged) { + // ToDo: Best practice: dispatch an action or commit a mutation instead state.values.meta_title = ''; } @@ -99,6 +107,7 @@ // Return the provided value as default return value; }, + validateMeta(length) { let validation; switch (true) {