diff --git a/bundles/index.rst b/bundles/index.rst index 2ab2f84a..bca08a8e 100644 --- a/bundles/index.rst +++ b/bundles/index.rst @@ -15,6 +15,7 @@ Bundles routing_auto/index routing/index search/index + seo/index simple_cms/index tree_browser/index diff --git a/bundles/map.rst.inc b/bundles/map.rst.inc index 387d92fc..a8e67e34 100644 --- a/bundles/map.rst.inc +++ b/bundles/map.rst.inc @@ -59,6 +59,12 @@ library or they introduce a complete new concept. * :doc:`search/introduction` +* :doc:`seo/index` + + * :doc:`seo/introduction` + * :doc:`seo/seo_aware` + * :doc:`seo/extractors` + * :doc:`tree_browser/index` * :doc:`tree_browser/introduction` diff --git a/bundles/seo/extractors.rst b/bundles/seo/extractors.rst new file mode 100644 index 00000000..f6ba2155 --- /dev/null +++ b/bundles/seo/extractors.rst @@ -0,0 +1,127 @@ +Using Extractors to Retrieve the Seo Metadata +============================================= + +Instead of setting every value to the ``SeoMetadata`` manually, an extractor +can do the work for you. Extractors are executed when the content object +implements a specific interface. The method required by that interface will +return the value for the specific SEO data. The extractor will then update the +``SeoMetadata`` object for the current object with the returned value. + +Available Extractors +-------------------- + ++--------------------------------+---------------------------+----------------------------------------------+ +| ExtractorInterface | Method | Type | ++================================+===========================+==============================================+ +| ``DescriptionReadInterface`` | ``getSeoDescription()`` | Returns the meta description | ++--------------------------------+---------------------------+----------------------------------------------+ +| ``TitleReadInterface`` | ``getSeoTitle()`` | Returns the page title | ++--------------------------------+---------------------------+----------------------------------------------+ +| - | ``getTitle()`` | If the document has a ``getTitle()`` method, | +| | | it'll be used as the page title | ++--------------------------------+---------------------------+----------------------------------------------+ +| ``OriginalUrlReadInterface`` | ``getSeoOriginalUrl()`` | Returns a absolute url object to redirect to | +| | | or create a canonical link from | ++--------------------------------+---------------------------+----------------------------------------------+ +| ``OriginalRouteReadInterface`` | ``getSeoOriginalRoute()`` | Return a ``Route`` object to redirect to | +| | | or create a canonical link from | ++--------------------------------+---------------------------+----------------------------------------------+ +| ``ExtrasReadInterface`` | ``getSeoExtras()`` | Returns an associative array using | +| | | ``property``, ``http-equiv`` and ``name`` | +| | | as keys (see below from an example). | ++--------------------------------+---------------------------+----------------------------------------------+ + +.. note:: + + The interfaces life in the ``Symfony\Cmf\Bundle\SeoBundle\Extractor`` + namespace. + +An Example +---------- + +Assume you have an ``Article`` object and you want to use both the ``$title`` +and ``$date`` properties as page title and the ``$intro`` property as +description, you can implement both interfaces and your result will be:: + + // src/Acme/BlogBundle/Document/Article.php + namespace Acme\BlogBundle\Document; + + use Symfony\Cmf\Bundle\SeoBundle\Extractor\TitleReadInterface; + use Symfony\Cmf\Bundle\SeoBundle\Extractor\DescriptionReadInterface; + use Symfony\Cmf\Bundle\SeoBundle\Extractor\ExtrasReadInterface; + + class Article implements TitleReadInterface, DescriptionReadInterface, ExtraReadInterface + { + protected $title; + protected $publishDate; + protected $intro; + + // ... + public function getSeoTitle() + { + return $this->title.' ~ '.date($this->publishDate, 'm-Y'); + } + + public function getSeoDescription() + { + return $this->intro; + } + + public function getSeoExtras() + { + return array( + 'property' => array( + 'og:title' => $this->title, + 'og:description' => $this->description, + ), + ); + } + } + +Creating Your Own Extractor +--------------------------- + +To customize the extraction process, you can create your own extractor. Just +create a class which implements the ``SeoExtractorInterface`` and tag it with +``cmf_seo.extractor``: + +.. configuration-block:: + + .. code-block:: yaml + + parameters: + acme_demo.extractor.custom.class: Acme\DemoBundle\Extractor\MyCustomExtractor + + services: + acme_demo.extractor.custom: + class: "%acme_demo.extractor.custom.class%" + tags: + - { name: cmf_seo.extractor } + + .. code-block:: xml + + + + + Acme\DemoBundle\Extractor\MyCustomExtractor + + + + + + + + + + .. code-block:: php + + $container->addParameter( + 'acme_demo.extractor.custom.class', + 'Acme\DemoBundle\Extractor\MyCustomExtractor' + ); + + $container->register('acme_demo.extractor.custom', '%acme_demo.extractor.custom.class%') + ->addTag('cmf_seo.extractor') + ; diff --git a/bundles/seo/index.rst b/bundles/seo/index.rst new file mode 100644 index 00000000..2c3866c1 --- /dev/null +++ b/bundles/seo/index.rst @@ -0,0 +1,9 @@ +SeoBundle +========= + +.. toctree:: + :maxdepth: 2 + + introduction + seo_aware + extractors diff --git a/bundles/seo/introduction.rst b/bundles/seo/introduction.rst new file mode 100644 index 00000000..3454a0c7 --- /dev/null +++ b/bundles/seo/introduction.rst @@ -0,0 +1,343 @@ +SeoBundle +========= + + This bundle provides a layer on top of the `SonataSeoBundle`_, to make it + easier to collect SEO data from content documents. + +Installation +------------ + +You can install this bundle `with composer`_ using the +``symfony-cmf/seo-content-bundle`` package on `Packagist`_. + +Both the CmfSeoBundle and SonataSeoBundle must be registered in the +``AppKernel``:: + + // app/appKernel.php + + // ... + public function registerBundles() + { + $bundles = array( + // ... + new Sonata\SeoBundle\SonataSeoBundle(), + new Symfony\Cmf\Bundle\SeoBundle\CmfSeoBundle(), + ); + + // ... + + return $bundles; + } + +Usage +~~~~~ + +The simplest use of this bundle would be to just set some configuration to the +``sonata_seo`` configuration section and use the twig helper in your templates. + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + sonata_seo: + page: + title: Page's default title + metas: + name: + description: The default description of the page + keywords: default, sonata, seo + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('sonata_seo', array( + 'page' => array( + 'title' => 'Page's default title', + 'metas' => array( + 'name' => array( + 'description' => 'default description', + 'keywords' => 'default, key, other', + ), + ), + ), + )); + +The only thing to do now is to use the twig helper in your templates: + +.. code-block:: html+jinja + + + + + + {{ sonata_seo_title() }} + + {{ sonata_seo_metadatas() }} + + +

Some page body.

+ + + +This will render a page with the default title ("Page's default title") as +title element. The information definded for description and keywords will go +into the correct metatags. + +.. seealso:: + + To get a deeper look into the SonataSeoBundle, you should visit the + `Sonata documentation`_. + +Using SeoMetadata +----------------- + +The basic example shown above works perfectly without the CmfSeoBundle. The +CmfSeoBundle provides more extension points to configure the SEO data with +data from the document (e.g. a ``StaticContent`` document). This is done by +using SEO metadata. This is SEO data which will be used for a particular +document. This metadata can hold: + +* The title; +* The meta keywords; +* The meta description; +* The original URL (when more than one URL contains the same content). +* Anything else that uses the ```` tag with the ``property``, ``name`` + or ``http-equiv`` type (e.g. Open Graph data). + +The content object is retrieved from the request attributes. By default, it +uses the ``DynamicRouter::CONTENT_KEY`` constant when the +:doc:`RoutingBundle <../routing/introduction>` is installed. To change this, +or if you don't use the RoutingBundle, you can configure it with +``cmf_seo.content_key``. +This bundle provides two ways of using this metadata: + +#. Implementing the ``SeoAwareInterface`` and persisting the ``SeoMetadata`` + with the object. +#. Using the extractors, to extract the ``SeoMetadata`` from already existing + values (e.g. the title of the page). + +You can also combine both ways, even on the same document. In that case, the +persisted ``SeoMetadata`` can be changed by the extractors, to add or tweak +the current available SEO information. For instance, if you are writing a +``BlogPost`` class, you want the SEO keywords to be set to the tags/category +of the post and any additional tags set by the admin. + +Persisting the ``SeoMetadata`` with the document makes it easy to edit for the +admin, while using the extractors are perfect to easily use values from the +displayed content. + +Both ways are documented in detail in seperate sections: + +* :doc:`seo_aware` +* :doc:`extractors` + +Choosing the Original Route Pattern +----------------------------------- + +Search engines don't like it when you provide the same content under several +URLs. The CMF allows you to have several URLs for the same content if you need +that. There are two solutions to avoid penalties with search engines: + +* Create a canonical link that identifies the original URL: + ```` +* Define an "original url" and redirect all duplicate URLs to it. + +The ``SeoMetadata`` can be configured with the original URL for the current +page. By default, this bundle will create a canonical link for the page. If +you want to change that to redirect instead, you can set the +``original_route_pattern`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + cmf_seo: + original_route_pattern: redirect + + .. code-block:: xml + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension( + 'cmf_seo' => array( + 'original_route_pattern' => 'redirect', + ), + ); + +.. _bundles-seo-title-description-emplate: + +Defining a Title and Description Template +----------------------------------------- + +Most of the times, the title of a site has a static and a dynamic part. For +instance, "The title of the Page - Symfony". Here, "- Symfony" is static and +"The title of the Page" will be replaced by the current title. It would not be +nice if you had to add this static part to all your titles in documents. + +The CmfSeoBundle allows you to define a title and description template for +this reason. When using these settings, there are 2 placeholders available: +``%content_title%`` and ``%content_description%``. These will be replaced with +the title extracted from the content object and the description extracted from +the content object. + +.. caution:: + + The title and description template is only used when the title is not set + on the content object or when the content object is not available, + otherwise it'll use the default set by the SonataSeoBundle. You should + make sure that the defaults also follow the template. + +For instance, to configure the titles of the symfony.com pages, you would do: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + cmf_seo: + title: "%%content_title%% - Symfony" + + .. code-block:: xml + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('cmf_seo', array( + 'title' => '%%content_title%% - Symfony', + )); + +.. caution:: + + Be sure to escape the percentage characters by using a double percentage + character, otherwise the container will try to replace it with the value + of a container parameter. + +This syntax might look familiar if you have used the Translation component +before. And that's correct, under the hood the Translation component is used +to replace the placeholders with the correct values. This also means you get +Multi Language Support for free! + +For instance, you can do: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + cmf_seo: + title: seo.title + description: seo.description + + .. code-block:: xml + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('cmf_seo', array( + 'title' => 'seo.title', + 'description' => 'seo.description', + )); + +And then configure the translation messages: + +.. configuration-block:: + + .. code-block:: xml + + + + + + + + seo.title + %content_title% | Default title + + + seo.description + Default description. %content_description% + + + + + + .. code-block:: php + + // app/Resources/translations/messages.en.php + return array( + 'seo' => array( + 'title' => '%content_title% | Default title', + 'description' => 'Default description. %content_description', + ), + ); + + .. code-block:: yaml + + # app/Resources/translations/messages.en.yml + seo: + title: "%content_title% | Default title" + description: "Default description. %content_description%" + +.. tip:: + + You don't have to escape the percent characters here, since the + Translation loaders know how to deal with them. + +For changing the default translation domain (messages), you should use the +``cmf_seo.translation_domain`` setting: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + cmf_seo: + translation_domain: AcmeDemoBundle + + .. code-block:: xml + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension( + 'cmf_seo' => array( + 'translation_domain' => 'AcmeDemoBundle', + ), + ); + +Conclusion +---------- + +That's it! You have now created a SEO optimized website using nothing more +than a couple of simple settings. + +.. _`SonataSeoBundle`: https://github.com/sonata-project/SonataSeoBundle +.. _`with composer`: http://getcomposer.org +.. _`packagist`: https://packagist.org/packages/symfony-cmf/menu-bundle +.. _`Sonata documentation`: http://sonata-project.org/bundles/seo/master/doc/index.html diff --git a/bundles/seo/seo_aware.rst b/bundles/seo/seo_aware.rst new file mode 100644 index 00000000..ed81031c --- /dev/null +++ b/bundles/seo/seo_aware.rst @@ -0,0 +1,252 @@ +Saving the SeoMetadata in the Object +==================================== + +The ``SeoMetadata`` can be saved in the object, so you can persist it into the +database. This option gives admins the possiblity of changing the SEO data for +the document. + +In order to save the ``SeoMetadata`` in the object, the object should +implement the ``SeoAwareInterface``. This requires a getter and a setter for +the ``SeoMetadata``:: + + // src/Acme/SiteBundle/Document/Page.php + namespace Acme\SiteBundle\Document; + + use Symfony\Cmf\Bundle\SeoBundle\SeoAwareInterface; + + class Page implements SeoAwareInterface + { + protected $seoMetadata; + + // ... + public function getSeoMetadata() + { + return $this->seoMetadata; + } + + public function setSeoMetadata($metadata) + { + $this->seoMetadata = $metadata; + } + } + +Now you can set some SEO data for this ``Page`` using the metadata:: + + use Acme\SiteBundle\Document\Page; + use Symfony\Cmf\Bundle\SeoBundle\SeoMetadata; + + $page = new Page(); + // ... set some page properties + + $pageMetadata = new SeoMetadata(); + $pageMetadata->setDescription('A special SEO description.'); + $pageMetadata->setTags('seo, cmf, symfony'); + + $page->setSeoMetadata($pageMetadata); + +Doctrine PHPCR-ODM Integration +------------------------------ + +In order to easily persist the SeoMetadata when using Doctrine PHPCR-ODM, the +SeoBundle provides a special ``SeoMetadata`` document with the correct +mappings. This document should be mapped as a child of the content document. + +To be able to use this document, you have to enable the PHPCR persistence: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + cmf_seo: + persistence: + phpcr: true + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('cmf_seo', array( + 'persistence' => array( + 'phpcr' => true, + ), + )); + +.. tip:: + + This is not needed if you already enabled PHPCR on the ``cmf_core`` + bundle. See :doc:`the CoreBundle docs <../core/persistence>` for more + information. + +After you've enabled PHPCR, map your seoMetadata as a child: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Acme/SiteBundle/Document/Page.php + namespace Acme\SiteBundle\Document; + + use Symfony\Cmf\Bundle\SeoBundle\SeoAwareInterface; + use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR; + + /** + * @PHPCR\Document() + */ + class Page implements SeoAwareInterface + { + /** + * @PHPCR\Child + */ + protected $seoMetadata; + + // ... + } + + .. code-block:: yaml + + # src/Acme/SiteBundle/Resources/config/doctrine/Page.odm.yml + Acme\SiteBundle\Document\Page: + # ... + child: + # ... + seoMetadata: ~ + + .. code-block:: xml + + + + + + + + + + +And after that, you can use the +``Symfony\Cmf\Bundle\SeoBundle\Doctrine\Phpcr\SeoMetadata`` document:: + + // src/Acme/SiteBundle/DataFixture/PHPCR/LoadPageData.php + namespace Acme\SiteBundle\DataFixtures\PHPCR; + + use Acme\SiteBundle\Document\Page; + use Symfony\Cmf\Bundle\SeoBundle\Doctrine\Phpcr\SeoMetadata; + use Doctrine\Common\Persistence\ObjectManager; + use Doctrine\Common\DataFixtures\FixtureInterface; + + class LoadPageData implements FixtureInterface + { + public function load(ObjectManager $manager) + { + $page = new Page(); + // ... set some page properties + + $pageMetadata = new SeoMetadata(); + $pageMetadata->setDescription('A special SEO description.'); + $pageMetadata->setTags('seo, cmf, symfony'); + + $page->setSeoMetadata($pageMetadata); + + $manager->persist($page); + $manager->flush(); + } + } + +Doctrine ORM +------------ + +You can also use the Doctrine ORM with the CmfSeoBundle. You can just use the +``Symfony\Cmf\Bundle\SeoBundle\SeoMetadata`` class and map it as an +object: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Acme/SiteBundle/Entity/Page.php + namespace Acme\SiteBundle\Entity; + + use Symfony\Cmf\Bundle\SeoBundle\SeoAwareInterface; + use Doctrine\ORM\Mapping as ORM; + + /** + * @ORM\Entity() + */ + class Page implements SeoAwareInterface + { + /** + * @ORM\Column(type="object") + */ + protected $seoMetadata; + + // ... + } + + .. code-block:: yaml + + # src/Acme/SiteBundle/Resources/config/doctrine/Page.orm.yml + Acme\SiteBundle\Entity\Page: + # ... + fields: + # ... + seoMetadata: + type: object + + .. code-block:: xml + + + + + + + + + + + +You can also choose to put the ``SeoMetadata`` class into a seperate table. To +do this, you have to enable ORM support just like you enabled PHPCR enabled +above and add a OneToOne or ManyToOne relation between the content entity and +the ``SeoMetadata`` entity. + +Form Type +--------- + +The bundle also provides a special form type called ``seo_metadata``. This +form type can be used in forms to edit the ``SeoMetadata`` object. + +.. caution:: + + The bundles requires the `BurgovKeyValueFormBundle`_ when using the form + type. Make sure you install and enable it. + +Sonata Admin Integration +------------------------ + +Besides providing a form type, the bundle also provides a Sonata Admin +Extension. This extension adds a field for the ``SeoMetadata`` when an admin +edits an objec that implements the ``SeoAwareInterface`` in the Sonata Admin +panel. + +.. caution:: + + The Sonata Admin uses the Form Type provided by the CmfSeoBundle, make + sure you have the `BurgovKeyValueFormBundle`_ installed. + +.. _`BurgovKeyValueFormBundle`: https://github.com/Burgov/KeyValueFormBundle diff --git a/reference/configuration/routing.rst b/reference/configuration/routing.rst index b19b2ccc..1bf69722 100644 --- a/reference/configuration/routing.rst +++ b/reference/configuration/routing.rst @@ -365,7 +365,6 @@ phpcr ), )); - enabled ******* diff --git a/reference/configuration/seo.rst b/reference/configuration/seo.rst new file mode 100644 index 00000000..fc22956e --- /dev/null +++ b/reference/configuration/seo.rst @@ -0,0 +1,136 @@ +SeoBundle Configuration +======================= + +The SeoBundle takes care of the SEO information of a page and can be +configured under the ``cmf_seo`` key in your application configuration. When +using XML, you can use the ``http://cmf.symfony.com/schema/dic/seo`` +namespace. + +Configuration +------------- + +persistence +~~~~~~~~~~~ + +phpcr +""""" + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + cmf_seo: + dynamic: + persistence: + phpcr: + enabled: false + manager_name: ~ + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + $container->loadFromExtension('cmf_seo', array( + 'dynamic' => array( + 'persistence' => array( + 'phpcr' => array( + 'enabled' => false, + 'manager_name' => null, + ), + ), + ), + )); + +enabled +******* + +.. include:: partials/persistence_phpcr_enabled.rst.inc + +manager_name +************ + +.. include:: partials/persistence_phpcr_manager_name.rst.inc + +translation_domain +~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``messages`` + +The translation domain to use when translating the title and description +template. See :ref:`bundles-seo-title-description-emplate` for more +information. + + +title +~~~~~ + +**type**: ``string`` **default**: ``null`` + +The title template, read :ref:`here ` +about the usage. + +description +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` + +The description template, read :ref:`here ` +about the usage. + +original_route_pattern +~~~~~~~~~~~~~~~~~~~~~~ + +**type**: ``string`` **default**: ``canonical`` + +The original route strategy to use when multiple routes have the same content. +Can be one of ``canonical`` or ``redirect``. + +content_key +~~~~~~~~~~~ + +**type**: ``string`` **default**: ``null`` (or ``DynamicRouter::CONTENT_KEY`` when RoutingBundle is enabled) + +The name of the Request attribute which contains the content object. This is +required when the RoutingBundle is not enabled, otherwise it defaults to +``DynamicRouter::CONTENT_KEY`` (which evaluates to ``contentDocument``). + +sonata_admin_extension +~~~~~~~~~~~~~~~~~~~~~~ + +If set to ``true``, the Sonata Admin Extension provided by the SeoBundle is +activated. + +enabled +""""""" + +**type**: ``enum`` **valid values** ``true|false|auto`` **default**: ``auto`` + +If ``true``, the Sonata Admin Extension will be activated. If set to ``auto``, +it is activated only if the SonataPhpcrAdminBundle is present. + +If the :doc:`CoreBundle <../../bundles/core/index>` is registered, this will default to the value +of ``cmf_core.persistence.phpcr.use_sonata_admin``. + +form_group +"""""""""" + +**type**: ``string`` **default**: ``form.group_seo`` + +The name of the form group of the group provided by the Sonata Admin +Extension. diff --git a/reference/index.rst b/reference/index.rst index 90393c53..78fbd819 100644 --- a/reference/index.rst +++ b/reference/index.rst @@ -4,16 +4,17 @@ Reference .. toctree:: :hidden: - configuration/block.rst - configuration/content.rst - configuration/core.rst - configuration/create.rst - configuration/media.rst - configuration/menu.rst - configuration/routing.rst - configuration/search.rst - configuration/simple_cms.rst - configuration/tree_browser.rst - configuration/phpcr_odm.rst + configuration/block + configuration/content + configuration/core + configuration/create + configuration/media + configuration/menu + configuration/routing + configuration/search + configuration/seo + configuration/simple_cms + configuration/tree_browser + configuration/phpcr_odm .. include:: map.rst.inc diff --git a/reference/map.rst.inc b/reference/map.rst.inc index 3e453923..463bc2f1 100644 --- a/reference/map.rst.inc +++ b/reference/map.rst.inc @@ -8,6 +8,7 @@ * :doc:`configuration/menu` * :doc:`configuration/routing` * :doc:`configuration/search` + * :doc:`configuration/seo` * :doc:`configuration/simple_cms` * :doc:`configuration/tree_browser` * :doc:`configuration/phpcr_odm`