Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cms/contents/page/translations/sfs_cms_contents.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -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)"
Expand Down
7 changes: 7 additions & 0 deletions cms/contents/page/translations/sfs_cms_contents.es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -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)"
Expand Down
6 changes: 5 additions & 1 deletion cms/layouts/default/render.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
<meta property="og:title" content="{{ version.seo.metaTitle|default|sfs_cms_trans }}">
<meta property="og:description" content="{{ version.seo.metaDescription|default|sfs_cms_trans }}">
<meta property="og:locale" content="{{ app.request.locale }}">
{% set canonicalUrl = sfs_cms_canonical_url() %}
{% if canonicalUrl %}
<link rel="canonical" href="{{ canonicalUrl }}" />
{% endif %}
{% for locale, url in sfs_cms_alternate_urls() %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternate links should only be rendered when the current page is the canonical URL. If canonicalUrl is different from the current URL, it means the page is not the canonical version. In that case, we should only output the canonical tag pointing to the canonical page, but not the hreflang alternates, since those should only exist on the canonical version.

<link rel="alternate" hreflang="{{ locale }}" href="{{ url }}" />
{% endfor %}
Expand Down Expand Up @@ -93,4 +97,4 @@
});
</script>
{% endif %}
{% endblock javascripts %}
{% endblock javascripts %}
4 changes: 4 additions & 0 deletions config/doctrine-mapping/entities/Content.orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
<join-column name="last_version_id" on-delete="SET NULL"/>
</many-to-one>

<many-to-one field="canonicalPage" target-entity="Softspring\CmsBundle\Model\ContentInterface" fetch="LAZY">
<join-column name="canonical_page_id" on-delete="SET NULL"/>
</many-to-one>

<one-to-many field="routes" target-entity="Softspring\CmsBundle\Model\RouteInterface" mapped-by="content">
<cache usage="NONSTRICT_READ_WRITE" />
<cascade>
Expand Down
4 changes: 4 additions & 0 deletions config/validation/ContentInterface.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
11 changes: 11 additions & 0 deletions src/Data/EntityTransformer/ContentEntityTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
];
Expand Down Expand Up @@ -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'])) {
Expand Down
11 changes: 11 additions & 0 deletions src/Form/Admin/Content/ContentCreateForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

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;
Expand Down Expand Up @@ -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'],
Expand Down
14 changes: 14 additions & 0 deletions src/Form/Admin/Content/ContentUpdateForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'],
Expand Down
1 change: 1 addition & 0 deletions src/Manager/ContentManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions src/Model/Content.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ abstract class Content implements ContentInterface
*/
protected Collection $routes;

protected ?ContentInterface $canonicalPage = null;

protected ?RouteInterface $canonical;

protected ?array $extraData = null;
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/Model/ContentInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions src/Twig/Extension/TranslateExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
];
}

Expand Down Expand Up @@ -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);
}
}
10 changes: 9 additions & 1 deletion templates/admin/content/_read_indexing.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@
<span class="badge bg-warning">{{ ('admin_'~content_type~'.read.indexing.sitemap.no')|trans }}</span>
{% endif %}
</dd>
<dt class="fw-normal">{{ ('admin_'~content_type~'.read.indexing.canonicalPage.label')|trans }}</dt>
<dd class="fw-bold">
{% if entity.canonicalPage %}
{{ entity.canonicalPage.name }}
{% else %}
{{ ('admin_'~content_type~'.read.indexing.canonicalPage.empty')|trans }}
{% endif %}
</dd>
</dl>
{% endblock %}
{% endembed %}
{% endembed %}
14 changes: 13 additions & 1 deletion tests/Unit/Entity/PageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -115,4 +127,4 @@ public function testLastVersionNumber(): void
$page->setLastVersionNumber(1);
$this->assertEquals(1, $page->getLastVersionNumber());
}
}
}