diff --git a/app/Resources/views/error/index.html.twig b/app/Resources/views/error/index.html.twig new file mode 100644 index 00000000..0b217b9f --- /dev/null +++ b/app/Resources/views/error/index.html.twig @@ -0,0 +1,42 @@ +{% extends 'base.html.twig' %} + +{% block content %} +

Oops! An Error Occurred

+

The server returned a "{{ status_code }} {{ status_text }}".

+

If you see this page, it means your sandbox is not correctly set up. + Please see the README file in the sandbox root folder and if you can't figure out + what is wrong, ask us on freenode irc #symfonycmf or the mailinglist cmfusers@groups.google.com. +

+ +

If you are seeing this page as the result of an edit in the admin tool, please report what you were doing + to our ticket system, + so that we can add means to prevent this issue in the future. But to get things working again + for now, please just click here + to reload the data fixtures. +

+ Detected the following problem: + {{ exception.getMessage() }} + +

Suggested pages

+
+

+ This page is rendered by the + SuggestionProviderController + of the CmfSeoBundle. This way, usefull suggestions can be shown to your users. +

+ + + Read about this feature in the CMF documentation. + +
+ {% for group, list in best_matches if list is not empty %} +

{{ group|capitalize }}

+ + {% else %} +

No suggestions found

+ {% endfor %} +{% endblock %} diff --git a/app/Resources/views/sitemap/index.html.twig b/app/Resources/views/sitemap/index.html.twig new file mode 100644 index 00000000..538038de --- /dev/null +++ b/app/Resources/views/sitemap/index.html.twig @@ -0,0 +1,20 @@ +{% extends 'base.html.twig' %} + +{% block content %} +

Sitemap

+

+ The sitemap feature allows to give an overview of the content. + The content document decides whether it should be displayed on a sitemap or not. + The sitemap of the symfony-cmf sandbox is relatively flat because the content URL structure is flat. + If you have deeper nested content, the sitemap is organized along the nested structure.
+ This information is arranged for human users. For search engines, the sitemap also exists in + an xml format. +

+ +{% endblock %} diff --git a/app/Resources/views/sitemap/index.xml.twig b/app/Resources/views/sitemap/index.xml.twig new file mode 100644 index 00000000..751ed7df --- /dev/null +++ b/app/Resources/views/sitemap/index.xml.twig @@ -0,0 +1,17 @@ + + + {% for url in urls %} + + {{ url.location }} + {% if url.lastModification %} + {{ url.lastModification }} + {% endif %} + {{ url.changeFrequency }} + {% if url.alternateLocales is defined and url.alternateLocales|length > 0 %} + {% for locale in url.alternateLocales %} + + {% endfor %} + {% endif %} + + {% endfor %} + diff --git a/app/config/config.yml b/app/config/config.yml index 90bf130e..1a0a9820 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -22,7 +22,7 @@ framework: twig: debug: '%kernel.debug%' strict_variables: '%kernel.debug%' - exception_controller: 'FOS\RestBundle\Controller\ExceptionController::showAction' + exception_controller: cmf_seo.error.suggestion_provider.controller:listAction # Assetic Configuration assetic: @@ -113,6 +113,23 @@ cmf_seo: title: 'CMF Sandbox - %%content_title%%' description: 'The Content Management Framework. %%content_description%%' alternate_locale: ~ + error: + enable_parent_provider: true + enable_sibling_provider: true + templates: + html: ":error/index.html.twig" + exclusion_rules: + - { path: 'excluded' } + sitemap: + defaults: + default_change_frequency: never + templates: + xml: ':sitemap/index.xml.twig' + html: ':sitemap/index.html.twig' + configurations: + sitemap: ~ + frequent: + default_change_frequency: always cmf_menu: voters: diff --git a/app/config/routing.yml b/app/config/routing.yml index 1d7620fc..811b826a 100644 --- a/app/config/routing.yml +++ b/app/config/routing.yml @@ -52,3 +52,7 @@ block_cache: cmf_resource: resource: '@CmfResourceRestBundle/Resources/config/routing.yml' prefix: /admin + +sitemaps: + prefix: /sitemaps + resource: "@CmfSeoBundle/Resources/config/routing/sitemap.xml" diff --git a/app/config/services.yml b/app/config/services.yml index 188fedd1..690a5593 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -3,13 +3,6 @@ services: class: AppBundle\Controller\ContentController parent: cmf_content.controller - app.exception_listener: - class: AppBundle\EventListener\SandboxExceptionListener - calls: - - [setContainer, ['@service_container']] - tags: - - { name: kernel.event_subscriber } - app.twig.menu_extension: class: AppBundle\Twig\MenuExtension arguments: ['@knp_menu.helper', '@knp_menu.matcher'] diff --git a/src/AppBundle/DataFixtures/PHPCR/LoadMenuData.php b/src/AppBundle/DataFixtures/PHPCR/LoadMenuData.php index d9a7b435..1972f97c 100644 --- a/src/AppBundle/DataFixtures/PHPCR/LoadMenuData.php +++ b/src/AppBundle/DataFixtures/PHPCR/LoadMenuData.php @@ -85,6 +85,8 @@ public function load(ObjectManager $manager) $this->createMenuNode($manager, $seo, 'simple-seo-example', array('en' => 'Seo-Simple-Content'), $manager->find(null, "$content_path/simple-seo-example")); $this->createMenuNode($manager, $seo, 'demo-seo-extractor', array('en' => 'Seo-Extractor'), $manager->find(null, "$content_path/demo-seo-extractor")); $this->createMenuNode($manager, $seo, 'simple-seo-property', array('en' => 'Seo-Extra-Properties'), $manager->find(null, "$content_path/simple-seo-property")); + $this->createMenuNode($manager, $seo, 'seo-error-pages', array('en' => 'Seo-Error-Pages'), $manager->find(null, "$content_path/seo-error-pages")); + $this->createMenuNode($manager, $seo, 'seo-sitemap', 'Sitemap', null, '/sitemaps/sitemap.html'); $this->createMenuNode($manager, $main, 'routing-auto-item', array('en' => 'Auto routing example', 'de' => 'Auto routing beispiel', 'fr' => 'Auto routing exemple'), $manager->find(null, "$content_path/news/RoutingAutoBundle generates routes!")); diff --git a/src/AppBundle/DataFixtures/PHPCR/LoadNewsData.php b/src/AppBundle/DataFixtures/PHPCR/LoadNewsData.php index a263ab93..e646856e 100644 --- a/src/AppBundle/DataFixtures/PHPCR/LoadNewsData.php +++ b/src/AppBundle/DataFixtures/PHPCR/LoadNewsData.php @@ -51,6 +51,7 @@ public function load(ObjectManager $manager) See the routing auto configuration file to see how this works. EOT ); + $news->setIsVisibleForSitemap(true); $manager->persist($news); $manager->flush(); diff --git a/src/AppBundle/DataFixtures/PHPCR/LoadRoutingData.php b/src/AppBundle/DataFixtures/PHPCR/LoadRoutingData.php index a664aaac..ebd35860 100644 --- a/src/AppBundle/DataFixtures/PHPCR/LoadRoutingData.php +++ b/src/AppBundle/DataFixtures/PHPCR/LoadRoutingData.php @@ -115,6 +115,11 @@ public function load(ObjectManager $manager) $seo->setPosition($home, 'demo-seo-extractor'); $seo->setContent($manager->find(null, "$content_path/demo-seo-extractor")); $manager->persist($seo); + + $seo = new Route(); + $seo->setPosition($home, 'seo-error-pages'); + $seo->setContent($manager->find(null, "$content_path/seo-error-pages")); + $manager->persist($seo); } // demo features of routing diff --git a/src/AppBundle/DataFixtures/PHPCR/LoadStaticPageData.php b/src/AppBundle/DataFixtures/PHPCR/LoadStaticPageData.php index 8bbf35d3..d8d84e58 100644 --- a/src/AppBundle/DataFixtures/PHPCR/LoadStaticPageData.php +++ b/src/AppBundle/DataFixtures/PHPCR/LoadStaticPageData.php @@ -18,6 +18,7 @@ use PHPCR\Util\NodeHelper; use AppBundle\Document\DemoSeoContent; use Symfony\Cmf\Bundle\SeoBundle\Doctrine\Phpcr\SeoMetadata; +use Symfony\Cmf\Bundle\SeoBundle\SitemapAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\Yaml\Parser; @@ -95,6 +96,10 @@ public function load(ObjectManager $manager) $this->loadBlock($manager, $page, $name, $block); } } + + if ($page instanceof SitemapAwareInterface) { + $page->setIsVisibleForSitemap(true); + } } //add a loading for a simple seo aware page @@ -114,6 +119,7 @@ public function load(ObjectManager $manager) EOH ); $seoDemo->setParentDocument($parent); + $seoDemo->setIsVisibleForSitemap(true); $seoMetadata = new SeoMetadata(); $seoMetadata->setTitle('Simple seo example'); @@ -146,6 +152,7 @@ public function load(ObjectManager $manager) $seoMetadata->addExtraName('robots', 'index, follow'); $seoMetadata->addExtraProperty('og:title', 'extra title'); $seoDemo->setSeoMetadata($seoMetadata); + $seoDemo->setIsVisibleForSitemap(true); $manager->persist($seoDemo); $manager->bindTranslation($seoDemo, 'en'); diff --git a/src/AppBundle/Document/DemoClassContent.php b/src/AppBundle/Document/DemoClassContent.php index cc9897ee..5defb947 100644 --- a/src/AppBundle/Document/DemoClassContent.php +++ b/src/AppBundle/Document/DemoClassContent.php @@ -12,6 +12,7 @@ namespace AppBundle\Document; use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM; +use Symfony\Cmf\Bundle\SeoBundle\SitemapAwareInterface; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Cmf\Component\Routing\RouteReferrersReadInterface; @@ -20,7 +21,7 @@ * * @PHPCRODM\Document(referenceable=true) */ -class DemoClassContent implements RouteReferrersReadInterface +class DemoClassContent implements RouteReferrersReadInterface, SitemapAwareInterface { /** * to create the document at the specified location. read only for existing documents. @@ -62,6 +63,13 @@ class DemoClassContent implements RouteReferrersReadInterface */ public $routes; + /** + * @var bool + * + * @PHPCRODM\Field(type="boolean", property="visible_for_sitemap") + */ + private $isVisibleForSitemap; + public function getName() { return $this->name; @@ -117,4 +125,25 @@ public function getRoutes() { return $this->routes->toArray(); } + + /** + * Decision whether a document should be visible + * in sitemap or not. + * + * @param $sitemap + * + * @return bool + */ + public function isVisibleInSitemap($sitemap) + { + return $this->isVisibleForSitemap; + } + + /** + * @param bool $isVisibleForSitemap + */ + public function setIsVisibleForSitemap($isVisibleForSitemap) + { + $this->isVisibleForSitemap = $isVisibleForSitemap; + } } diff --git a/src/AppBundle/Document/DemoSeoContent.php b/src/AppBundle/Document/DemoSeoContent.php index facfa447..0fb257fd 100644 --- a/src/AppBundle/Document/DemoSeoContent.php +++ b/src/AppBundle/Document/DemoSeoContent.php @@ -15,6 +15,7 @@ use Symfony\Cmf\Bundle\ContentBundle\Doctrine\Phpcr\StaticContent; use Symfony\Cmf\Bundle\SeoBundle\SeoAwareInterface; use Symfony\Cmf\Bundle\SeoBundle\Doctrine\Phpcr\SeoMetadata; +use Symfony\Cmf\Bundle\SeoBundle\SitemapAwareInterface; /** * That example class uses the extractors for the creation of the SeoMetadata. @@ -23,7 +24,7 @@ * * @author Maximilian Berghoff */ -class DemoSeoContent extends StaticContent implements SeoAwareInterface +class DemoSeoContent extends StaticContent implements SeoAwareInterface, SitemapAwareInterface { /** * @var SeoMetadata @@ -32,6 +33,13 @@ class DemoSeoContent extends StaticContent implements SeoAwareInterface */ protected $seoMetadata; + /** + * @var bool + * + * @PHPCRODM\Field(type="boolean", property="visible_for_sitemap") + */ + private $isVisibleForSitemap; + public function __construct() { $this->seoMetadata = new SeoMetadata(); @@ -53,4 +61,25 @@ public function setSeoMetadata($seoMetadata) { $this->seoMetadata = $seoMetadata; } + + /** + * Decision whether a document should be visible + * in sitemap or not. + * + * @param $sitemap + * + * @return bool + */ + public function isVisibleInSitemap($sitemap) + { + return $this->isVisibleForSitemap; + } + + /** + * @param bool $isVisibleForSitemap + */ + public function setIsVisibleForSitemap($isVisibleForSitemap) + { + $this->isVisibleForSitemap = $isVisibleForSitemap; + } } diff --git a/src/AppBundle/Document/DemoSeoExtractor.php b/src/AppBundle/Document/DemoSeoExtractor.php index e596562f..6b2c5233 100644 --- a/src/AppBundle/Document/DemoSeoExtractor.php +++ b/src/AppBundle/Document/DemoSeoExtractor.php @@ -24,11 +24,7 @@ * * @author Maximilian Berghoff */ -class DemoSeoExtractor extends DemoSeoContent implements - TitleReadInterface, - DescriptionReadInterface, - OriginalUrlReadInterface, - KeywordsReadInterface +class DemoSeoExtractor extends DemoSeoContent implements TitleReadInterface, DescriptionReadInterface, OriginalUrlReadInterface, KeywordsReadInterface { /** * {@inheritdoc} diff --git a/src/AppBundle/EventListener/SandboxExceptionListener.php b/src/AppBundle/EventListener/SandboxExceptionListener.php deleted file mode 100644 index 8770dcf9..00000000 --- a/src/AppBundle/EventListener/SandboxExceptionListener.php +++ /dev/null @@ -1,82 +0,0 @@ -getException() instanceof NotFoundHttpException) { - return; - } - - if (!$this->container->has('doctrine_phpcr.odm.default_document_manager')) { - $error = 'Missing the service doctrine_phpcr.odm.default_document_manager.'; - } else { - try { - $om = $this->container->get('doctrine_phpcr.odm.default_document_manager'); - $doc = $om->find(null, $this->container->getParameter('cmf_menu.persistence.phpcr.menu_basepath')); - if ($doc) { - $error = 'Hm. No clue what goes wrong. Maybe this is a real 404?
'.$event->getException()->__toString().'
'; - } else { - $error = 'Did you load the fixtures? See README for how to load them. I found no node at menu_basepath: '.$this->container->getParameter('cmf_menu.persistence.phpcr.menu_basepath'); - } - } catch (RepositoryException $e) { - $error = 'There was an exception loading the document manager: '.$e->getMessage(). - "
\nMake sure you have a phpcr backend properly set up and running.
".
-                    $e->__toString().'
'; - } - } - // do not even trust the templating system to work - $response = new Response(" -

Sandbox

-

If you see this page, it means your sandbox is not correctly set up. - Please see the README file in the sandbox root folder and if you can't figure out - what is wrong, ask us on freenode irc #symfony-cmf or the mailinglist cmf-users@groups.google.com. -

- -

If you are seeing this page as the result of an edit in the admin tool, please report what you were doing - to our ticket system, - so that we can add means to prevent this issue in the future. But to get things working again - for now, please just getRequest()->getSchemeAndHttpHost()."/reload-fixtures.php\">click here - to reload the data fixtures. -

- Detected the following problem: $error -

- - "); - - $event->setResponse($response); - } - - public static function getSubscribedEvents() - { - return array( - KernelEvents::EXCEPTION => array('onKernelException', 0), - ); - } -} diff --git a/src/AppBundle/Resources/data/page.yml b/src/AppBundle/Resources/data/page.yml index 4b8c0f4c..ff048ef7 100644 --- a/src/AppBundle/Resources/data/page.yml +++ b/src/AppBundle/Resources/data/page.yml @@ -236,3 +236,38 @@ static: können einfach ausgelesen werden. Read about this feature in the CMF documentation (translation needed). + - + name: seo-error-pages + title: + en: SEO error pages + de: SEO optimierte Fehlerseiten + body: + en: | + Error pages with stack traces are good for development, but very bad end user experience and + a security risk. The default Symfony error handling produces a decent but very unhelpful + error page. +
+ With the CMF, we have the possibility to do better, particulary on not found pages. + The CmfSeoBundle defines the SuggestionProvider. Implementations provide possible alternatives + when content is not found. This could be pages with a parent path of the requested path + or other custom logic. +
+ To see this in action, try to open the following pages: + + de: | + Fehlerseiten mit Stack Traces von Exceptions sind gut zum entwickeln, aber schlecht für + die Benutzer der Seite und ein Sicherheitsproblem. Das Symfony Fehlerhandling produziert + vernünftige Fehlerseiten, die aber dem Benutzer nicht wirklich weiter helfen.
+ Mit dem CMF haben wir bessere Möglichkeiten, insbesondere wenn etwas nicht gefunden wird. + Das CmfSeoBundle definiert SuggestionProvider. Implementationen dieses providers liefern + mögliche Alternativen wenn ein Inhalt nicht gefunden wird. Zum Beispiel Nachbarn oder Eltern + der gewünschten URL.
+ Probiere es einfach aus, nimm Buchstaben aus der aktuellen URL oder klicke einen dieser Links: + + diff --git a/tests/Functional/HomepageTest.php b/tests/Functional/HomepageTest.php index 5823c8ac..65d806e4 100644 --- a/tests/Functional/HomepageTest.php +++ b/tests/Functional/HomepageTest.php @@ -38,7 +38,7 @@ public function testContents() $this->assertCount(1, $crawler->filter('h1:contains(Homepage)')); $this->assertCount(1, $crawler->filter('h2:contains("Welcome to the Symfony CMF Demo")')); - $this->assertCount(23, $crawler->filter('.panel-nav li')); + $this->assertCount(25, $crawler->filter('.panel-nav li')); } public function testJsonContents() diff --git a/tests/Functional/StaticPageTest.php b/tests/Functional/StaticPageTest.php index efb5c8a7..667f49a5 100644 --- a/tests/Functional/StaticPageTest.php +++ b/tests/Functional/StaticPageTest.php @@ -47,6 +47,7 @@ public function contentDataProvider() array('/demo/class', 'Controller by class'), array('/hello', 'Hello World!'), array('/en/about', 'Some information about us'), + 'sitemap' => array('/sitemaps/sitemap.html', 'Sitemap'), ); } @@ -65,4 +66,14 @@ public function testJson() ); $this->assertContains('"title":"The Team",', $response->getContent()); } + + public function testErrorPage() + { + $client = $this->createClient(); + $client->request('GET', '/en/company/tea'); + $this->assertStatusCode(404, $client); + + $response = $client->getResponse(); + $this->assertContains('Oops! An Error Occurred', $response->getContent()); + } } diff --git a/web/app.php b/web/app.php index bb47d3c9..638ad3a2 100644 --- a/web/app.php +++ b/web/app.php @@ -21,7 +21,6 @@ $kernel->loadClassCache(); //$kernel = new AppCache($kernel); - // When using the HttpCache, you need to call the method in your front // controller instead of relying on the configuration parameter //Request::enableHttpMethodParameterOverride(); diff --git a/web/assets/css/style.css b/web/assets/css/style.css index 9596d790..19586af6 100644 --- a/web/assets/css/style.css +++ b/web/assets/css/style.css @@ -26,6 +26,21 @@ body { padding-top:50px; } } .navbar-dropdown .dropdown-menu { margin-top:-5px; } +.cmf-sitemap .indent-1 { + margin-left: 20px; +} + +.cmf-sitemap .indent-2 { + margin-left: 40px; +} + +.cmf-sitemap .indent-3 { + margin-left: 60px; +} + +.cmf-sitemap .indent-4 { + margin-left: 80px; +}