From d90040e722cf87cf09cae99dea14a8b87dbd390c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 30 Dec 2025 20:42:41 +0000
Subject: [PATCH 1/5] Initial plan
From 480fb1ca88b1ffbc127666f0da7b7b23174f4f8a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 30 Dec 2025 20:49:08 +0000
Subject: [PATCH 2/5] Add configurable blog URL prefix feature
Co-authored-by: 7underlines <17121556+7underlines@users.noreply.github.com>
---
src/Content/Blog/BlogSeoUrlRoute.php | 22 +++++++++++--
src/Resources/config/config.xml | 9 +++++
src/Resources/config/services.xml | 1 +
src/Subscriber/BlogCacheInvalidSubscriber.php | 33 +++++++++++++++++++
4 files changed, 62 insertions(+), 3 deletions(-)
diff --git a/src/Content/Blog/BlogSeoUrlRoute.php b/src/Content/Blog/BlogSeoUrlRoute.php
index 064445dd..a227db36 100644
--- a/src/Content/Blog/BlogSeoUrlRoute.php
+++ b/src/Content/Blog/BlogSeoUrlRoute.php
@@ -9,26 +9,42 @@
use Shopware\Core\Framework\DataAbstractionLayer\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\System\SalesChannel\SalesChannelEntity;
+use Shopware\Core\System\SystemConfig\SystemConfigService;
class BlogSeoUrlRoute implements SeoUrlRouteInterface
{
public const ROUTE_NAME = 'werkl.frontend.blog.detail';
public const DEFAULT_TEMPLATE = 'blog/{{ entry.blogCategories.first.translated.name|lower }}/{{ entry.translated.slug|lower }}';
- public function __construct(private readonly BlogEntryDefinition $blogEntryDefinition)
- {
+ public function __construct(
+ private readonly BlogEntryDefinition $blogEntryDefinition,
+ private readonly SystemConfigService $systemConfigService
+ ) {
}
public function getConfig(): SeoUrlRouteConfig
{
+ $template = $this->getTemplate();
+
return new SeoUrlRouteConfig(
$this->blogEntryDefinition,
self::ROUTE_NAME,
- self::DEFAULT_TEMPLATE,
+ $template,
true
);
}
+ private function getTemplate(): string
+ {
+ $prefix = $this->systemConfigService->getString('WerklOpenBlogware.config.blogUrlPrefix');
+ if (empty($prefix)) {
+ $prefix = 'blog';
+ }
+
+ // Use the configured prefix instead of hardcoded "blog"
+ return $prefix . '/{{ entry.blogCategories.first.translated.name|lower }}/{{ entry.translated.slug|lower }}';
+ }
+
public function prepareCriteria(Criteria $criteria, SalesChannelEntity $salesChannel): void
{
$criteria->addAssociations([
diff --git a/src/Resources/config/config.xml b/src/Resources/config/config.xml
index b74e2a28..50d2f10d 100644
--- a/src/Resources/config/config.xml
+++ b/src/Resources/config/config.xml
@@ -16,6 +16,15 @@
SEO Meta Configuration
SEO Metakonfiguration
+
+ blogUrlPrefix
+
+
+ blog
+ The prefix used in blog detail URLs. Change this to customize your blog URLs (e.g., from "blog/my-post" to "beauty-blog/my-post"). After changing this value, the SEO URLs will be regenerated automatically.
+ Das Präfix für Blog-Detail-URLs. Ändern Sie dies, um Ihre Blog-URLs anzupassen (z. B. von "blog/mein-beitrag" zu "beauty-blog/mein-beitrag"). Nach der Änderung werden die SEO-URLs automatisch neu generiert.
+
+
maximumMetaTitleCharacter
diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml
index 67c27133..be7a1585 100644
--- a/src/Resources/config/services.xml
+++ b/src/Resources/config/services.xml
@@ -127,6 +127,7 @@
+
diff --git a/src/Subscriber/BlogCacheInvalidSubscriber.php b/src/Subscriber/BlogCacheInvalidSubscriber.php
index 710aef9c..c138b7b3 100644
--- a/src/Subscriber/BlogCacheInvalidSubscriber.php
+++ b/src/Subscriber/BlogCacheInvalidSubscriber.php
@@ -66,6 +66,9 @@ public static function getSubscribedEvents(): array
SeoEvents::SEO_URL_TEMPLATE_WRITTEN_EVENT => [
['updateSeoUrlForAllArticles', 10],
],
+ 'system_config.written' => [
+ ['onSystemConfigChanged', 10],
+ ],
];
}
@@ -147,6 +150,36 @@ public function updateSeoUrlForAllArticles(EntityWrittenEvent $event): void
$this->seoUrlUpdater->update(BlogSeoUrlRoute::ROUTE_NAME, $ids);
}
+ /**
+ * When the blog URL prefix configuration changes, regenerate all blog SEO URLs
+ */
+ public function onSystemConfigChanged(EntityWrittenEvent $event): void
+ {
+ $context = $event->getContext();
+
+ // Check if any of the written config keys is the blogUrlPrefix
+ $payloads = $event->getPayloads();
+ $needsUpdate = false;
+
+ foreach ($payloads as $payload) {
+ if (isset($payload['configurationKey']) && $payload['configurationKey'] === 'WerklOpenBlogware.config.blogUrlPrefix') {
+ $needsUpdate = true;
+ break;
+ }
+ }
+
+ if (!$needsUpdate) {
+ return;
+ }
+
+ /** @var list $ids */
+ $ids = $this->blogRepository->searchIds(new Criteria(), $context)->getIds();
+
+ $this->seoUrlUpdater->update(BlogSeoUrlRoute::ROUTE_NAME, $ids);
+ $this->invalidateCache($ids);
+ $this->invalidateCacheCategory($context);
+ }
+
/**
* Invalidate blog category cache
*/
From f285128f9938dc39c56d1ecc4a34a3e129a523ae Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 30 Dec 2025 20:51:02 +0000
Subject: [PATCH 3/5] Add unit tests for BlogSeoUrlRoute with custom prefix
Co-authored-by: 7underlines <17121556+7underlines@users.noreply.github.com>
---
.../Content/Blog/BlogSeoUrlRouteTest.php | 100 ++++++++++++++++++
1 file changed, 100 insertions(+)
create mode 100644 tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php
diff --git a/tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php b/tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php
new file mode 100644
index 00000000..77859b3d
--- /dev/null
+++ b/tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php
@@ -0,0 +1,100 @@
+createMock(SystemConfigService::class);
+ $systemConfigService->method('getString')
+ ->with('WerklOpenBlogware.config.blogUrlPrefix')
+ ->willReturn('');
+
+ $blogEntryDefinition = $this->createMock(BlogEntryDefinition::class);
+ $route = new BlogSeoUrlRoute($blogEntryDefinition, $systemConfigService);
+
+ $config = $route->getConfig();
+ $template = $config->getTemplate();
+
+ static::assertStringStartsWith('blog/', $template);
+ static::assertStringContainsString('{{ entry.blogCategories.first.translated.name|lower }}', $template);
+ static::assertStringContainsString('{{ entry.translated.slug|lower }}', $template);
+ }
+
+ /**
+ * Test that custom prefix is used when configured
+ */
+ public function testGetConfigWithCustomPrefix(): void
+ {
+ $systemConfigService = $this->createMock(SystemConfigService::class);
+ $systemConfigService->method('getString')
+ ->with('WerklOpenBlogware.config.blogUrlPrefix')
+ ->willReturn('beauty-blog');
+
+ $blogEntryDefinition = $this->createMock(BlogEntryDefinition::class);
+ $route = new BlogSeoUrlRoute($blogEntryDefinition, $systemConfigService);
+
+ $config = $route->getConfig();
+ $template = $config->getTemplate();
+
+ static::assertStringStartsWith('beauty-blog/', $template);
+ static::assertStringContainsString('{{ entry.blogCategories.first.translated.name|lower }}', $template);
+ static::assertStringContainsString('{{ entry.translated.slug|lower }}', $template);
+ }
+
+ /**
+ * Test that various custom prefixes work correctly
+ */
+ public function testGetConfigWithVariousPrefixes(): void
+ {
+ $testPrefixes = [
+ 'news',
+ 'articles',
+ 'my-custom-blog',
+ 'tech',
+ 'lifestyle',
+ ];
+
+ foreach ($testPrefixes as $prefix) {
+ $systemConfigService = $this->createMock(SystemConfigService::class);
+ $systemConfigService->method('getString')
+ ->with('WerklOpenBlogware.config.blogUrlPrefix')
+ ->willReturn($prefix);
+
+ $blogEntryDefinition = $this->createMock(BlogEntryDefinition::class);
+ $route = new BlogSeoUrlRoute($blogEntryDefinition, $systemConfigService);
+
+ $config = $route->getConfig();
+ $template = $config->getTemplate();
+
+ static::assertStringStartsWith($prefix . '/', $template, "Prefix '$prefix' should be used in template");
+ }
+ }
+
+ /**
+ * Test that route name remains constant
+ */
+ public function testRouteNameIsConstant(): void
+ {
+ $systemConfigService = $this->createMock(SystemConfigService::class);
+ $systemConfigService->method('getString')->willReturn('test-prefix');
+
+ $blogEntryDefinition = $this->createMock(BlogEntryDefinition::class);
+ $route = new BlogSeoUrlRoute($blogEntryDefinition, $systemConfigService);
+
+ $config = $route->getConfig();
+
+ static::assertSame(BlogSeoUrlRoute::ROUTE_NAME, $config->getRouteName());
+ static::assertSame('werkl.frontend.blog.detail', $config->getRouteName());
+ }
+}
From 110441f651e7d9f2e0e575fe4a84f7877f245ac2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 30 Dec 2025 20:52:02 +0000
Subject: [PATCH 4/5] Add documentation for customizable blog URL prefix
feature
Co-authored-by: 7underlines <17121556+7underlines@users.noreply.github.com>
---
CHANGELOG_de-DE.md | 5 +++++
CHANGELOG_en-GB.md | 5 +++++
README.md | 19 +++++++++++++++++++
3 files changed, 29 insertions(+)
diff --git a/CHANGELOG_de-DE.md b/CHANGELOG_de-DE.md
index 363c5bfb..39f49c3c 100644
--- a/CHANGELOG_de-DE.md
+++ b/CHANGELOG_de-DE.md
@@ -1,3 +1,8 @@
+# Unveröffentlicht
+- Konfigurierbares Blog-URL-Präfix hinzugefügt, um Blog-Detail-URLs anzupassen (z. B. von "blog/..." zu "beauty-blog/...")
+- SEO-URLs werden automatisch neu generiert, wenn das Präfix geändert wird
+- Standard-Präfix bleibt "blog" für Abwärtskompatibilität
+
# 5.0.2
- Kompatibilität mit Shopware 6.7.5.0
diff --git a/CHANGELOG_en-GB.md b/CHANGELOG_en-GB.md
index ed70fba5..c89b2ba2 100644
--- a/CHANGELOG_en-GB.md
+++ b/CHANGELOG_en-GB.md
@@ -1,3 +1,8 @@
+# Unreleased
+- Added configurable blog URL prefix to allow customization of blog detail URLs (e.g., from "blog/..." to "beauty-blog/...")
+- SEO URLs are automatically regenerated when the prefix is changed
+- Default prefix remains "blog" for backward compatibility
+
# 5.0.2
- Compatibility with Shopware 6.7.5.0
diff --git a/README.md b/README.md
index 30ac5545..8733e3db 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,25 @@ where you can also select from all available variables.

*SEO URL template*
+#### Custom Blog URL Prefix
+You can now customize the URL prefix for your blog detail pages. Instead of the default "blog" prefix,
+you can set any custom prefix (e.g., "beauty-blog", "news", "articles") in the plugin configuration:
+
+1. Navigate to **Settings → System → OpenBlogware**
+2. Find the **"Blog URL prefix"** field in the "SEO Meta Configuration" section
+3. Enter your desired prefix (e.g., "beauty-blog")
+4. Save the configuration
+
+When you change the prefix, all existing blog SEO URLs are automatically regenerated. This is particularly
+useful for:
+- Maintaining existing URL structure when migrating from other platforms
+- SEO optimization with brand-specific URLs
+- Multi-language sites with different prefixes
+
+**Example:**
+- Default: `blog/category-name/my-post`
+- Custom: `beauty-blog/category-name/my-post`
+
## RSS Feed
For access **RSS Feed** url you can use this path `/blog/rss`
Example(`http://Your-domain/blog/rss`)
From 518f4814d6ae3e9458c0d75c26b0bedf76a054f2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 30 Dec 2025 20:53:31 +0000
Subject: [PATCH 5/5] Address code review feedback - improve null checks and
test efficiency
Co-authored-by: 7underlines <17121556+7underlines@users.noreply.github.com>
---
src/Content/Blog/BlogSeoUrlRoute.php | 2 +-
.../Content/Blog/BlogSeoUrlRouteTest.php | 76 +++++++++++++------
2 files changed, 53 insertions(+), 25 deletions(-)
diff --git a/src/Content/Blog/BlogSeoUrlRoute.php b/src/Content/Blog/BlogSeoUrlRoute.php
index a227db36..0b07640d 100644
--- a/src/Content/Blog/BlogSeoUrlRoute.php
+++ b/src/Content/Blog/BlogSeoUrlRoute.php
@@ -37,7 +37,7 @@ public function getConfig(): SeoUrlRouteConfig
private function getTemplate(): string
{
$prefix = $this->systemConfigService->getString('WerklOpenBlogware.config.blogUrlPrefix');
- if (empty($prefix)) {
+ if ($prefix === null || $prefix === '') {
$prefix = 'blog';
}
diff --git a/tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php b/tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php
index 77859b3d..c5074caa 100644
--- a/tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php
+++ b/tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php
@@ -53,32 +53,60 @@ public function testGetConfigWithCustomPrefix(): void
}
/**
- * Test that various custom prefixes work correctly
+ * Test that "news" prefix works correctly
*/
- public function testGetConfigWithVariousPrefixes(): void
+ public function testGetConfigWithNewsPrefix(): void
{
- $testPrefixes = [
- 'news',
- 'articles',
- 'my-custom-blog',
- 'tech',
- 'lifestyle',
- ];
-
- foreach ($testPrefixes as $prefix) {
- $systemConfigService = $this->createMock(SystemConfigService::class);
- $systemConfigService->method('getString')
- ->with('WerklOpenBlogware.config.blogUrlPrefix')
- ->willReturn($prefix);
-
- $blogEntryDefinition = $this->createMock(BlogEntryDefinition::class);
- $route = new BlogSeoUrlRoute($blogEntryDefinition, $systemConfigService);
-
- $config = $route->getConfig();
- $template = $config->getTemplate();
-
- static::assertStringStartsWith($prefix . '/', $template, "Prefix '$prefix' should be used in template");
- }
+ $systemConfigService = $this->createMock(SystemConfigService::class);
+ $systemConfigService->method('getString')
+ ->with('WerklOpenBlogware.config.blogUrlPrefix')
+ ->willReturn('news');
+
+ $blogEntryDefinition = $this->createMock(BlogEntryDefinition::class);
+ $route = new BlogSeoUrlRoute($blogEntryDefinition, $systemConfigService);
+
+ $config = $route->getConfig();
+ $template = $config->getTemplate();
+
+ static::assertStringStartsWith('news/', $template);
+ }
+
+ /**
+ * Test that "articles" prefix works correctly
+ */
+ public function testGetConfigWithArticlesPrefix(): void
+ {
+ $systemConfigService = $this->createMock(SystemConfigService::class);
+ $systemConfigService->method('getString')
+ ->with('WerklOpenBlogware.config.blogUrlPrefix')
+ ->willReturn('articles');
+
+ $blogEntryDefinition = $this->createMock(BlogEntryDefinition::class);
+ $route = new BlogSeoUrlRoute($blogEntryDefinition, $systemConfigService);
+
+ $config = $route->getConfig();
+ $template = $config->getTemplate();
+
+ static::assertStringStartsWith('articles/', $template);
+ }
+
+ /**
+ * Test that "my-custom-blog" prefix with hyphens works correctly
+ */
+ public function testGetConfigWithHyphenatedPrefix(): void
+ {
+ $systemConfigService = $this->createMock(SystemConfigService::class);
+ $systemConfigService->method('getString')
+ ->with('WerklOpenBlogware.config.blogUrlPrefix')
+ ->willReturn('my-custom-blog');
+
+ $blogEntryDefinition = $this->createMock(BlogEntryDefinition::class);
+ $route = new BlogSeoUrlRoute($blogEntryDefinition, $systemConfigService);
+
+ $config = $route->getConfig();
+ $template = $config->getTemplate();
+
+ static::assertStringStartsWith('my-custom-blog/', $template);
}
/**