diff --git a/README.md b/README.md index cf3ad4b..30d38cc 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ All parameters are optional. ## TODO +- [ ] Add locale support for custom sitemap urls - [ ] Add redirect support for .htaccess & web.config to improve performance - [ ] Add hooks for plugin support - [ ] Include more fields in snippet (i.e. Type, Image, etc.) @@ -88,6 +89,10 @@ All parameters are optional. ## Changelog +### 1.2.3 +- Added Craft Commerce product types to Sitemap. +- Sitemap and Redirects are now stored in their own database tables, fixing the issue with the ~194 limit. + ### 1.1.3 - Fixed #15 - Fixed bug where global settings undefined on new install diff --git a/SeoPlugin.php b/SeoPlugin.php index 51763db..c6aa7bd 100644 --- a/SeoPlugin.php +++ b/SeoPlugin.php @@ -12,6 +12,8 @@ */ class SeoPlugin extends BasePlugin { + public static $commerceInstalled = false; + public function getName() { return 'SEO'; @@ -24,12 +26,12 @@ public function getDescription() public function getVersion() { - return '1.2.1'; + return '1.2.3'; } public function getSchemaVersion() { - return '0.0.11'; + return '0.0.12'; } public function getDeveloper() @@ -102,6 +104,18 @@ public function registerUserPermissions() public function init() { + $commerce = craft()->db->createCommand() + ->select('id') + ->from('plugins') + ->where("class = 'Commerce'") + ->queryScalar(); + + if ($commerce) { + SeoPlugin::$commerceInstalled = true; + } + + // TODO: On category / section update, update sitemap + if (craft()->request->isSiteRequest() && !craft()->request->isLivePreview()) { craft()->onException = function(\CExceptionEvent $event) diff --git a/controllers/SeoController.php b/controllers/SeoController.php index ae7dcf3..c944bf4 100644 --- a/controllers/SeoController.php +++ b/controllers/SeoController.php @@ -76,9 +76,10 @@ public function actionSitemapPage () ], // Sitemap - 'sitemap' => craft()->seo->getData('sitemap'), + 'sitemap' => craft()->seo_sitemap->getSitemap(), 'sections' => craft()->seo_sitemap->getValidSections(), 'categories' => craft()->seo_sitemap->getValidCategories(), + 'productTypes' => craft()->seo_sitemap->getValidProductTypes(), )); } @@ -92,8 +93,6 @@ public function actionRedirectsPage () craft()->templates->includeJsResource('seo/js/seo-settings.js'); craft()->templates->includeJs("new SeoSettings('{$namespace}', 'redirects');"); - $redirects = craft()->seo_redirect->getAllRedirects(); - $this->renderTemplate('seo/redirects', array( // Global 'namespace' => $namespace, @@ -106,7 +105,7 @@ public function actionRedirectsPage () ], // Redirecs - 'redirects' => $redirects, + 'redirects' => craft()->seo_redirect->getAllRedirects(), )); } diff --git a/controllers/Seo_SitemapController.php b/controllers/Seo_SitemapController.php index 220be22..9ca6b6f 100644 --- a/controllers/Seo_SitemapController.php +++ b/controllers/Seo_SitemapController.php @@ -10,14 +10,28 @@ class Seo_SitemapController extends BaseController public function init() { - $this->sitemap = craft()->seo->getData('sitemap'); + $this->sitemap = craft()->seo_sitemap->getSitemap(); parent::init(); } + public function actionSaveSitemap () + { + $this->requirePostRequest(); + + if (craft()->seo_sitemap->saveSitemap(craft()->request->getRequiredPost('data'))) { + craft()->userSession->setNotice(Craft::t('Sitemap updated.')); + } else { + craft()->userSession->setError(Craft::t('Couldn’t update sitemap.')); + } + + $this->redirectToPostedUrl(); + } + public function actionGenerate () { $sectionUrls = $this->_generateSections(); $categoryUrls = $this->_generateCategories(); + $productTypeUrls = $this->_generateProductTypes(); HeaderHelper::setContentTypeByExtension('xml'); HeaderHelper::setHeader(array('charset' => 'utf-8')); @@ -28,6 +42,7 @@ public function actionGenerate () $this->renderTemplate('_sitemap', array( 'sectionUrls' => $sectionUrls, 'categoryUrls' => $categoryUrls, + 'productTypeUrls' => $productTypeUrls, 'customUrls' => array_key_exists('customUrls', $this->sitemap) ? $this->sitemap['customUrls'] : [], )); } @@ -62,6 +77,23 @@ private function _generateCategories () return $urls; } + private function _generateProductTypes () + { + if (!SeoPlugin::$commerceInstalled) return array(); + + $urls = []; + + if (array_key_exists('productTypes', $this->sitemap) && !empty($this->sitemap['productTypes'])) { + foreach ($this->sitemap['productTypes'] as $productTypeId => $productType) + { + if ($productType['enabled']) + $urls = array_merge($urls, $this->_generateUrls($productTypeId, $productType, 'Commerce_Product')); + } + } + + return $urls; + } + private function _generateUrls ($id, $section, $elemType) { $urls = []; @@ -77,7 +109,7 @@ private function _generateUrls ($id, $section, $elemType) if (is_array($elem->locales) && count($elem->locales) > 1) { foreach ($elem->locales as $locale => $settings) { - $locale = $elemType == ElementType::Category ? $settings : $locale; + $locale = ($elemType == ElementType::Category) || ($elemType == 'Commerce_Product') ? $settings : $locale; if ($locale !== craft()->language) { $urlAlts[] = [ diff --git a/migrations/m160809_144009_seo_AddProductTypeToSitemapGroupColumn.php b/migrations/m160809_144009_seo_AddProductTypeToSitemapGroupColumn.php new file mode 100644 index 0000000..9c5042b --- /dev/null +++ b/migrations/m160809_144009_seo_AddProductTypeToSitemapGroupColumn.php @@ -0,0 +1,21 @@ +db->createCommand() + ->alterColumn('seo_sitemaps', 'group', array('values' => 'sections,categories,customUrls,productTypes', 'column' => 'enum', 'required' => true)); + + return true; + } +} diff --git a/records/Seo_SitemapRecord.php b/records/Seo_SitemapRecord.php index c9a5b70..060ab5c 100644 --- a/records/Seo_SitemapRecord.php +++ b/records/Seo_SitemapRecord.php @@ -22,7 +22,7 @@ public function primaryKey() public function defineAttributes() { return [ - 'group' => array(AttributeType::Enum, 'values' => "sections,categories,customUrls", 'required' => true), + 'group' => array(AttributeType::Enum, 'values' => "sections,categories,customUrls,productTypes", 'required' => true), 'url' => array(AttributeType::String, 'required' => true), 'frequency' => array(AttributeType::Enum, 'values' => "always,hourly,daily,weekly,monthly,yearly,never", 'required' => true), 'priority' => array(AttributeType::Number, 'required' => true, 'decimals' => 1), diff --git a/releases.json b/releases.json index 25810f6..6b16d5b 100644 --- a/releases.json +++ b/releases.json @@ -91,10 +91,11 @@ ] }, { - "version": "1.2.1", - "downloadUrl": "https://github.com/ethercreative/seo/archive/v1.2.1.zip", + "version": "1.2.3", + "downloadUrl": "https://github.com/ethercreative/seo/archive/v1.2.3.zip", "date": "2016-08-09T11:00:00-08:00", "notes": [ + "[Added] Added Craft Commerce product types to Sitemap.", "[Improvement] Sitemap and Redirects are now stored in their own database tables, fixing the issue with the ~194 limit." ] } diff --git a/resources/js/seo-settings.js b/resources/js/seo-settings.js index fbface4..dbf395c 100644 --- a/resources/js/seo-settings.js +++ b/resources/js/seo-settings.js @@ -86,6 +86,8 @@ SeoSettings.EditableTable.prototype.addRow = function () { var newRow = this.row.cloneNode(true); + newRow.innerHTML = newRow.innerHTML.replace(/\{i}/g, this.table.childNodes.length - 2); + newRow.getElementsByClassName('delete')[0].addEventListener('click', function () { newRow.remove(); self.rowCb(); diff --git a/resources/js/seo-settings.min.js b/resources/js/seo-settings.min.js index 5efcd0f..14c4bd5 100644 --- a/resources/js/seo-settings.min.js +++ b/resources/js/seo-settings.min.js @@ -1 +1 @@ -var SeoSettings=function(e,t){var a=this;switch(this.namespace=e,t){case"sitemap":new SeoSettings.EditableTable(this.namespace+"-customUrls",this.namespace+"-addCustomUrl");break;case"redirects":var i=document.getElementById(this.namespace+"-redirects"),n=document.getElementById(this.namespace+"-redirects-field");new SeoSettings.EditableTable(this.namespace+"-redirects",this.namespace+"-addRedirect",function(){a.redirectsForm(i,n)}),this.redirectsForm(i,n);break;case"settings":this.sitemapName(),new SeoSettings.SortableList("#"+this.namespace+"-readability")}};SeoSettings.prototype.sitemapName=function(){var e=document.getElementById(this.namespace+"-sitemapNameExample");document.getElementById(this.namespace+"-sitemapName").addEventListener("input",function(){e.textContent=this.value+".xml"})},SeoSettings.prototype.redirectsForm=function(e,t){function a(){var a=[];[].slice.call(e.querySelectorAll("tbody tr:not(.hidden)")).forEach(function(e){a.push({id:+e.getAttribute("data-id"),uri:e.querySelector('[data-name="redirects-uri"]').value.trim(),to:e.querySelector('[data-name="redirects-to"]').value.trim(),type:e.querySelector('[data-name="redirects-type"]').value})}),t.value=JSON.stringify(a).replace(/\\n/g,"\\n").replace(/\\'/g,"\\'").replace(/\\"/g,'\\"').replace(/\\&/g,"\\&").replace(/\\r/g,"\\r").replace(/\\t/g,"\\t").replace(/\\b/g,"\\b").replace(/\\f/g,"\\f")}a(),[].slice.call(document.querySelectorAll("[data-name]")).forEach(function(e){e.addEventListener("input",a)})},SeoSettings.EditableTable=function(e,t,a){var i=this;this.rowCb="function"==typeof a?a:function(){},this.table=document.getElementById(e).getElementsByTagName("tbody")[0],this.row=this.table.firstElementChild.cloneNode(!0),this.table.firstElementChild.remove(),this.row.classList.remove("hidden"),[].slice.call(this.table.getElementsByClassName("delete")).forEach(function(e){e.addEventListener("click",function(){e.parentNode.parentNode.remove(),i.rowCb()})}),document.getElementById(t).addEventListener("click",function(){i.addRow()})},SeoSettings.EditableTable.prototype.addRow=function(){var e=this,t=this.row.cloneNode(!0);t.getElementsByClassName("delete")[0].addEventListener("click",function(){t.remove(),e.rowCb()}),this.table.appendChild(t),this.rowCb()},SeoSettings.SortableList=Garnish.DragSort.extend({$readability:null,init:function(e,t){this.$readability=$(e);var a=this.$readability.children(".input").children(":not(.filler)");t=$.extend({},SeoSettings.SortableList.defaults,t),t.container=this.$readability.children(".input"),t.helper=$.proxy(this,"getHelper"),t.caboose=".readabiltiy-row",t.axis=Garnish.Y_AXIS,t.magnetStrength=4,t.helperLagBase=1.5,this.base(a,t)},getHelper:function(e){var t=$('
').appendTo(Garnish.$bod);return e.appendTo(t),t}},{defaults:{handle:".move",helperClass:"sortablelisthelper"}}); \ No newline at end of file +var SeoSettings=function(e,t){var i=this;switch(this.namespace=e,t){case"sitemap":new SeoSettings.EditableTable(this.namespace+"-customUrls",this.namespace+"-addCustomUrl");break;case"redirects":var a=document.getElementById(this.namespace+"-redirects"),n=document.getElementById(this.namespace+"-redirects-field");new SeoSettings.EditableTable(this.namespace+"-redirects",this.namespace+"-addRedirect",function(){i.redirectsForm(a,n)}),this.redirectsForm(a,n);break;case"settings":this.sitemapName(),new SeoSettings.SortableList("#"+this.namespace+"-readability")}};SeoSettings.prototype.sitemapName=function(){var e=document.getElementById(this.namespace+"-sitemapNameExample");document.getElementById(this.namespace+"-sitemapName").addEventListener("input",function(){e.textContent=this.value+".xml"})},SeoSettings.prototype.redirectsForm=function(e,t){function i(){var i=[];[].slice.call(e.querySelectorAll("tbody tr:not(.hidden)")).forEach(function(e){i.push({id:+e.getAttribute("data-id"),uri:e.querySelector('[data-name="redirects-uri"]').value.trim(),to:e.querySelector('[data-name="redirects-to"]').value.trim(),type:e.querySelector('[data-name="redirects-type"]').value})}),t.value=JSON.stringify(i).replace(/\\n/g,"\\n").replace(/\\'/g,"\\'").replace(/\\"/g,'\\"').replace(/\\&/g,"\\&").replace(/\\r/g,"\\r").replace(/\\t/g,"\\t").replace(/\\b/g,"\\b").replace(/\\f/g,"\\f")}i(),[].slice.call(document.querySelectorAll("[data-name]")).forEach(function(e){e.addEventListener("input",i)})},SeoSettings.EditableTable=function(e,t,i){var a=this;this.rowCb="function"==typeof i?i:function(){},this.table=document.getElementById(e).getElementsByTagName("tbody")[0],this.row=this.table.firstElementChild.cloneNode(!0),this.table.firstElementChild.remove(),this.row.classList.remove("hidden"),[].slice.call(this.table.getElementsByClassName("delete")).forEach(function(e){e.addEventListener("click",function(){e.parentNode.parentNode.remove(),a.rowCb()})}),document.getElementById(t).addEventListener("click",function(){a.addRow()})},SeoSettings.EditableTable.prototype.addRow=function(){var e=this,t=this.row.cloneNode(!0);t.innerHTML=t.innerHTML.replace(/\{i}/g,this.table.childNodes.length-2),t.getElementsByClassName("delete")[0].addEventListener("click",function(){t.remove(),e.rowCb()}),this.table.appendChild(t),this.rowCb()},SeoSettings.SortableList=Garnish.DragSort.extend({$readability:null,init:function(e,t){this.$readability=$(e);var i=this.$readability.children(".input").children(":not(.filler)");t=$.extend({},SeoSettings.SortableList.defaults,t),t.container=this.$readability.children(".input"),t.helper=$.proxy(this,"getHelper"),t.caboose=".readabiltiy-row",t.axis=Garnish.Y_AXIS,t.magnetStrength=4,t.helperLagBase=1.5,this.base(i,t)},getHelper:function(e){var t=$('
').appendTo(Garnish.$bod);return e.appendTo(t),t}},{defaults:{handle:".move",helperClass:"sortablelisthelper"}}); \ No newline at end of file diff --git a/services/Seo_RedirectService.php b/services/Seo_RedirectService.php index bab568a..341fdb3 100644 --- a/services/Seo_RedirectService.php +++ b/services/Seo_RedirectService.php @@ -68,6 +68,8 @@ public function saveAllRedirects ($data) $record->save(); } + // TODO: Add redirects to .htaccess / web.config to improve performance + return true; } diff --git a/services/Seo_SitemapService.php b/services/Seo_SitemapService.php index e9342ea..faa7751 100644 --- a/services/Seo_SitemapService.php +++ b/services/Seo_SitemapService.php @@ -5,6 +5,98 @@ class Seo_SitemapService extends BaseApplicationComponent { + public function getSitemap () + { + $sitemapRaw = Seo_SitemapRecord::model()->findAll(); + $sitemap = []; + + foreach ($sitemapRaw as $row) + { + if (!array_key_exists($row['group'], $sitemap)) $sitemap[$row['group']] = []; + + if ($row['group'] == 'customUrls') { + $sitemap[$row['group']][] = $row; + } else { + $sitemap[$row['group']][$row['url']] = $row; + } + } + + return $sitemap; + } + + public function saveSitemap ($data) + { + $oldSitemap = $this->getSitemap(); + $newSitemap = $data; + + // Delete removed rows + $newById = []; + $oldById = []; + + $newRecordsRaw = []; + + foreach ($newSitemap as $group => $rows) + { + foreach ($rows as $new) + { + $new['group'] = $group; + + if ($new['id'] != "-1") $newById[$new['id']] = $new; + else $newRecordsRaw[] = $new; + } + } + + $idsToDelete = []; + foreach ($oldSitemap as $group => $rows) + { + foreach ($rows as $old) + { + if (array_key_exists($old['id'], $newById)) { + $oldById[$old['id']] = $old; + } else { + $idsToDelete[] = $old['id']; + } + } + } + + if (!empty($idsToDelete)) { + craft()->db->createCommand()->delete('seo_sitemaps', array('in', 'id', $idsToDelete)); + } + + // Update current rows + foreach ($newById as $new) + { + $old = $oldById[$new['id']]; + + if ( + $old['url'] !== $new['url'] || + $old['frequency'] !== $new['frequency'] || + $old['priority'] !== $new['priority'] || + $old['enabled'] !== !!$new['enabled'] + ) { + $old->setAttribute('url', $new['url']); + $old->setAttribute('frequency', $new['frequency']); + $old->setAttribute('priority', $new['priority']); + $old->setAttribute('enabled', !!$new['enabled']); + $old->save(); + } + } + + // Add new rows + foreach ($newRecordsRaw as $new) + { + $record = new Seo_SitemapRecord(); + $record->setAttribute('url', $new['url']); + $record->setAttribute('frequency', $new['frequency']); + $record->setAttribute('priority', $new['priority']); + $record->setAttribute('enabled', !!$new['enabled']); + $record->setAttribute('group', $new['group']); + $record->save(); + } + + return true; + } + public function getValidSections () { return array_filter(craft()->sections->allSections, function ($section) { @@ -19,4 +111,13 @@ public function getValidCategories () }); } + public function getValidProductTypes () + { + if (!SeoPlugin::$commerceInstalled) return array(); + + return array_filter(craft()->commerce_productTypes->getAllProductTypes(), function ($productType) { + return $productType->hasUrls; + }); + } + } \ No newline at end of file diff --git a/templates/_sitemap.twig b/templates/_sitemap.twig index c8265b6..9c73c74 100644 --- a/templates/_sitemap.twig +++ b/templates/_sitemap.twig @@ -4,7 +4,7 @@ {# Sections #} - {% for section in sectionUrls %} + {% for section in sectionUrls -%} {{ url(section.url) }} {{ section.lastmod }} @@ -17,7 +17,7 @@ {# Categories #} - {% for category in categoryUrls %} + {% for category in categoryUrls -%} {{ url(category.url) }} {{ category.lastmod }} @@ -28,10 +28,23 @@ {% endfor %} + {# Products #} + + {% for category in productTypeUrls -%} + + {{ url(category.url) }} + {{ category.lastmod }} + {{ category.frequency }} + {{ category.priority }} + {% for alt in category.urlAlts %} + {% endfor %} + + {% endfor %} + {# Custom Urls #} - {% for custom in customUrls %} - {% if custom.enabled %} + {% for custom in customUrls -%} + {%- if custom.enabled -%} {{ url(custom.url) }} {{ custom.frequency }} diff --git a/templates/sitemap.twig b/templates/sitemap.twig index a10228f..3152764 100644 --- a/templates/sitemap.twig +++ b/templates/sitemap.twig @@ -5,8 +5,7 @@ {% import "_includes/forms" as forms %} {% block saveButton %} - - + {% endblock %} @@ -37,6 +36,7 @@ {% block content %} {% namespace namespace %} +
@@ -61,6 +61,8 @@ {{ section.name }} + + {{ section.isHomepage () ? '/' : section.urlFormat }} @@ -115,6 +117,8 @@ {{ category.name }} + + {{ category.locales[craft.locale].urlFormat }} @@ -145,6 +149,64 @@
+ {% if productTypes|length %} +
+
+ +
How should we handle each product type?
+
+
+ + + + + + + + + + + + + + {% for section in productTypes %} + + + + + + + + {% endfor %} + +
Product TypeURL FormatChange FrequencyPriorityEnabled?
+ {{ section.name }} + + + + {{ section.locales[craft.locale].urlFormat }} + {{ forms.selectField({ + name: "productTypes["~section.id~"][frequency]", + options: changeFrequencyOpts, + value: sitemap.productTypes[section.id].frequency|default('weekly') + }) }} + + {{ forms.selectField({ + name: "productTypes["~section.id~"][priority]", + options: priorityOpts, + value: sitemap.productTypes[section.id].priority|default('0.5') + }) }} + + {{ forms.checkbox({ + name: "productTypes["~section.id~"][enabled]", + value: true, + checked: (sitemap.productTypes is defined and sitemap.productTypes[section.id] is defined ? sitemap.productTypes[section.id].enabled : true) + }) }} +
+
+
+ {% endif %} +
@@ -164,9 +226,10 @@ - + + @@ -200,6 +263,7 @@ +