├── LICENSE ├── README.md └── sitemap ├── SitemapPlugin.php ├── controllers └── Sitemap_SitemapController.php ├── library ├── Sitemap_ChangeFrequency.php └── Sitemap_Priority.php ├── models ├── Sitemap_AlternateUrlModel.php ├── Sitemap_BaseModel.php └── Sitemap_UrlModel.php ├── services └── SitemapService.php └── templates └── _settings.html /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Craft Plugins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > 3 | > **This plugin is no longer maintained.** 4 | > 5 | > We recommend the [SEOmatic plugin](https://plugins.craftcms.com/seomatic) or the [SEO plugin](https://plugins.craftcms.com/seo) instead. 6 | 7 | # Craft Sitemap 8 | 9 | A simple plugin for [Craft](http://craftcms.com) that generates a [sitemap.xml](http://www.sitemaps.org/) based on enabled sections. 10 | 11 | ![Settings](http://i.imgur.com/DhXTn2f.jpg) 12 | 13 | ## Installation 14 | 15 | 1. Copy the `sitemap/` folder into `craft/plugins/` 16 | 2. Go to Settings → Plugins and click the “Install” button next to “Sitemap” 17 | 18 | ## Usage 19 | 20 | Within the plugin settings, check the boxes in the “Enabled” column to include them in the sitemap. 21 | 22 | To view the output visit `/sitemap.xml`. 23 | 24 | ## Advanced 25 | 26 | This plugin exposes various [service methods](#service-methods), which can be used to add custom items to the sitemap through the [`renderSitemap` hook](#rendersitemap). Please read the official [‘Hooks and Events’ documentation](http://buildwithcraft.com/docs/plugins/hooks-and-events), if you’re not sure how this works. 27 | 28 | ### Hooks 29 | 30 | ##### `renderSitemap` 31 | 32 | Add a `renderSitemap` method to your plugin to add items via the various [service methods](#service-methods) listed below. 33 | 34 | Here’s an example plugin hook method with comments: 35 | 36 | ```php 37 | public function renderSitemap() 38 | { 39 | // Get an ElementCriteriaModel from the ElementsService 40 | $criteria = craft()->elements->getCriteria(ElementType::Entry); 41 | 42 | // Specify that we want entries within the ‘locations’ section 43 | $criteria->section = 'locations'; 44 | 45 | // Loop through any entries that were found 46 | foreach ($criteria->find() as $locationEntry) 47 | { 48 | // Here we’re building a path using the entry slug. 49 | // This might match a custom route you’ve defined that 50 | // should be included in the sitemap. 51 | $path = 'cars-for-sale-in-' . $locationEntry->slug; 52 | 53 | // Make sure that we’re using a full URL, not just the path. 54 | $url = UrlHelper::getSiteUrl($path); 55 | 56 | // For the sake of this example, we’re setting the $lastmod 57 | // value to the most recent time the location entry was 58 | // updated. You can pass any time using the DateTime class. 59 | $lastmod = $locationEntry->dateUpdated; 60 | 61 | // Add the URL to the sitemap 62 | craft()->sitemap->addUrl($url, $lastmod, Sitemap_ChangeFrequency::Daily, 0.5); 63 | } 64 | } 65 | ``` 66 | 67 | And here’s an example of the resulting element in the sitemap XML: 68 | 69 | ```xml 70 | 71 | http://example.com/cars-for-sale-in-scotland 72 | 2015-08-28T15:08:28+00:00 73 | 74 | ``` 75 | 76 | ### Service Methods 77 | 78 | There’s several service methods made available to add items to the sitemap. 79 | 80 | ##### `addUrl($loc, $lastmod, [$changefreq, [$priority]])` 81 | Adds a URL to the sitemap. 82 | 83 | ```php 84 | $loc = UrlHelper::getSiteUrl('special/route'); 85 | $lastmod = new DateTime('now'); 86 | craft()->sitemap->addUrl($loc, $lastmod, Sitemap_ChangeFrequency::Yearly, 0.1); 87 | ``` 88 | 89 | ##### `addElement(BaseElementModel $element, [$changefreq, [$priority]])` 90 | Adds an element to the sitemap. 91 | 92 | ```php 93 | $element = craft()->elements->getElementById(2); 94 | craft()->sitemap->addElement($element, Sitemap_ChangeFrequency::Daily, 1.0); 95 | ``` 96 | 97 | ##### `addSection(SectionModel $section, [$changefreq, [$priority]])` 98 | Adds all entries in the section to the sitemap. 99 | 100 | ```php 101 | $section = craft()->sections->getSectionByHandle('homepage'); 102 | craft()->sitemap->addSection($section, Sitemap_ChangeFrequency::Weekly, 1.0); 103 | ``` 104 | 105 | ##### `addCategoryGroup(CategoryGroupModel $categoryGroup, [$changefreq, [$priority]])` 106 | Adds all categories in the group to the sitemap. 107 | 108 | ```php 109 | $group = craft()->categories->getGroupByHandle('news'); 110 | craft()->sitemap->addCategoryGroup($group); 111 | ``` 112 | 113 | ##### `getElementUrlForLocale(BaseElementModel $element, $locale)` 114 | Gets a element URL for the specified locale. The locale must be enabled. 115 | 116 | ```php 117 | echo $element->url; 118 | // http://example.com/en/hello-world 119 | 120 | echo craft()->sitemap->getElementUrlForLocale($element, 'fr'); 121 | // http://example.com/fr/bonjour-monde 122 | ``` 123 | 124 | ##### `getUrlForLocale($path, $locale)` 125 | Gets a URL for the specified locale. The locale must be enabled. 126 | 127 | ```php 128 | echo UrlHelper::getSiteUrl('foo/bar'); 129 | // http://example.com/en/foo/bar 130 | 131 | echo craft()->sitemap->getUrlForLocale('foo/bar', 'fr'); 132 | // http://example.com/fr/foo/bar 133 | ``` 134 | 135 | #### Helper Classes 136 | 137 | ##### `Sitemap_ChangeFrequency` 138 | Enumeration of valid `changefreq` values. 139 | 140 | ```php 141 | Sitemap_ChangeFrequency::Always 142 | Sitemap_ChangeFrequency::Hourly 143 | Sitemap_ChangeFrequency::Daily 144 | Sitemap_ChangeFrequency::Weekly 145 | Sitemap_ChangeFrequency::Monthly 146 | Sitemap_ChangeFrequency::Yearly 147 | Sitemap_ChangeFrequency::Never 148 | ``` 149 | -------------------------------------------------------------------------------- /sitemap/SitemapPlugin.php: -------------------------------------------------------------------------------- 1 | array(), 46 | ); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} BaseSavableComponentType::getSettingsHtml() 51 | */ 52 | public function getSettingsHtml() 53 | { 54 | return craft()->templates->render('sitemap/_settings', array( 55 | 'sections' => craft()->sitemap->sectionsWithUrls, 56 | 'settings' => $this->settings, 57 | )); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} BaseSavableComponentType::prepSettings() 62 | */ 63 | public function prepSettings($input) 64 | { 65 | // We’re rewriting every time 66 | $settings = $this->defineSettings(); 67 | 68 | // Loop through valid sections 69 | foreach (craft()->sitemap->sectionsWithUrls as $section) { 70 | // Check if the section is enabled 71 | if ($input['enabled'][$section->id]) { 72 | // If it is, save the changefreq and priority values into settings 73 | $settings['sections'][$section->id] = array( 74 | 'changefreq' => $input['changefreq'][$section->id], 75 | 'priority' => $input['priority'][$section->id], 76 | ); 77 | } 78 | } 79 | 80 | // Return the parsed settings ready for the database 81 | return $settings; 82 | } 83 | 84 | /** 85 | * Registers the /sitemap.xml route. 86 | * 87 | * @return array 88 | */ 89 | public function registerSiteRoutes() 90 | { 91 | return array( 92 | 'sitemap.xml' => array( 93 | 'action' => 'sitemap/sitemap/output', 94 | ), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /sitemap/controllers/Sitemap_SitemapController.php: -------------------------------------------------------------------------------- 1 | sitemap->sitemap; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sitemap/library/Sitemap_ChangeFrequency.php: -------------------------------------------------------------------------------- 1 | hreflang = $hreflang; 16 | $this->href = $href; 17 | } 18 | 19 | /** 20 | * {@inheritdoc} Sitemap_UrlModel::getDomElement() 21 | */ 22 | public function getDomElement(\DOMDocument $document) 23 | { 24 | $element = $document->createElement('xhtml:link'); 25 | $element->setAttribute('rel', 'alternate'); 26 | $element->setAttribute('hreflang', $this->hreflang); 27 | $element->setAttribute('href', $this->href); 28 | 29 | return $element; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} BaseModel::rules() 34 | */ 35 | public function rules() 36 | { 37 | $rules = parent::rules(); 38 | $rules[] = array('href', 'CUrlValidator'); 39 | return $rules; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} BaseModel::defineAttributes() 44 | */ 45 | protected function defineAttributes() 46 | { 47 | return array( 48 | 'hreflang' => AttributeType::Locale, 49 | 'href' => AttributeType::Url, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sitemap/models/Sitemap_BaseModel.php: -------------------------------------------------------------------------------- 1 | config->get('devMode')) { 17 | foreach ($this->getAllErrors() as $attribute => $error) { 18 | throw new Exception(Craft::t($error)); 19 | } 20 | } 21 | 22 | return $validate; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sitemap/models/Sitemap_UrlModel.php: -------------------------------------------------------------------------------- 1 | loc = $loc; 25 | $this->lastmod = $lastmod; 26 | $this->changefreq = $changefreq; 27 | $this->priority = $priority; 28 | } 29 | 30 | /** 31 | * Add an alternate URL. 32 | * 33 | * @param string|LocaleModel $hreflang 34 | * @param string $href 35 | * 36 | * @return Sitemap_AlternateUrlModel 37 | */ 38 | public function addAlternateUrl($hreflang, $href) 39 | { 40 | $alternateUrl = new Sitemap_AlternateUrlModel($hreflang, $href); 41 | 42 | if ($alternateUrl->validate()) { 43 | $this->alternateUrls[$alternateUrl->hreflang] = $alternateUrl; 44 | } 45 | 46 | return $alternateUrl; 47 | } 48 | 49 | /** 50 | * Returns an array of assigned Sitemap_AlternateUrlModel instances. 51 | * 52 | * @return array 53 | */ 54 | public function getAlternateUrls() 55 | { 56 | return $this->alternateUrls; 57 | } 58 | 59 | /** 60 | * Returns true if there’s one or more alternate URLs, excluding the current locale. 61 | * 62 | * @return bool 63 | */ 64 | public function hasAlternateUrls() 65 | { 66 | return count(array_filter($this->alternateUrls, function ($alternateUrl) { 67 | return $alternateUrl->hreflang != craft()->language; 68 | })) > 0; 69 | } 70 | 71 | /** 72 | * Generates the relevant DOMElement instances. 73 | * 74 | * @param \DOMDocument $document 75 | * 76 | * @return \DOMElement 77 | */ 78 | public function getDomElement(\DOMDocument $document) 79 | { 80 | $url = $document->createElement('url'); 81 | 82 | $loc = $document->createElement('loc', $this->loc); 83 | $url->appendChild($loc); 84 | 85 | $lastmod = $document->createElement('lastmod', $this->lastmod->w3c()); 86 | $url->appendChild($lastmod); 87 | 88 | if ($this->changefreq) { 89 | $changefreq = $document->createElement('changefreq', $this->changefreq); 90 | $url->appendChild($changefreq); 91 | } 92 | 93 | if ($this->priority) { 94 | $priority = $document->createElement('priority', $this->priority); 95 | $url->appendChild($priority); 96 | } 97 | 98 | if ($this->hasAlternateUrls()) { 99 | foreach ($this->alternateUrls as $alternateUrl) { 100 | $link = $alternateUrl->getDomElement($document); 101 | $url->appendChild($link); 102 | } 103 | } 104 | 105 | return $url; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} BaseModel::setAttribute() 110 | */ 111 | public function setAttribute($name, $value) 112 | { 113 | if ($name == 'loc') { 114 | $this->addAlternateUrl(craft()->language, $value); 115 | } 116 | 117 | if ($name == 'lastmod') { 118 | if (!$value instanceof \DateTime) { 119 | try { 120 | $value = new DateTime($value); 121 | } catch (\Exception $e) { 122 | $message = Craft::t('“{object}->{attribute}” must be a DateTime object or a valid Unix timestamp.', array('object' => get_class($this), 'attribute' => $name)); 123 | throw new Exception($message); 124 | } 125 | } 126 | if (new DateTime() < $value) { 127 | $message = Craft::t('“{object}->{attribute}” must be in the past.', array('object' => get_class($this), 'attribute' => $name)); 128 | throw new Exception($message); 129 | } 130 | } 131 | 132 | return parent::setAttribute($name, $value); 133 | } 134 | 135 | /** 136 | * {@inheritdoc} BaseModel::rules() 137 | */ 138 | public function rules() 139 | { 140 | $rules = parent::rules(); 141 | $rules[] = array('loc', 'CUrlValidator'); 142 | return $rules; 143 | } 144 | 145 | /** 146 | * {@inheritdoc} BaseModel::defineAttributes() 147 | */ 148 | protected function defineAttributes() 149 | { 150 | return array( 151 | 'loc' => AttributeType::Url, 152 | 'lastmod' => AttributeType::DateTime, 153 | 'changefreq' => array(AttributeType::Enum, 'values' => Sitemap_ChangeFrequency::getConstants()), 154 | 'priority' => array(AttributeType::Enum, 'values' => Sitemap_Priority::getConstants()), 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /sitemap/services/SitemapService.php: -------------------------------------------------------------------------------- 1 | sections->allSections, function ($section) { 32 | return $section->isHomepage() || $section->urlFormat; 33 | }); 34 | } 35 | 36 | /** 37 | * Return the sitemap as a string. 38 | * 39 | * @return string 40 | */ 41 | public function getSitemap() 42 | { 43 | $settings = $this->pluginSettings; 44 | 45 | // Loop through and add the sections checked in the plugin settings 46 | foreach ($this->sectionsWithUrls as $section) { 47 | if (!empty($settings['sections'][$section->id])) { 48 | $changefreq = $settings['sections'][$section->id]['changefreq']; 49 | $priority = $settings['sections'][$section->id]['priority']; 50 | $this->addSection($section, $changefreq, $priority); 51 | } 52 | } 53 | 54 | // Hook: renderSitemap 55 | craft()->plugins->call('renderSitemap'); 56 | 57 | // Use DOMDocument to generate XML 58 | $document = new \DOMDocument('1.0', 'utf-8'); 59 | 60 | // Format XML output when devMode is active for easier debugging 61 | if (craft()->config->get('devMode')) { 62 | $document->formatOutput = true; 63 | } 64 | 65 | // Append a urlset node 66 | $urlset = $document->createElement('urlset'); 67 | $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); 68 | $urlset->setAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); 69 | $document->appendChild($urlset); 70 | 71 | // Loop through and append Sitemap_UrlModel elements 72 | foreach ($this->urls as $url) { 73 | $urlElement = $url->getDomElement($document); 74 | $urlset->appendChild($urlElement); 75 | } 76 | 77 | return $document->saveXML(); 78 | } 79 | 80 | /** 81 | * Adds a URL to the sitemap. 82 | * 83 | * @param string $loc 84 | * @param DateTime $lastmod 85 | * @param string $changefreq 86 | * @param string $priority 87 | * 88 | * @return Sitemap_UrlModel 89 | */ 90 | public function addUrl($loc, $lastmod, $changefreq = null, $priority = null) 91 | { 92 | $url = new Sitemap_UrlModel($loc, $lastmod, $changefreq, $priority); 93 | 94 | if ($url->validate()) { 95 | $this->urls[$url->loc] = $url; 96 | } 97 | 98 | return $url; 99 | } 100 | 101 | /** 102 | * Adds an element to the sitemap. 103 | * 104 | * @param BaseElementModel $element 105 | * @param string $changefreq 106 | * @param string $priority 107 | */ 108 | public function addElement(BaseElementModel $element, $changefreq = null, $priority = null) 109 | { 110 | $url = $this->addUrl($element->url, $element->dateUpdated, $changefreq, $priority); 111 | 112 | $locales = craft()->elements->getEnabledLocalesForElement($element->id); 113 | foreach ($locales as $locale) { 114 | $href = craft()->sitemap->getElementUrlForLocale($element, $locale); 115 | $url->addAlternateUrl($locale, $href); 116 | } 117 | } 118 | 119 | /** 120 | * Adds all entries in the section to the sitemap. 121 | * 122 | * @param SectionModel $section 123 | * @param string $changefreq 124 | * @param string $priority 125 | */ 126 | public function addSection(SectionModel $section, $changefreq = null, $priority = null) 127 | { 128 | $criteria = craft()->elements->getCriteria(ElementType::Entry); 129 | $criteria->section = $section; 130 | foreach ($criteria->find() as $element) { 131 | $this->addElement($element, $changefreq, $priority); 132 | } 133 | } 134 | 135 | /** 136 | * Adds all categories in the group to the sitemap. 137 | * 138 | * @param CategoryGroupModel $categoryGroup 139 | * @param string $changefreq 140 | * @param string $priority 141 | */ 142 | public function addCategoryGroup(CategoryGroupModel $categoryGroup, $changefreq = null, $priority = null) 143 | { 144 | $criteria = craft()->elements->getCriteria(ElementType::Category); 145 | $criteria->group = $categoryGroup; 146 | 147 | $categories = $criteria->find(); 148 | foreach ($categories as $category) { 149 | $this->addElement($category, $changefreq, $priority); 150 | } 151 | } 152 | 153 | /** 154 | * Gets a element URL for the specified locale. 155 | * 156 | * @param Element $element 157 | * @param string|LocaleModel $locale 158 | * 159 | * @return string 160 | */ 161 | public function getElementUrlForLocale(BaseElementModel $element, $locale) 162 | { 163 | $this->validateLocale($locale); 164 | 165 | $oldLocale = $element->locale; 166 | $oldUri = $element->uri; 167 | $element->locale = $locale; 168 | $element->uri = craft()->elements->getElementUriForLocale($element->id, $locale); 169 | $url = $element->getUrl(); 170 | $element->locale = $oldLocale; 171 | $element->uri = $oldUri; 172 | 173 | return $url; 174 | } 175 | 176 | /** 177 | * Gets a URL for the specified locale. 178 | * 179 | * @param string $path 180 | * @param string|LocaleModel $locale 181 | * 182 | * @return string 183 | */ 184 | public function getUrlForLocale($path, $locale) 185 | { 186 | $this->validateLocale($locale); 187 | 188 | // Get the site URL for the current locale 189 | $siteUrl = craft()->siteUrl; 190 | 191 | if (UrlHelper::isFullUrl($path)) { 192 | // Return $path if it’s a remote URL 193 | if (!stripos($path, $siteUrl)) { 194 | return $path; 195 | } 196 | 197 | // Remove the current locale siteUrl 198 | $path = str_replace($siteUrl, '', $path); 199 | } 200 | 201 | // Get the site URL for the specified locale 202 | $localizedSiteUrl = craft()->config->getLocalized('siteUrl', $locale); 203 | 204 | // Trim slahes 205 | $localizedSiteUrl = rtrim($localizedSiteUrl, '/'); 206 | $path = trim($path, '/'); 207 | 208 | return UrlHelper::getUrl($localizedSiteUrl.'/'.$path); 209 | } 210 | 211 | /** 212 | * Ensures that the requested locale is valid. 213 | * 214 | * @param string|LocaleModel $locale 215 | */ 216 | protected function validateLocale($locale) 217 | { 218 | if (!in_array($locale, craft()->i18n->siteLocales)) { 219 | throw new Exception(Craft::t('“{locale}” is not a valid site locale.', array('locale' => $locale))); 220 | } 221 | } 222 | 223 | /** 224 | * Gets the plugin settings. 225 | * 226 | * @return array 227 | */ 228 | protected function getPluginSettings() 229 | { 230 | $plugin = craft()->plugins->getPlugin('sitemap'); 231 | 232 | if (is_null($plugin)) { 233 | return array(); 234 | } 235 | 236 | return $plugin->settings; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /sitemap/templates/_settings.html: -------------------------------------------------------------------------------- 1 | {% import '_includes/forms' as forms %} 2 | 3 | {% set frequencyOptions = { 4 | 'always': 'always', 5 | 'hourly': 'hourly', 6 | 'daily': 'daily', 7 | 'weekly': 'weekly', 8 | 'monthly': 'monthly', 9 | 'yearly': 'yearly', 10 | 'never': 'never', 11 | } %} 12 | 13 | {% set priorityOptions = { 14 | '1.0': '1.0', 15 | '0.9': '0.9', 16 | '0.8': '0.8', 17 | '0.7': '0.7', 18 | '0.6': '0.6', 19 | '0.5': '0.5', 20 | '0.4': '0.4', 21 | '0.3': '0.3', 22 | '0.2': '0.2', 23 | '0.1': '0.1', 24 | '0.0': '0.0', 25 | } %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% for section in sections %} 38 | 39 | 46 | 49 | 56 | 59 | 66 | 73 | 74 | {% endfor %} 75 | 76 |
{{ 'Enabled'|t }}{{ 'Section'|t }} {{ 'URL Format'|t }}{{ 'Entries'|t }}{{ 'Change Frequency'|t }}{{ 'Priority'|t }}
40 | {{- forms.checkbox({ 41 | name: "enabled[#{section.id}]", 42 | value: true, 43 | checked: settings.sections[section.id] is defined 44 | }) -}} 45 | 47 | {{- section -}} 48 | 50 | {%- if section.isHomepage() -%} 51 |
52 | {%- else -%} 53 | {{ section.urlFormat }} 54 | {%- endif -%} 55 |
57 | {{- craft.entries.section(section).total() -}} 58 | 60 | {{- forms.selectField({ 61 | name: "changefreq[#{section.id}]", 62 | options: frequencyOptions, 63 | value: settings.sections[section.id].changefreq|default('weekly') 64 | }) -}} 65 | 67 | {{- forms.selectField({ 68 | name: "priority[#{section.id}]", 69 | options: priorityOptions, 70 | value: settings.sections[section.id].priority|default(section.isHomepage() ? '1.0' : '0.5') 71 | }) -}} 72 |
77 | --------------------------------------------------------------------------------