siteOrigin, '/'); $home = $this->settings->homePath(); $entries = []; // Only published pages that are allowed to be indexed. A draft or a noindex page in the // sitemap would ask a search engine to fetch something it must not list. foreach ($this->db->fetchAllAssociative( "SELECT p.path, p.type, p.published_at, r.created_at FROM page p JOIN revision r ON r.id = p.published_revision_id AND r.page_id = p.id WHERE p.website_id = ? AND p.robots = 'index' AND p.deleted_at IS NULL ORDER BY p.path", ['default'], ) as $row) { $entries[] = [ 'loc' => $origin.$this->settings->publicPath($row['path'], $home), 'lastmod' => substr((string) ($row['published_at'] ?? $row['created_at']), 0, 10), ]; } // Archives exist only while they list something. if ([] !== $this->posts->published(1, null, 1)['posts']) { $entries[] = ['loc' => $origin.'/beitraege', 'lastmod' => null]; } foreach ($this->db->fetchAllAssociative( 'SELECT DISTINCT t.kind, t.slug FROM term t JOIN page_term pt ON pt.term_id = t.id JOIN page p ON p.id = pt.page_id AND p.published_revision_id IS NOT NULL AND p.deleted_at IS NULL ORDER BY t.kind, t.slug', ) as $row) { $entries[] = ['loc' => $origin.'/'.PostRepository::ARCHIVE_PREFIX[$row['kind']].'/'.$row['slug'], 'lastmod' => null]; } return $this->xml($this->renderView('seo/sitemap.xml.twig', ['entries' => $entries])); } #[Route('/robots.txt', name: 'robots', methods: ['GET'])] public function robots(): Response { $settings = $this->settings->load(); // Die Verwaltung wird hier nur genannt, solange sie „admin" heisst - diese Adresse kennt // ohnehin jeder Scanner. Wer sie umbenannt hat, will sie nicht in einer oeffentlichen // Datei wiederfinden: robots.txt ist das erste, was ein Angreifer liest. Vom Indexieren // haelt die Verwaltung ihr eigener Kopf `X-Robots-Tag: noindex` ab. $lines = ['User-agent: *']; if ('admin' === $this->adminPath) { $lines[] = 'Disallow: /admin'; } $lines[] = 'Disallow: /formular/'; if ('' !== $settings['robotsExtra']) { $lines[] = $settings['robotsExtra']; } $lines[] = ''; $lines[] = 'Sitemap: '.rtrim($this->siteOrigin, '/').'/sitemap.xml'; return $this->text(implode("\n", $lines)."\n"); } /** * A machine readable summary of public content. Optional and switched off by default. It is * no ranking factor and no requirement for any search product; it simply saves an agent the * crawl. Only published, indexable pages appear. */ #[Route('/llms.txt', name: 'llms', methods: ['GET'])] public function llms(): Response { $settings = $this->settings->load(); if (!$settings['llmsTxt']) { throw $this->createNotFoundException('llms.txt ist für diese Website nicht aktiviert.'); } $origin = rtrim($this->siteOrigin, '/'); $home = $settings['homePath']; $name = (string) $this->db->fetchOne('SELECT name FROM website WHERE id = ?', ['default']); $lines = ['# '.('' !== $settings['legalName'] ? $settings['legalName'] : $name), '']; if ('' !== $settings['description']) { $lines[] = '> '.$settings['description']; $lines[] = ''; } foreach (['page' => '## Seiten', 'post' => '## Beiträge'] as $type => $heading) { $rows = $this->db->fetchAllAssociative( "SELECT p.path, p.excerpt, r.document FROM page p JOIN revision r ON r.id = p.published_revision_id AND r.page_id = p.id WHERE p.website_id = ? AND p.robots = 'index' AND p.deleted_at IS NULL AND p.type = ? ORDER BY p.path", ['default', $type], ); if ([] === $rows) { continue; } $lines[] = $heading; foreach ($rows as $row) { $document = json_decode($row['document'], true, 512, JSON_THROW_ON_ERROR); $summary = trim((string) ($row['excerpt'] ?? $document['description'] ?? '')); $lines[] = sprintf('- [%s](%s)%s', $document['title'] ?? $row['path'], $origin.$this->settings->publicPath($row['path'], $home), '' === $summary ? '' : ': '.$summary); } $lines[] = ''; } return $this->text(implode("\n", $lines)); } private function text(string $body): Response { return $this->harden(new Response($body, 200, ['Content-Type' => 'text/plain; charset=UTF-8'])); } private function xml(string $body): Response { return $this->harden(new Response($body, 200, ['Content-Type' => 'application/xml; charset=UTF-8'])); } private function harden(Response $response): Response { $response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('Cache-Control', 'no-store'); return $response; } }