From 6ad59a1162d443ab110e4569e0885d2316af0fb1 Mon Sep 17 00:00:00 2001 From: aritz Date: Thu, 5 Mar 2026 16:17:58 +0100 Subject: [PATCH 1/5] ADD canonical url --- .../translations/sfs_cms_contents.en.yaml | 7 +++++ .../translations/sfs_cms_contents.es.yaml | 7 +++++ cms/layouts/default/render.html.twig | 6 +++- .../doctrine-mapping/entities/Content.orm.xml | 4 +++ config/validation/ContentInterface.yaml | 4 +++ .../ContentEntityTransformer.php | 11 +++++++ src/Form/Admin/Content/ContentCreateForm.php | 11 +++++++ src/Form/Admin/Content/ContentUpdateForm.php | 14 +++++++++ src/Manager/ContentManager.php | 1 + src/Model/Content.php | 12 +++++++ src/Model/ContentInterface.php | 4 +++ src/Twig/Extension/TranslateExtension.php | 31 +++++++++++++++++++ .../admin/content/_read_indexing.html.twig | 10 +++++- tests/Unit/Entity/PageTest.php | 14 ++++++++- 14 files changed, 133 insertions(+), 3 deletions(-) diff --git a/cms/contents/page/translations/sfs_cms_contents.en.yaml b/cms/contents/page/translations/sfs_cms_contents.en.yaml index 470776b5..8a69db9e 100644 --- a/cms/contents/page/translations/sfs_cms_contents.en.yaml +++ b/cms/contents/page/translations/sfs_cms_contents.en.yaml @@ -105,6 +105,9 @@ admin_page: no: "no" yes: "yes" edit.link: "Edit" + canonicalPage: + label: "Canonical URL source" + empty: "This page is canonical to itself" routes: title: "Routes" cache: "(cache %ttl% seconds)" @@ -307,6 +310,10 @@ admin_page: yearly: "yearly" never: "never" sitemapPriority.label: "URL priority for sitemap" + canonicalPage: + label: "Canonical URL source" + placeholder: "Use this page as canonical (default)" + help: "If selected, this page will output a canonical URL pointing to the selected page." seo: metaTitle.label: "Title (browser title and meta tags)" metaDescription.label: "Description (meta tag)" diff --git a/cms/contents/page/translations/sfs_cms_contents.es.yaml b/cms/contents/page/translations/sfs_cms_contents.es.yaml index f1d10709..9497e161 100644 --- a/cms/contents/page/translations/sfs_cms_contents.es.yaml +++ b/cms/contents/page/translations/sfs_cms_contents.es.yaml @@ -105,6 +105,9 @@ admin_page: no: "no" yes: "sí" edit.link: "Editar" + canonicalPage: + label: "Fuente de URL canónica" + empty: "Esta página es canónica de sí misma" routes: title: "Rutas" cache: "(cache %ttl% segundos)" @@ -307,6 +310,10 @@ admin_page: yearly: "anualmente" never: "nunca" sitemapPriority.label: "Prioridad de URL en el sitemap" + canonicalPage: + label: "Fuente de URL canónica" + placeholder: "Usar esta página como canónica (por defecto)" + help: "Si se selecciona, esta página generará la URL canónica apuntando a la página seleccionada." seo: metaTitle.label: "Título (título del navegador y etiquetas meta)" metaDescription.label: "Descripción (etiqueta meta)" diff --git a/cms/layouts/default/render.html.twig b/cms/layouts/default/render.html.twig index 40db06b5..53463aeb 100644 --- a/cms/layouts/default/render.html.twig +++ b/cms/layouts/default/render.html.twig @@ -21,6 +21,10 @@ + {% set canonicalUrl = sfs_cms_canonical_url() %} + {% if canonicalUrl %} + + {% endif %} {% for locale, url in sfs_cms_alternate_urls() %} {% endfor %} @@ -93,4 +97,4 @@ }); {% endif %} -{% endblock javascripts %} \ No newline at end of file +{% endblock javascripts %} diff --git a/config/doctrine-mapping/entities/Content.orm.xml b/config/doctrine-mapping/entities/Content.orm.xml index 56cdc88a..3b4bc420 100644 --- a/config/doctrine-mapping/entities/Content.orm.xml +++ b/config/doctrine-mapping/entities/Content.orm.xml @@ -50,6 +50,10 @@ + + + + diff --git a/config/validation/ContentInterface.yaml b/config/validation/ContentInterface.yaml index b773b590..b2049f81 100644 --- a/config/validation/ContentInterface.yaml +++ b/config/validation/ContentInterface.yaml @@ -5,3 +5,7 @@ Softspring\CmsBundle\Model\ContentInterface: getters: routes: - Valid: ~ + canonicalPage: + - Symfony\Component\Validator\Constraints\Expression: + expression: "this.getCanonicalPage() == null or this.getCanonicalPage() != this" + message: "Canonical page cannot reference itself" diff --git a/src/Data/EntityTransformer/ContentEntityTransformer.php b/src/Data/EntityTransformer/ContentEntityTransformer.php index 42c393f8..a979a8b0 100644 --- a/src/Data/EntityTransformer/ContentEntityTransformer.php +++ b/src/Data/EntityTransformer/ContentEntityTransformer.php @@ -94,6 +94,7 @@ public function export(object $element, &$files = [], ?object $contentVersion = 'sites' => $content->getSites()->map(fn (SiteInterface $site) => $site->getId())->toArray(), 'extra' => $content->getExtraData(), 'indexing' => $content->getIndexing(), + 'canonical_page' => $content->getCanonicalPage()?->getName(), 'versions' => $versions, ], ]; @@ -138,6 +139,16 @@ public function import(array $data, ReferencesRepository $referencesRepository, $content->setExtraData($contentData['extra']); + if (!empty($contentData['canonical_page'])) { + try { + /** @var ContentInterface $canonicalPage */ + $canonicalPage = $referencesRepository->getReference('content___'.Slugger::lowerSlug($contentData['canonical_page']), true); + $content->setCanonicalPage($canonicalPage); + } catch (ReferenceNotFoundException) { + // ignore references outside the imported fixture set + } + } + if (isset($contentData['seo'])) { $content->setSeo($contentData['seo']); } elseif (isset($contentData['indexing'])) { diff --git a/src/Form/Admin/Content/ContentCreateForm.php b/src/Form/Admin/Content/ContentCreateForm.php index 23a5d34f..4d6284ef 100644 --- a/src/Form/Admin/Content/ContentCreateForm.php +++ b/src/Form/Admin/Content/ContentCreateForm.php @@ -7,6 +7,7 @@ use Softspring\CmsBundle\Form\Type\DynamicFormType; use Softspring\CmsBundle\Model\ContentInterface; use Softspring\CmsBundle\Translator\TranslatableContext; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -14,6 +15,7 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Intl\Locales; +use Doctrine\ORM\EntityRepository; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -74,6 +76,15 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'by_reference' => false, ]); + $builder->add('canonicalPage', EntityType::class, [ + 'class' => $options['content_config']['entity_class'], + 'required' => false, + 'choice_label' => 'name', + 'placeholder' => "admin_{$options['content_config']['_id']}.form.canonicalPage.placeholder", + 'help' => "admin_{$options['content_config']['_id']}.form.canonicalPage.help", + 'query_builder' => fn (EntityRepository $repository) => $repository->createQueryBuilder('c')->orderBy('c.name', 'ASC'), + ]); + if (!empty($options['content_config']['extra_fields'])) { $builder->add('extraData', DynamicFormType::class, [ 'form_fields' => $options['content_config']['extra_fields'], diff --git a/src/Form/Admin/Content/ContentUpdateForm.php b/src/Form/Admin/Content/ContentUpdateForm.php index a1c7fedd..bbe3fcf5 100644 --- a/src/Form/Admin/Content/ContentUpdateForm.php +++ b/src/Form/Admin/Content/ContentUpdateForm.php @@ -2,10 +2,12 @@ namespace Softspring\CmsBundle\Form\Admin\Content; +use Doctrine\ORM\EntityRepository; use Softspring\CmsBundle\Form\Admin\SiteChoiceType; use Softspring\CmsBundle\Form\Type\DynamicFormType; use Softspring\CmsBundle\Model\ContentInterface; use Softspring\CmsBundle\Translator\TranslatableContext; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -72,6 +74,18 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'constraints' => new Count(['min' => 1]), ]); + $builder->add('canonicalPage', EntityType::class, [ + 'class' => $options['content_config']['entity_class'], + 'required' => false, + 'choice_label' => 'name', + 'placeholder' => "admin_{$options['content_config']['_id']}.form.canonicalPage.placeholder", + 'help' => "admin_{$options['content_config']['_id']}.form.canonicalPage.help", + 'query_builder' => fn (EntityRepository $repository) => $repository->createQueryBuilder('c') + ->andWhere('c != :content') + ->setParameter('content', $options['content']) + ->orderBy('c.name', 'ASC'), + ]); + if (!empty($options['content_config']['extra_fields'])) { $builder->add('extraData', DynamicFormType::class, [ 'form_fields' => $options['content_config']['extra_fields'], diff --git a/src/Manager/ContentManager.php b/src/Manager/ContentManager.php index aaae84f3..93d355ec 100644 --- a/src/Manager/ContentManager.php +++ b/src/Manager/ContentManager.php @@ -83,6 +83,7 @@ public function duplicateEntity(ContentInterface $content): ContentInterface $newContent->setName($content->getName().' (copy)'); $newContent->setExtraData($content->getExtraData()); $newContent->setIndexing($content->getIndexing()); + $newContent->setCanonicalPage($content->getCanonicalPage()); $newContent->setDefaultLocale($content->getDefaultLocale()); $newContent->setLocales($content->getLocales()); foreach ($content->getSites() as $site) { diff --git a/src/Model/Content.php b/src/Model/Content.php index e395db6c..deb1acaf 100644 --- a/src/Model/Content.php +++ b/src/Model/Content.php @@ -25,6 +25,8 @@ abstract class Content implements ContentInterface */ protected Collection $routes; + protected ?ContentInterface $canonicalPage = null; + protected ?RouteInterface $canonical; protected ?array $extraData = null; @@ -104,6 +106,16 @@ public function removeRoute(RouteInterface $route): void } } + public function getCanonicalPage(): ?ContentInterface + { + return $this->canonicalPage; + } + + public function setCanonicalPage(?ContentInterface $canonicalPage): void + { + $this->canonicalPage = $canonicalPage; + } + public function getCanonicalRoutePath(?string $locale = null): ?RoutePathInterface { // TODO, by now there is not a canonical mark in route paths, so we return the first one diff --git a/src/Model/ContentInterface.php b/src/Model/ContentInterface.php index 2e5268fa..2d2b6a41 100644 --- a/src/Model/ContentInterface.php +++ b/src/Model/ContentInterface.php @@ -39,6 +39,10 @@ public function addRoute(RouteInterface $route): void; public function removeRoute(RouteInterface $route): void; + public function getCanonicalPage(): ?ContentInterface; + + public function setCanonicalPage(?ContentInterface $canonicalPage): void; + public function getCanonicalRoutePath(?string $locale = null): ?RoutePathInterface; public function getExtraData(): ?array; diff --git a/src/Twig/Extension/TranslateExtension.php b/src/Twig/Extension/TranslateExtension.php index 0c801a88..1e628687 100644 --- a/src/Twig/Extension/TranslateExtension.php +++ b/src/Twig/Extension/TranslateExtension.php @@ -40,6 +40,7 @@ public function getFunctions(): array new TwigFunction('sfs_cms_available_locales', [$this, 'getAvailableLocales']), new TwigFunction('sfs_cms_alternate_urls', [$this, 'getAlternateUrls']), new TwigFunction('sfs_cms_locale_paths', [$this, 'getLocalePaths']), + new TwigFunction('sfs_cms_canonical_url', [$this, 'getCanonicalUrl']), ]; } @@ -160,4 +161,34 @@ public function getLocalePaths(?string $defaultRoute = null): array return $localePaths; } + + public function getCanonicalUrl(): ?string + { + $request = $this->requestStack->getCurrentRequest(); + if (!$request) { + return null; + } + + $site = $request->attributes->get('_sfs_cms_site'); + $locale = $request->getLocale(); + + /** @var ?RoutePathInterface $routePath */ + $routePath = $request->attributes->get('routePath'); + if (!$routePath) { + return null; + } + + $content = $routePath->getRoute()->getContent(); + $canonicalPage = $content?->getCanonicalPage(); + + if ($canonicalPage) { + $canonicalRoutePath = $canonicalPage->getCanonicalRoutePath($locale) ?: $canonicalPage->getCanonicalRoutePath($canonicalPage->getDefaultLocale()); + + if ($canonicalRoutePath) { + return $this->cmsUrlGenerator->getUrlFixed($canonicalRoutePath, $site); + } + } + + return $this->cmsUrlGenerator->getUrlFixed($routePath, $site); + } } diff --git a/templates/admin/content/_read_indexing.html.twig b/templates/admin/content/_read_indexing.html.twig index f022c222..9f5e48f6 100644 --- a/templates/admin/content/_read_indexing.html.twig +++ b/templates/admin/content/_read_indexing.html.twig @@ -38,6 +38,14 @@ {{ ('admin_'~content_type~'.read.indexing.sitemap.no')|trans }} {% endif %} +
{{ ('admin_'~content_type~'.read.indexing.canonicalPage.label')|trans }}
+
+ {% if entity.canonicalPage %} + {{ entity.canonicalPage.name }} + {% else %} + {{ ('admin_'~content_type~'.read.indexing.canonicalPage.empty')|trans }} + {% endif %} +
{% endblock %} -{% endembed %} \ No newline at end of file +{% endembed %} diff --git a/tests/Unit/Entity/PageTest.php b/tests/Unit/Entity/PageTest.php index 4d72b329..918212e6 100644 --- a/tests/Unit/Entity/PageTest.php +++ b/tests/Unit/Entity/PageTest.php @@ -69,6 +69,18 @@ public function testRoutes(): void $this->assertCount(0, $page->getRoutes()); } + public function testCanonicalPage(): void + { + $page = new Page(); + $this->assertNull($page->getCanonicalPage()); + + $page->setCanonicalPage($canonicalPage = new Page()); + $this->assertSame($canonicalPage, $page->getCanonicalPage()); + + $page->setCanonicalPage(null); + $this->assertNull($page->getCanonicalPage()); + } + public function testExtraData(): void { $page = new Page(); @@ -115,4 +127,4 @@ public function testLastVersionNumber(): void $page->setLastVersionNumber(1); $this->assertEquals(1, $page->getLastVersionNumber()); } -} \ No newline at end of file +} From 3f6e8fa54937abd07146537c8d17a9d4b5f68d8e Mon Sep 17 00:00:00 2001 From: aritz Date: Thu, 5 Mar 2026 16:26:00 +0100 Subject: [PATCH 2/5] ADD canonical url --- src/Form/Admin/Content/ContentCreateForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Form/Admin/Content/ContentCreateForm.php b/src/Form/Admin/Content/ContentCreateForm.php index 4d6284ef..b8d20507 100644 --- a/src/Form/Admin/Content/ContentCreateForm.php +++ b/src/Form/Admin/Content/ContentCreateForm.php @@ -2,6 +2,7 @@ namespace Softspring\CmsBundle\Form\Admin\Content; +use Doctrine\ORM\EntityRepository; use Softspring\CmsBundle\Form\Admin\Route\RouteCollectionType; use Softspring\CmsBundle\Form\Admin\SiteChoiceType; use Softspring\CmsBundle\Form\Type\DynamicFormType; @@ -15,7 +16,6 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Intl\Locales; -use Doctrine\ORM\EntityRepository; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; From de308712613cfc3517a81b4f25b4ce140bb264df Mon Sep 17 00:00:00 2001 From: aritz Date: Mon, 9 Mar 2026 13:40:03 +0100 Subject: [PATCH 3/5] ADD canonical url --- cms/contents/page/config.yaml | 8 +++- cms/contents/page/seo-migrate.php | 9 +++++ cms/layouts/default/render.html.twig | 15 +++++-- .../ContentVersionTransformer.php | 16 ++++---- src/Form/Type/ContentType.php | 38 ++++++++++++++++++ src/Helper/RoutingHelper.php | 39 ++++++++++++------- src/Model/ContentVersion.php | 20 ++++++++++ src/Routing/UrlGenerator.php | 2 +- src/Twig/Extension/RouterExtension.php | 1 + .../admin/content/_version_info_seo.html.twig | 2 +- 10 files changed, 121 insertions(+), 29 deletions(-) create mode 100644 src/Form/Type/ContentType.php diff --git a/cms/contents/page/config.yaml b/cms/contents/page/config.yaml index e78666e3..4c073c21 100644 --- a/cms/contents/page/config.yaml +++ b/cms/contents/page/config.yaml @@ -1,6 +1,6 @@ # this is the page base configuration content: - revision: 2 + revision: 3 entity_class: 'Softspring\CmsBundle\Entity\Page' default_layout: 'default' version_seo: @@ -10,6 +10,10 @@ content: type: translation metaKeywords: type: translation + canonicalContent: + type: translatable + type_options: + type: content indexing: noIndex: type: checkbox @@ -41,7 +45,7 @@ content: admin_page.form.indexing.sitemapChangefreq.values.weekly: weekly admin_page.form.indexing.sitemapChangefreq.values.monthly: monthly admin_page.form.indexing.sitemapChangefreq.values.yearly: yearly - admin_page.form.indexing.sitemapChangefreq.values.never: never + admin_page.form.indexing.sitemapChangefreq.values.never: never sitemapPriority: type: number type_options: diff --git a/cms/contents/page/seo-migrate.php b/cms/contents/page/seo-migrate.php index 58fc8a1c..2d5091a2 100644 --- a/cms/contents/page/seo-migrate.php +++ b/cms/contents/page/seo-migrate.php @@ -13,5 +13,14 @@ } } + // canonicalContent became translatable in revision 3. + // Migrate previous scalar/entity value to a translatable structure. + if ($originVersion < 3 && $targetVersion >= 3 && isset($seo['canonicalContent']) && !is_array($seo['canonicalContent'])) { + $seo['canonicalContent'] = \Softspring\TranslatableBundle\Model\Translation::createFromArray([ + 'en' => $seo['canonicalContent'], + '_default' => 'en', + ]); + } + return $seo; }; diff --git a/cms/layouts/default/render.html.twig b/cms/layouts/default/render.html.twig index 53463aeb..e4471878 100644 --- a/cms/layouts/default/render.html.twig +++ b/cms/layouts/default/render.html.twig @@ -25,9 +25,18 @@ {% if canonicalUrl %} {% endif %} - {% for locale, url in sfs_cms_alternate_urls() %} - - {% endfor %} + {% set canonical_content = version.seo.canonicalContent|default(null) %} + {% if canonical_content is iterable %} + {% set canonical_content = canonical_content[app.request.locale]|default(null) %} + {% endif %} + {% if canonical_content and canonical_content != version.content %} + + {% else %} + + {% for locale, url in sfs_cms_alternate_urls() %} + + {% endfor %} + {% endif %} {% endblock seo %} {% block body %} diff --git a/src/EntityTransformer/ContentVersionTransformer.php b/src/EntityTransformer/ContentVersionTransformer.php index 53ff8d10..506d3294 100644 --- a/src/EntityTransformer/ContentVersionTransformer.php +++ b/src/EntityTransformer/ContentVersionTransformer.php @@ -77,14 +77,16 @@ public function untransform(object $entity, ObjectManager $em): void } if ($contentVersion->getSeo()) { - $seo = $contentVersion->getSeo(); - $config = $this->cmsConfig->getContent($contentVersion->getContent()); - $seo = DataMigrator::migrate($config['seo_revision_migration_scripts'], $seo, $config['revision'], $this->cmsConfig); - foreach ($seo as $field => $value) { - $seo[$field] = $this->untransformEntityValues($value, $em); - } - $contentVersion->setSeo($seo); + + $contentVersion->_setSeoCallback(function (array $seo) use ($em, $config): array { + $seo = DataMigrator::migrate($config['seo_revision_migration_scripts'], $seo, $config['revision'], $this->cmsConfig); + foreach ($seo as $field => $value) { + $seo[$field] = $this->untransformEntityValues($value, $em); + } + + return $seo; + }); } } diff --git a/src/Form/Type/ContentType.php b/src/Form/Type/ContentType.php new file mode 100644 index 00000000..52c2de6b --- /dev/null +++ b/src/Form/Type/ContentType.php @@ -0,0 +1,38 @@ +setDefaults([ + 'class' => ContentInterface::class, + 'em' => $this->sfsContentEm, + 'required' => false, + 'choice_label' => function (ContentInterface $content) { + return $content->getName(); + }, + 'group_by' => function (ContentInterface $content) { + return $this->translator->trans("{$this->contentManager->getType($content)}.name", [], 'sfs_cms_contents'); + }, + ]); + } +} diff --git a/src/Helper/RoutingHelper.php b/src/Helper/RoutingHelper.php index 81ecff8b..aa4b95cb 100644 --- a/src/Helper/RoutingHelper.php +++ b/src/Helper/RoutingHelper.php @@ -91,34 +91,43 @@ public function generatePath($route, ?string $locale = null, $site = null): stri return $this->generateUrl($route, $locale, $site, UrlGeneratorInterface::ABSOLUTE_PATH); } + public function generateUrlForContent($content, string $locale, $site = null): string + { + foreach ($content->getRoutes() as $route) { + if ($route->getPathForLocale($locale)) { + return $this->generateUrl($route, $locale, $site); + } + } + + return '#'; + } + public function generateUrl($route, ?string $locale = null, $site = null, int $referenceType = UrlGeneratorInterface::ABSOLUTE_URL): string { if (is_null($route)) { return '#'; } - if (is_array($route)) { - if (is_null($route['route_name'])) { - return '#'; - } + $params = []; + $params['_locale'] = $locale ?: ($this->requestStack->getCurrentRequest()?->getLocale() ?: 'en'); - $params = $route['route_params'] ?? []; + if ($site) { + $params['_site'] = $site; + } - $params['_locale'] = $locale ?: ($this->requestStack->getCurrentRequest()?->getLocale() ?: 'en'); + $routeName = $route; - if ($site) { - $params['_site'] = $site; + if (is_array($route)) { + if (is_null($route['route_name'])) { + return '#'; } - return $this->router->generate($route['route_name'], $params, $referenceType); + $params = array_merge($params, $route['route_params'] ?? []); + $routeName = $route['route_name']; } elseif ($route instanceof RouteInterface) { - return $this->router->generate($route->getId(), [], $referenceType); + $routeName = $route->getId(); } - $params = [ - '_locale' => $locale ?: ($this->requestStack->getCurrentRequest()?->getLocale() ?: 'en'), - ]; - - return $this->router->generate($route, $params, $referenceType); + return $this->router->generate($routeName, $params, $referenceType); } } diff --git a/src/Model/ContentVersion.php b/src/Model/ContentVersion.php index 4eaba454..789a7cc9 100644 --- a/src/Model/ContentVersion.php +++ b/src/Model/ContentVersion.php @@ -16,6 +16,10 @@ abstract class ContentVersion implements ContentVersionInterface protected ?array $seo = null; + protected mixed $_getSeoCallback = null; + + protected mixed $_rawSeo = null; + public function getContent(): ?ContentInterface { return $this->content; @@ -49,8 +53,19 @@ public function setLayout(?string $layout): void $this->layout = $layout; } + public function _setSeoCallback(callable $getSeoCallback): void + { + $this->_getSeoCallback = $getSeoCallback; + } + public function getSeo(): ?array { + if ($this->_getSeoCallback) { + $this->_rawSeo = $this->seo; + $this->seo = call_user_func($this->_getSeoCallback, $this->seo); + $this->_getSeoCallback = null; + } + return $this->seo; } @@ -58,4 +73,9 @@ public function setSeo(?array $seo): void { $this->seo = $seo; } + + public function getRawSeo(): ?array + { + return $this->_rawSeo ?? $this->seo; + } } diff --git a/src/Routing/UrlGenerator.php b/src/Routing/UrlGenerator.php index ec85e5d8..0388de41 100644 --- a/src/Routing/UrlGenerator.php +++ b/src/Routing/UrlGenerator.php @@ -48,7 +48,7 @@ public function getUrl($routeOrName, ?string $locale = null, $site = null, array } if ($route->getContent() && !$route->getContent()->getPublishedVersion()) { - return '#'; + return '#not-published'; } $queryString = !empty($routeParams) ? '?'.http_build_query($routeParams) : ''; diff --git a/src/Twig/Extension/RouterExtension.php b/src/Twig/Extension/RouterExtension.php index 016ff2a3..e5560b0c 100644 --- a/src/Twig/Extension/RouterExtension.php +++ b/src/Twig/Extension/RouterExtension.php @@ -24,6 +24,7 @@ public function getFunctions(): array new TwigFunction('sfs_cms_link_attr', [$this, 'generateLinkAttributes'], ['is_safe' => ['html']]), new TwigFunction('sfs_cms_resolve_request_from_url', [$this->routingHelper, 'resolveRequestFromUrl']), new TwigFunction('sfs_cms_url', [$this->routingHelper, 'generateUrl']), + new TwigFunction('sfs_cms_url_for_content', [$this->routingHelper, 'generateUrlForContent']), new TwigFunction('sfs_cms_path', [$this->routingHelper, 'generatePath']), new TwigFunction('sfs_cms_route_path_url', [$this->urlGenerator, 'getUrlFixed']), // TODO REVIEW THIS, check if it works with symfony native routes new TwigFunction('sfs_cms_route_path_path', [$this->urlGenerator, 'getPathFixed']), // TODO REVIEW THIS, check if it works with symfony native routes diff --git a/templates/admin/content/_version_info_seo.html.twig b/templates/admin/content/_version_info_seo.html.twig index 30eab94d..da436059 100644 --- a/templates/admin/content/_version_info_seo.html.twig +++ b/templates/admin/content/_version_info_seo.html.twig @@ -6,7 +6,7 @@ {% block content %}
{{ version_entity.seo|json_encode(constant('JSON_PRETTY_PRINT')) }}
+ style="max-height: 300px">{{ version_entity.rawSeo()|json_encode(constant('JSON_PRETTY_PRINT')) }}
{% endblock content %} {% endembed %} From 28bc9da4e62beea468a6394282dff4b6e4d09f06 Mon Sep 17 00:00:00 2001 From: aritz Date: Mon, 9 Mar 2026 13:47:50 +0100 Subject: [PATCH 4/5] ADD canonical url --- config/validation/ContentInterface.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/validation/ContentInterface.yaml b/config/validation/ContentInterface.yaml index b2049f81..b773b590 100644 --- a/config/validation/ContentInterface.yaml +++ b/config/validation/ContentInterface.yaml @@ -5,7 +5,3 @@ Softspring\CmsBundle\Model\ContentInterface: getters: routes: - Valid: ~ - canonicalPage: - - Symfony\Component\Validator\Constraints\Expression: - expression: "this.getCanonicalPage() == null or this.getCanonicalPage() != this" - message: "Canonical page cannot reference itself" From 339896e05731b6baaa3ec7f4bb22cd8985fc3513 Mon Sep 17 00:00:00 2001 From: aritz Date: Tue, 10 Mar 2026 08:41:12 +0100 Subject: [PATCH 5/5] ADD canonical url --- .../translations/sfs_cms_contents.en.yaml | 10 ++----- .../translations/sfs_cms_contents.es.yaml | 10 ++----- cms/layouts/default/render.html.twig | 4 --- .../doctrine-mapping/entities/Content.orm.xml | 4 --- .../ContentEntityTransformer.php | 11 ------- src/Form/Admin/Content/ContentCreateForm.php | 11 ------- src/Form/Admin/Content/ContentUpdateForm.php | 14 --------- src/Manager/ContentManager.php | 1 - src/Model/Content.php | 12 -------- src/Model/ContentInterface.php | 4 --- src/Twig/Extension/TranslateExtension.php | 30 ------------------- .../admin/content/_read_indexing.html.twig | 8 ----- tests/Unit/Entity/PageTest.php | 12 -------- 13 files changed, 6 insertions(+), 125 deletions(-) diff --git a/cms/contents/page/translations/sfs_cms_contents.en.yaml b/cms/contents/page/translations/sfs_cms_contents.en.yaml index 8a69db9e..2e02beb0 100644 --- a/cms/contents/page/translations/sfs_cms_contents.en.yaml +++ b/cms/contents/page/translations/sfs_cms_contents.en.yaml @@ -105,9 +105,6 @@ admin_page: no: "no" yes: "yes" edit.link: "Edit" - canonicalPage: - label: "Canonical URL source" - empty: "This page is canonical to itself" routes: title: "Routes" cache: "(cache %ttl% seconds)" @@ -310,14 +307,13 @@ admin_page: yearly: "yearly" never: "never" sitemapPriority.label: "URL priority for sitemap" - canonicalPage: - label: "Canonical URL source" - placeholder: "Use this page as canonical (default)" - help: "If selected, this page will output a canonical URL pointing to the selected page." seo: metaTitle.label: "Title (browser title and meta tags)" metaDescription.label: "Description (meta tag)" metaKeywords.label: "Keywords (meta tag)" + canonicalContent.label: "Canonical URL source" + canonicalContent.placeholder: "Use this page as canonical (default)" + canonicalContent.help: "If selected, this page will output a canonical URL pointing to the selected page." version_form: note.label: "Note" diff --git a/cms/contents/page/translations/sfs_cms_contents.es.yaml b/cms/contents/page/translations/sfs_cms_contents.es.yaml index 9497e161..b5945f39 100644 --- a/cms/contents/page/translations/sfs_cms_contents.es.yaml +++ b/cms/contents/page/translations/sfs_cms_contents.es.yaml @@ -105,9 +105,6 @@ admin_page: no: "no" yes: "sí" edit.link: "Editar" - canonicalPage: - label: "Fuente de URL canónica" - empty: "Esta página es canónica de sí misma" routes: title: "Rutas" cache: "(cache %ttl% segundos)" @@ -310,14 +307,13 @@ admin_page: yearly: "anualmente" never: "nunca" sitemapPriority.label: "Prioridad de URL en el sitemap" - canonicalPage: - label: "Fuente de URL canónica" - placeholder: "Usar esta página como canónica (por defecto)" - help: "Si se selecciona, esta página generará la URL canónica apuntando a la página seleccionada." seo: metaTitle.label: "Título (título del navegador y etiquetas meta)" metaDescription.label: "Descripción (etiqueta meta)" metaKeywords.label: "Palabras clave (etiqueta meta)" + canonicalContent.label: "Fuente de URL canónica" + canonicalContent.placeholder: "Usar esta página como canónica (por defecto)" + canonicalContent.help: "Si se selecciona, esta página generará la URL canónica apuntando a la página seleccionada." version_form: note.label: "Nota" diff --git a/cms/layouts/default/render.html.twig b/cms/layouts/default/render.html.twig index e4471878..1a6dec5b 100644 --- a/cms/layouts/default/render.html.twig +++ b/cms/layouts/default/render.html.twig @@ -21,10 +21,6 @@ - {% set canonicalUrl = sfs_cms_canonical_url() %} - {% if canonicalUrl %} - - {% endif %} {% set canonical_content = version.seo.canonicalContent|default(null) %} {% if canonical_content is iterable %} {% set canonical_content = canonical_content[app.request.locale]|default(null) %} diff --git a/config/doctrine-mapping/entities/Content.orm.xml b/config/doctrine-mapping/entities/Content.orm.xml index 3b4bc420..56cdc88a 100644 --- a/config/doctrine-mapping/entities/Content.orm.xml +++ b/config/doctrine-mapping/entities/Content.orm.xml @@ -50,10 +50,6 @@ - - - - diff --git a/src/Data/EntityTransformer/ContentEntityTransformer.php b/src/Data/EntityTransformer/ContentEntityTransformer.php index a979a8b0..42c393f8 100644 --- a/src/Data/EntityTransformer/ContentEntityTransformer.php +++ b/src/Data/EntityTransformer/ContentEntityTransformer.php @@ -94,7 +94,6 @@ public function export(object $element, &$files = [], ?object $contentVersion = 'sites' => $content->getSites()->map(fn (SiteInterface $site) => $site->getId())->toArray(), 'extra' => $content->getExtraData(), 'indexing' => $content->getIndexing(), - 'canonical_page' => $content->getCanonicalPage()?->getName(), 'versions' => $versions, ], ]; @@ -139,16 +138,6 @@ public function import(array $data, ReferencesRepository $referencesRepository, $content->setExtraData($contentData['extra']); - if (!empty($contentData['canonical_page'])) { - try { - /** @var ContentInterface $canonicalPage */ - $canonicalPage = $referencesRepository->getReference('content___'.Slugger::lowerSlug($contentData['canonical_page']), true); - $content->setCanonicalPage($canonicalPage); - } catch (ReferenceNotFoundException) { - // ignore references outside the imported fixture set - } - } - if (isset($contentData['seo'])) { $content->setSeo($contentData['seo']); } elseif (isset($contentData['indexing'])) { diff --git a/src/Form/Admin/Content/ContentCreateForm.php b/src/Form/Admin/Content/ContentCreateForm.php index b8d20507..23a5d34f 100644 --- a/src/Form/Admin/Content/ContentCreateForm.php +++ b/src/Form/Admin/Content/ContentCreateForm.php @@ -2,13 +2,11 @@ namespace Softspring\CmsBundle\Form\Admin\Content; -use Doctrine\ORM\EntityRepository; use Softspring\CmsBundle\Form\Admin\Route\RouteCollectionType; use Softspring\CmsBundle\Form\Admin\SiteChoiceType; use Softspring\CmsBundle\Form\Type\DynamicFormType; use Softspring\CmsBundle\Model\ContentInterface; use Softspring\CmsBundle\Translator\TranslatableContext; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -76,15 +74,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'by_reference' => false, ]); - $builder->add('canonicalPage', EntityType::class, [ - 'class' => $options['content_config']['entity_class'], - 'required' => false, - 'choice_label' => 'name', - 'placeholder' => "admin_{$options['content_config']['_id']}.form.canonicalPage.placeholder", - 'help' => "admin_{$options['content_config']['_id']}.form.canonicalPage.help", - 'query_builder' => fn (EntityRepository $repository) => $repository->createQueryBuilder('c')->orderBy('c.name', 'ASC'), - ]); - if (!empty($options['content_config']['extra_fields'])) { $builder->add('extraData', DynamicFormType::class, [ 'form_fields' => $options['content_config']['extra_fields'], diff --git a/src/Form/Admin/Content/ContentUpdateForm.php b/src/Form/Admin/Content/ContentUpdateForm.php index bbe3fcf5..a1c7fedd 100644 --- a/src/Form/Admin/Content/ContentUpdateForm.php +++ b/src/Form/Admin/Content/ContentUpdateForm.php @@ -2,12 +2,10 @@ namespace Softspring\CmsBundle\Form\Admin\Content; -use Doctrine\ORM\EntityRepository; use Softspring\CmsBundle\Form\Admin\SiteChoiceType; use Softspring\CmsBundle\Form\Type\DynamicFormType; use Softspring\CmsBundle\Model\ContentInterface; use Softspring\CmsBundle\Translator\TranslatableContext; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -74,18 +72,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'constraints' => new Count(['min' => 1]), ]); - $builder->add('canonicalPage', EntityType::class, [ - 'class' => $options['content_config']['entity_class'], - 'required' => false, - 'choice_label' => 'name', - 'placeholder' => "admin_{$options['content_config']['_id']}.form.canonicalPage.placeholder", - 'help' => "admin_{$options['content_config']['_id']}.form.canonicalPage.help", - 'query_builder' => fn (EntityRepository $repository) => $repository->createQueryBuilder('c') - ->andWhere('c != :content') - ->setParameter('content', $options['content']) - ->orderBy('c.name', 'ASC'), - ]); - if (!empty($options['content_config']['extra_fields'])) { $builder->add('extraData', DynamicFormType::class, [ 'form_fields' => $options['content_config']['extra_fields'], diff --git a/src/Manager/ContentManager.php b/src/Manager/ContentManager.php index 93d355ec..aaae84f3 100644 --- a/src/Manager/ContentManager.php +++ b/src/Manager/ContentManager.php @@ -83,7 +83,6 @@ public function duplicateEntity(ContentInterface $content): ContentInterface $newContent->setName($content->getName().' (copy)'); $newContent->setExtraData($content->getExtraData()); $newContent->setIndexing($content->getIndexing()); - $newContent->setCanonicalPage($content->getCanonicalPage()); $newContent->setDefaultLocale($content->getDefaultLocale()); $newContent->setLocales($content->getLocales()); foreach ($content->getSites() as $site) { diff --git a/src/Model/Content.php b/src/Model/Content.php index deb1acaf..e395db6c 100644 --- a/src/Model/Content.php +++ b/src/Model/Content.php @@ -25,8 +25,6 @@ abstract class Content implements ContentInterface */ protected Collection $routes; - protected ?ContentInterface $canonicalPage = null; - protected ?RouteInterface $canonical; protected ?array $extraData = null; @@ -106,16 +104,6 @@ public function removeRoute(RouteInterface $route): void } } - public function getCanonicalPage(): ?ContentInterface - { - return $this->canonicalPage; - } - - public function setCanonicalPage(?ContentInterface $canonicalPage): void - { - $this->canonicalPage = $canonicalPage; - } - public function getCanonicalRoutePath(?string $locale = null): ?RoutePathInterface { // TODO, by now there is not a canonical mark in route paths, so we return the first one diff --git a/src/Model/ContentInterface.php b/src/Model/ContentInterface.php index 2d2b6a41..2e5268fa 100644 --- a/src/Model/ContentInterface.php +++ b/src/Model/ContentInterface.php @@ -39,10 +39,6 @@ public function addRoute(RouteInterface $route): void; public function removeRoute(RouteInterface $route): void; - public function getCanonicalPage(): ?ContentInterface; - - public function setCanonicalPage(?ContentInterface $canonicalPage): void; - public function getCanonicalRoutePath(?string $locale = null): ?RoutePathInterface; public function getExtraData(): ?array; diff --git a/src/Twig/Extension/TranslateExtension.php b/src/Twig/Extension/TranslateExtension.php index 1e628687..0922a3e4 100644 --- a/src/Twig/Extension/TranslateExtension.php +++ b/src/Twig/Extension/TranslateExtension.php @@ -40,7 +40,6 @@ public function getFunctions(): array new TwigFunction('sfs_cms_available_locales', [$this, 'getAvailableLocales']), new TwigFunction('sfs_cms_alternate_urls', [$this, 'getAlternateUrls']), new TwigFunction('sfs_cms_locale_paths', [$this, 'getLocalePaths']), - new TwigFunction('sfs_cms_canonical_url', [$this, 'getCanonicalUrl']), ]; } @@ -162,33 +161,4 @@ public function getLocalePaths(?string $defaultRoute = null): array return $localePaths; } - public function getCanonicalUrl(): ?string - { - $request = $this->requestStack->getCurrentRequest(); - if (!$request) { - return null; - } - - $site = $request->attributes->get('_sfs_cms_site'); - $locale = $request->getLocale(); - - /** @var ?RoutePathInterface $routePath */ - $routePath = $request->attributes->get('routePath'); - if (!$routePath) { - return null; - } - - $content = $routePath->getRoute()->getContent(); - $canonicalPage = $content?->getCanonicalPage(); - - if ($canonicalPage) { - $canonicalRoutePath = $canonicalPage->getCanonicalRoutePath($locale) ?: $canonicalPage->getCanonicalRoutePath($canonicalPage->getDefaultLocale()); - - if ($canonicalRoutePath) { - return $this->cmsUrlGenerator->getUrlFixed($canonicalRoutePath, $site); - } - } - - return $this->cmsUrlGenerator->getUrlFixed($routePath, $site); - } } diff --git a/templates/admin/content/_read_indexing.html.twig b/templates/admin/content/_read_indexing.html.twig index 9f5e48f6..e2a340b6 100644 --- a/templates/admin/content/_read_indexing.html.twig +++ b/templates/admin/content/_read_indexing.html.twig @@ -38,14 +38,6 @@ {{ ('admin_'~content_type~'.read.indexing.sitemap.no')|trans }} {% endif %} -
{{ ('admin_'~content_type~'.read.indexing.canonicalPage.label')|trans }}
-
- {% if entity.canonicalPage %} - {{ entity.canonicalPage.name }} - {% else %} - {{ ('admin_'~content_type~'.read.indexing.canonicalPage.empty')|trans }} - {% endif %} -
{% endblock %} {% endembed %} diff --git a/tests/Unit/Entity/PageTest.php b/tests/Unit/Entity/PageTest.php index 918212e6..12dcfe73 100644 --- a/tests/Unit/Entity/PageTest.php +++ b/tests/Unit/Entity/PageTest.php @@ -69,18 +69,6 @@ public function testRoutes(): void $this->assertCount(0, $page->getRoutes()); } - public function testCanonicalPage(): void - { - $page = new Page(); - $this->assertNull($page->getCanonicalPage()); - - $page->setCanonicalPage($canonicalPage = new Page()); - $this->assertSame($canonicalPage, $page->getCanonicalPage()); - - $page->setCanonicalPage(null); - $this->assertNull($page->getCanonicalPage()); - } - public function testExtraData(): void { $page = new Page();