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`)
diff --git a/src/Content/Blog/BlogSeoUrlRoute.php b/src/Content/Blog/BlogSeoUrlRoute.php
index 064445dd..0b07640d 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 ($prefix === null || $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 ConfigurationSEO 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
*/
diff --git a/tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php b/tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php
new file mode 100644
index 00000000..c5074caa
--- /dev/null
+++ b/tests/PHPUnit/Content/Blog/BlogSeoUrlRouteTest.php
@@ -0,0 +1,128 @@
+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 "news" prefix works correctly
+ */
+ public function testGetConfigWithNewsPrefix(): void
+ {
+ $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);
+ }
+
+ /**
+ * 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());
+ }
+}