From d4664347d070feafbaddd20109ee41ab8d8a759a Mon Sep 17 00:00:00 2001 From: Artur Kyryliuk Date: Sun, 9 Aug 2026 18:00:10 +0200 Subject: [PATCH 1/3] Fixed bun audit failures --- bun.lock | 17 +++++++++-------- package.json | 4 +++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index 292e9d590..e2b503409 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "bewelcome-rox", @@ -26,7 +25,7 @@ "@tomickigrzegorz/autocomplete": "^3.3.2", "@trevoreyre/autocomplete-js": "^3.0.3", "@types/geojson": "^7946.0.16", - "@types/leaflet": "^1.9.21", + "@types/leaflet": "^1.9.22", "@types/leaflet-fullscreen": "^1.0.10", "add-to-homescreen": "^3.2.6", "autoprefixer": "^10.5.4", @@ -46,6 +45,7 @@ "jarallax": "^1.12.8", "jquery-mousewheel": "^3.2.2", "jquery-ui": "~1.13.3", + "js-yaml": "4.3.1", "leaflet": "^1.9.4", "leaflet-polylinedecorator": "^1.6.0", "leaflet.fullscreen": "^2.4.0", @@ -97,8 +97,9 @@ "fast-uri": "^3.1.5", "http-proxy-middleware": "^3.0.6", "immutable": "^5.1.8", - "js-yaml": "^4.3.0", + "js-yaml": "4.3.1", "minimatch": "^3.1.3", + "nanoid": "^3.3.17", "postcss": "^8.5.24", "qs": "^6.15.2", "serialize-javascript": "^7.0.2", @@ -949,7 +950,7 @@ "bootstrap": ["bootstrap@5.3.8", "", { "peerDependencies": { "@popperjs/core": "^2.11.8" } }, "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg=="], - "bootstrap-autohidingnavbar": ["bootstrap-autohidingnavbar@github:istvan-ujjmeszaros/bootstrap-autohidingnavbar#0077987", {}, "istvan-ujjmeszaros-bootstrap-autohidingnavbar-0077987", "sha512-5nsIOoG8CA47Uq4joJ+89UPzX1NGzIxdlxSJv3kODFKDMoDirXH7pUoDiIk36712tgz4aTLZZkEYBSbXGzVgmA=="], + "bootstrap-autohidingnavbar": ["bootstrap-autohidingnavbar@github:istvan-ujjmeszaros/bootstrap-autohidingnavbar#0077987", {}, "istvan-ujjmeszaros-bootstrap-autohidingnavbar-0077987"], "bootstrap-icons": ["bootstrap-icons@1.13.1", "", {}, "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw=="], @@ -1519,7 +1520,7 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], @@ -1747,7 +1748,7 @@ "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], - "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], @@ -2083,7 +2084,7 @@ "shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="], - "show-more-read": ["show-more-read@github:tomik23/show-more#1f068b0", {}, "tomickigrzegorz-show-more-1f068b0", "sha512-7bQaWd3Ko9c6t+Sm3n5c3eweHLwRtqEEy5/Sdm9bXtuGyI4q1WSUmshhzmSKG13bOaf86QX3+JFNBvndj+9k/A=="], + "show-more-read": ["show-more-read@github:tomik23/show-more#1f068b0", {}, "tomickigrzegorz-show-more-1f068b0"], "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], @@ -2279,7 +2280,7 @@ "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], - "ux-translator": ["ux-translator@github:symfony/ux-translator#411b360", {}, "symfony-ux-translator-411b360", "sha512-S6WJguhbUjvyO4l00jb/V8HDVmNTe4BQ3+zsu/VeldT2KBI1UXXx+5wXNuPjRgCDQfaPEOrM54bYleDk0/kNvw=="], + "ux-translator": ["ux-translator@github:symfony/ux-translator#411b360", {}, "symfony-ux-translator-411b360"], "vanilla-calendar-pro": ["vanilla-calendar-pro@3.1.0", "", {}, "sha512-yXDtCaedcKz6i5OOdWGwui0C8MAmjXjj7JzKZyjDlkczSRqnhI8BDGFygqT2K+qL1uY7R2fLYlTlxA6oyFs2yg=="], diff --git a/package.json b/package.json index 2b34d658b..f91d949d4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "gsap": "^3.15.0", "imagesloaded": "^4.1.4", "improved-yarn-audit": "^2.3.3", + "js-yaml": "4.3.1", "intl-messageformat": "^10.7.18", "jarallax": "^1.12.8", "jquery-mousewheel": "^3.2.2", @@ -95,7 +96,8 @@ "fast-uri": "^3.1.5", "immutable": "^5.1.8", "svgo": "^4.0.2", - "js-yaml": "^4.3.0", + "js-yaml": "4.3.1", + "nanoid": "^3.3.17", "body-parser": "^1.20.6", "minimatch": "^3.1.3", "serialize-javascript": "^7.0.2", From 072b2d62c397382ad3536c7b7ed6fefef29e1812 Mon Sep 17 00:00:00 2001 From: Artur Kyryliuk Date: Sun, 9 Aug 2026 18:09:20 +0200 Subject: [PATCH 2/3] Sitemap generator including newsletters --- composer.json | 1 + composer.lock | 3 +- config/sitemap.yaml | 43 +++++ src/Command/GenerateSitemapCommand.php | 236 +++++++++++++++++++++++++ 4 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 config/sitemap.yaml create mode 100644 src/Command/GenerateSitemapCommand.php diff --git a/composer.json b/composer.json index d466816ee..10a4ae4cb 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "ext-pdo": "*", "ext-pdo_mysql": "*", "ext-session": "*", + "ext-xmlwriter": "*", "ext-xsl": "*", "ext-zip": "*", "amphp/http-client": "^5.2", diff --git a/composer.lock b/composer.lock index ae5248ce1..8605e975a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e9333618d6e15152a47f9be23a98ab8e", + "content-hash": "28e69c78a2a70b211eb37c50960b4316", "packages": [ { "name": "amphp/amp", @@ -19428,6 +19428,7 @@ "ext-pdo": "*", "ext-pdo_mysql": "*", "ext-session": "*", + "ext-xmlwriter": "*", "ext-xsl": "*", "ext-zip": "*" }, diff --git a/config/sitemap.yaml b/config/sitemap.yaml new file mode 100644 index 000000000..876bc9a9a --- /dev/null +++ b/config/sitemap.yaml @@ -0,0 +1,43 @@ +# Sitemap configuration +# Defines static routes and their priorities for the sitemap generator. +# +# Priority levels: +# 1.0 - Main pages (home, signup, login, etc.) +# 0.8 - Secondary pages (about sections, safety pages, etc.) +# 0.6 - Tertiary content (FAQ articles, newsletters, etc.) + +sitemap: + # Static routes - URLs that don't require database lookup + static_routes: + - { path: '/', priority: 1.0 } + - { path: '/signup', priority: 1.0 } + - { path: '/about', priority: 1.0 } + - { path: '/terms', priority: 1.0 } + - { path: '/privacy', priority: 1.0 } + - { path: '/impressum', priority: 1.0 } + - { path: '/safety', priority: 1.0 } + - { path: '/volunteer', priority: 1.0 } + - { path: '/login', priority: 1.0 } + - { path: '/stats', priority: 1.0 } + - { path: '/about/faq', priority: 0.8 } + - { path: '/about/getactive', priority: 0.8 } + - { path: '/about/thepeople', priority: 0.8 } + - { path: '/about/credits', priority: 0.8 } + - { path: '/safety/basics', priority: 0.8 } + - { path: '/safety/whattodo', priority: 0.8 } + - { path: '/safety/team', priority: 0.8 } + - { path: '/safety/faq', priority: 0.8 } + - { path: '/safety/tips', priority: 0.8 } + - { path: '/about/statistics', priority: 0.8 } + - { path: '/about/commentguidelines', priority: 0.8 } + - { path: '/press-information', priority: 0.8 } + - { path: '/feedback', priority: 0.8 } + + # Dynamic routes - URLs generated from database content + dynamic_routes: + faq_categories: + # FAQ categories from database + priority: 0.6 + newsletters: + # Newsletter articles with language variants + priority: 0.6 \ No newline at end of file diff --git a/src/Command/GenerateSitemapCommand.php b/src/Command/GenerateSitemapCommand.php new file mode 100644 index 000000000..e98f946ef --- /dev/null +++ b/src/Command/GenerateSitemapCommand.php @@ -0,0 +1,236 @@ +sitemapConfig = $this->loadSitemapConfig(); + } + + protected function configure(): void + { + $this + ->addOption('base-url', 'b', InputOption::VALUE_OPTIONAL, 'Base URL for the sitemap (scheme + host)', null) + ->setHelp('This command generates a sitemap.xml file with all publicly accessible URLs including newsletters and FAQ categories.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln([ + 'Generating Sitemap', + '==================', + '', + ]); + + // Get base URL + $baseUrl = $input->getOption('base-url'); + if (!$baseUrl) { + $context = $this->router->getContext(); + $scheme = $context->getScheme(); + $host = $context->getHost(); + $port = 'https' === $scheme ? $context->getHttpsPort() : $context->getHttpPort(); + $baseUrl = $scheme . '://' . $host; + if (80 !== $port && 443 !== $port) { + $baseUrl .= ':' . $port; + } + } + // Remove index.php or similar from base URL if present + $baseUrl = preg_replace('#/index\.php$#', '', $baseUrl); + + $output->writeln("Base URL: {$baseUrl}"); + + // Collect all URLs + $urls = $this->collectUrls($output); + + // Generate XML + $xml = $this->generateSitemapXml($urls, $baseUrl); + + // Write to file + $sitemapPath = $this->projectDir . '/public/sitemap.xml'; + file_put_contents($sitemapPath, $xml); + + $output->writeln(''); + $output->writeln('Sitemap generated with ' . \count($urls) . ' URLs'); + $output->writeln("Written to: {$sitemapPath}"); + + return Command::SUCCESS; + } + + /** + * Load sitemap configuration from YAML file. + */ + private function loadSitemapConfig(): array + { + $configFile = $this->projectDir . '/config/sitemap.yaml'; + if (!file_exists($configFile)) { + return ['sitemap' => ['static_routes' => [], 'dynamic_routes' => []]]; + } + + $yaml = Yaml::parseFile($configFile); + + return $yaml['sitemap'] ?? []; + } + + /** + * Collect all URLs from static routes and dynamic content. + */ + private function collectUrls(OutputInterface $output): array + { + $urls = []; + $now = new DateTime(); + + // Add static public routes from config + $staticRoutes = $this->sitemapConfig['static_routes'] ?? []; + foreach ($staticRoutes as $route) { + $urls[] = [ + 'loc' => $route['path'], + 'lastmod' => $now, + 'priority' => $route['priority'], + ]; + } + + $output->writeln('Static routes: ' . \count($staticRoutes)); + + // Add FAQ category URLs (dynamic) + $faqPriority = $this->sitemapConfig['dynamic_routes']['faq_categories']['priority'] ?? 0.6; + $faqUrls = $this->getFaqCategoryUrls($faqPriority); + $urls = array_merge($urls, $faqUrls); + $output->writeln('FAQ categories: ' . \count($faqUrls)); + + // Add Newsletter URLs (dynamic with language variants) + $newsletterPriority = $this->sitemapConfig['dynamic_routes']['newsletters']['priority'] ?? 0.6; + $newsletterUrls = $this->getNewsletterUrls($newsletterPriority); + $urls = array_merge($urls, $newsletterUrls); + $output->writeln('Newsletter URLs: ' . \count($newsletterUrls)); + + return $urls; + } + + /** + * Get all FAQ category URLs. + */ + private function getFaqCategoryUrls(float $priority): array + { + $urls = []; + $faqCategories = $this->entityManager->getRepository(FaqCategory::class) + ->findBy([], ['sortOrder' => 'ASC']); + + foreach ($faqCategories as $category) { + $urls[] = [ + 'loc' => '/faq/' . $category->getId(), + 'lastmod' => new DateTime(), + 'priority' => $priority, + ]; + } + + return $urls; + } + + /** + * Get all newsletter URLs with their available language translations. + */ + private function getNewsletterUrls(float $priority): array + { + $urls = []; + $newsletterRepository = $this->entityManager->getRepository(Newsletter::class); + $translationRepository = $this->entityManager->getRepository(Word::class); + + $newsletters = $newsletterRepository->findAllPublished(); + + foreach ($newsletters as $newsletter) { + $id = $newsletter->getId(); + $name = $newsletter->getName(); + $created = $newsletter->getCreated(); + + // Find all languages this newsletter is translated into + $translations = $translationRepository->findBy([ + 'code' => strtolower('Broadcast_body_' . $name), + ]); + + foreach ($translations as $translation) { + $languageCode = $translation->getLanguage()->getShortCode(); + $urls[] = [ + 'loc' => "/newsletters/{$id}/{$languageCode}", + 'lastmod' => $created instanceof \Carbon\Carbon ? $created->toDateTime() : $created, + 'priority' => $priority, + ]; + } + } + + return $urls; + } + + /** + * Generate the sitemap XML content. + */ + private function generateSitemapXml(array $urls, string $baseUrl): string + { + $xml = new XMLWriter(); + $xml->openMemory(); + $xml->startDocument('1.0', 'UTF-8'); + $xml->setIndent(true); + $xml->setIndentString(' '); + + $xml->startElement('urlset'); + $xml->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); + + foreach ($urls as $urlData) { + $xml->startElement('url'); + + // Location + $xml->startElement('loc'); + $fullUrl = rtrim($baseUrl, '/') . $urlData['loc']; + $xml->text($fullUrl); + $xml->endElement(); // loc + + // Last modified + $xml->startElement('lastmod'); + $lastmod = $urlData['lastmod']; + if ($lastmod instanceof \Carbon\Carbon) { + $lastmod = $lastmod->toDateTime(); + } + $xml->text($lastmod->format(DateTime::W3C)); + $xml->endElement(); // lastmod + + // Priority + $xml->startElement('priority'); + $xml->text((string) $urlData['priority']); + $xml->endElement(); // priority + + $xml->endElement(); // url + } + + $xml->endElement(); // urlset + $xml->endDocument(); + + return $xml->outputMemory(true); + } +} From 0e3f80fb5bc461230f1612c86826f76cb3509be4 Mon Sep 17 00:00:00 2001 From: Artur Kyryliuk Date: Sun, 9 Aug 2026 21:46:14 +0200 Subject: [PATCH 3/3] Sitemap generator including newsletters --- config/sitemap.yaml | 2 +- src/Command/GenerateSitemapCommand.php | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/sitemap.yaml b/config/sitemap.yaml index 876bc9a9a..fa13a5840 100644 --- a/config/sitemap.yaml +++ b/config/sitemap.yaml @@ -40,4 +40,4 @@ sitemap: priority: 0.6 newsletters: # Newsletter articles with language variants - priority: 0.6 \ No newline at end of file + priority: 0.6 diff --git a/src/Command/GenerateSitemapCommand.php b/src/Command/GenerateSitemapCommand.php index e98f946ef..24dcdfcac 100644 --- a/src/Command/GenerateSitemapCommand.php +++ b/src/Command/GenerateSitemapCommand.php @@ -5,6 +5,7 @@ use App\Entity\FaqCategory; use App\Entity\Newsletter; use App\Entity\Word; +use Carbon\Carbon; use DateTime; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Attribute\AsCommand; @@ -179,7 +180,7 @@ private function getNewsletterUrls(float $priority): array $languageCode = $translation->getLanguage()->getShortCode(); $urls[] = [ 'loc' => "/newsletters/{$id}/{$languageCode}", - 'lastmod' => $created instanceof \Carbon\Carbon ? $created->toDateTime() : $created, + 'lastmod' => $created instanceof Carbon ? $created->toDateTime() : $created, 'priority' => $priority, ]; } @@ -214,7 +215,7 @@ private function generateSitemapXml(array $urls, string $baseUrl): string // Last modified $xml->startElement('lastmod'); $lastmod = $urlData['lastmod']; - if ($lastmod instanceof \Carbon\Carbon) { + if ($lastmod instanceof Carbon) { $lastmod = $lastmod->toDateTime(); } $xml->text($lastmod->format(DateTime::W3C));