diff --git a/codex.md b/codex.md index 2ccd6ab..699c13a 100644 --- a/codex.md +++ b/codex.md @@ -186,7 +186,10 @@ Shortcodes: ``` `[kb_docs]` bindet die Dokumentation in eine normale WordPress-Seite ein. -Die Ausgabe ist als Doku-App aufgebaut: Startseite rechts, persistente Sidebar links. Die Sidebar zeigt alle Produkte, deren Versionen und fuer die aktive Version alle Seiten, sodass man direkt von der Portalseite in die konkrete Doku springen kann. +Die Ausgabe ist als Doku-App aufgebaut: Startseite rechts, persistente Sidebar links. Die Sidebar zeigt alle Produkte und fuer die aktive Version alle Seiten, sodass man direkt von der Portalseite in die konkrete Doku springen kann. Die Version wird nicht in der Sidebar gewechselt, sondern per Dropdown im Kopf der jeweiligen Dokumentationsseite. + +Die Dokumentations-Startseite ist im Backend anpassbar. Sie enthaelt einen frei pflegbaren Anleitungstext zum Umgang mit der Dokumentation und einen Produktupdate-Bereich. Der Produktupdate-Bereich liest wahlweise einen konfigurierbaren RSS-/XML-Feed oder eine REST-/JSON-API aus und zeigt die neuesten Updates mit Produktname, Version, Datum und Changelog. Quelle, URL, Anzahl der Updates, Eintrag-/Listenpfad und die Feldpfade fuer Produktname, Version, Datum und Changelog sind im Backend frei definierbar. +Im Backend gibt es einen Testbutton fuer die konfigurierte Produktupdate-Quelle. Der Test zeigt HTTP-Status, Content-Type und einen gekuerzten Roh-Response an, damit die Feldzuordnung gegen die echte API-Antwort geprueft werden kann. ## 10. Admin-Einstellungen @@ -215,6 +218,9 @@ Einstellungen: - Automatische Synchronisation - Frontend-Design - Optionale eigene `theme.css` +- Dokumentations-Startseite mit Anleitungstext +- Produktupdate-Quelle als RSS/XML oder REST/JSON inkl. frei definierbarer Feldzuordnung fuer Produktname, Version, Datum und Changelog +- Testbutton fuer die Produktupdate-Quelle mit Anzeige der Rohantwort Es gibt keine Renderer-Modus-Einstellung mehr. Markdown wird direkt im Plugin verarbeitet. @@ -243,7 +249,11 @@ Es gibt keine Renderer-Modus-Einstellung mehr. Markdown wird direkt im Plugin ve - Bilder aus `images/` werden importiert und im HTML ersetzt. - Interne `.md`-Links funktionieren. - `/docs/` zeigt die Dokumentationsuebersicht. -- `[kb_docs]` zeigt eine Startseite mit persistenter Produkt-, Versions- und Seitennavigation. +- `[kb_docs]` zeigt eine Startseite mit persistenter Produkt- und Seitennavigation sowie Versionswechsel per Dropdown in der Dokumentationsseite. +- Die Dokumentations-Startseite zeigt den im Backend gepflegten Anleitungstext. +- Die Dokumentations-Startseite zeigt die neuesten Produktupdates aus einem konfigurierbaren RSS-/XML-Feed oder REST-/JSON-Endpunkt. +- Die XML- oder JSON-Felder fuer Produktname, Version, Datum und Changelog koennen im Backend frei zugeordnet werden. +- Die konfigurierte Produktupdate-Quelle kann im Backend getestet werden; Status, Content-Type und Rohantwort werden angezeigt. - `/docs/{product}/{version}/` zeigt die Startseite. - Im Backend koennen Produkte bei fehlerhaften Importen verwaltet und entfernt werden. - Synchronisation dupliziert unveraenderte Seiten nicht. diff --git a/kb-markdown-importer.zip b/kb-markdown-importer.zip index f001bc1..4b1ae93 100644 Binary files a/kb-markdown-importer.zip and b/kb-markdown-importer.zip differ diff --git a/kb-markdown-importer/assets/css/frontend.css b/kb-markdown-importer/assets/css/frontend.css index 09e02e8..26fb60c 100644 --- a/kb-markdown-importer/assets/css/frontend.css +++ b/kb-markdown-importer/assets/css/frontend.css @@ -77,7 +77,6 @@ } .kb-app-product__link, -.kb-app-version-list a, .kb-app-page-list a { display: block; border-radius: 6px; @@ -90,24 +89,16 @@ font-weight: 700; } -.kb-app-version-list a { +.kb-app-page-list a { padding: 6px 10px 6px 22px; color: var(--kb-muted); font-size: 14px; -} - -.kb-app-page-list a { - padding: 5px 10px 5px 36px; - color: var(--kb-muted); - font-size: 13px; line-height: 1.35; } .kb-app-product__link:hover, -.kb-app-version-list a:hover, .kb-app-page-list a:hover, .kb-app-product.is-active > .kb-app-product__link, -.kb-app-version-list li.is-active > a, .kb-app-page-list li.is-active > a { background: var(--kb-accent-soft); color: var(--kb-accent); @@ -129,6 +120,7 @@ gap: 16px; } +.kb-home-card, .kb-product-card, .kb-search { border: 1px solid var(--kb-border); @@ -138,6 +130,57 @@ box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04); } +.kb-docs-home-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(280px, 0.8fr); + gap: 16px; + margin-bottom: 16px; +} + +.kb-home-card h2 { + margin-top: 0; + font-size: 20px; +} + +.kb-home-intro__content p:last-child, +.kb-product-updates p:last-child { + margin-bottom: 0; +} + +.kb-product-updates__list { + margin: 0; + padding: 0; + list-style: none; +} + +.kb-product-updates__list li { + padding: 12px 0; + border-top: 1px solid var(--kb-border); +} + +.kb-product-updates__list li:first-child { + padding-top: 0; + border-top: 0; +} + +.kb-product-updates__meta { + display: flex; + flex-wrap: wrap; + gap: 6px 10px; + align-items: baseline; +} + +.kb-product-updates__meta strong { + color: var(--kb-text); +} + +.kb-product-updates__meta span, +.kb-product-updates__meta time, +.kb-empty-state { + color: var(--kb-muted); + font-size: 14px; +} + .kb-product-card h2 { margin-top: 0; font-size: 20px; @@ -173,6 +216,7 @@ padding: 8px 22px 16px 0; } +.kb-version-switcher select, .kb-sidebar select, .kb-search input { width: 100%; @@ -235,18 +279,48 @@ border-radius: 6px; } -.kb-doc-content { +.kb-doc-content, +.kb-docs-version { max-width: 860px; min-width: 0; + border: 1px solid var(--kb-border); + border-radius: var(--kb-radius, 8px); + padding: 22px; + background: var(--kb-surface); + box-shadow: var(--kb-shadow, 0 1px 2px rgba(16, 24, 40, 0.04)); +} + +.kb-doc-header { + display: flex; + gap: 20px; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 22px; +} + +.kb-doc-header__main { + min-width: 0; } -.kb-doc-content > h1 { +.kb-doc-header h1 { margin-top: 0; - margin-bottom: 22px; + margin-bottom: 0; font-size: clamp(28px, 4vw, 42px); line-height: 1.1; } +.kb-version-switcher { + flex: 0 0 190px; +} + +.kb-version-switcher label { + display: block; + margin-bottom: 6px; + color: var(--kb-muted); + font-size: 13px; + font-weight: 700; +} + .kb-rendered-content h1 { display: none; } @@ -307,6 +381,10 @@ grid-template-columns: 1fr; } + .kb-docs-home-grid { + grid-template-columns: 1fr; + } + .kb-app-sidebar { position: static; max-height: none; @@ -324,4 +402,12 @@ padding-right: 0; padding-bottom: 18px; } + + .kb-doc-header { + display: block; + } + + .kb-version-switcher { + margin-top: 16px; + } } diff --git a/kb-markdown-importer/assets/css/themes/obyte.css b/kb-markdown-importer/assets/css/themes/obyte.css index 7535710..319ed78 100644 --- a/kb-markdown-importer/assets/css/themes/obyte.css +++ b/kb-markdown-importer/assets/css/themes/obyte.css @@ -61,14 +61,17 @@ color: var(--kb-text); } -.kb-doc-content > h1 { +.kb-doc-header h1 { font-family: var(--kb-font-strong); font-weight: 600; letter-spacing: 0; } .kb-product-card, +.kb-home-card, .kb-search, +.kb-doc-content, +.kb-docs-version, .kb-app-sidebar { border-radius: var(--kb-radius); border-color: var(--kb-border); @@ -85,16 +88,13 @@ } .kb-app-product__link, -.kb-app-version-list a, .kb-app-page-list a { border-radius: 11px; } .kb-app-product__link:hover, -.kb-app-version-list a:hover, .kb-app-page-list a:hover, .kb-app-product.is-active > .kb-app-product__link, -.kb-app-version-list li.is-active > a, .kb-app-page-list li.is-active > a { background: rgba(0, 167, 230, 0.12); color: var(--kb-primary-shade); @@ -113,6 +113,7 @@ border-right-color: var(--kb-border); } +.kb-version-switcher select, .kb-sidebar select { border-radius: 11px; font-family: var(--kb-font-body); diff --git a/kb-markdown-importer/includes/Admin/SettingsPage.php b/kb-markdown-importer/includes/Admin/SettingsPage.php index db11663..e0c4865 100644 --- a/kb-markdown-importer/includes/Admin/SettingsPage.php +++ b/kb-markdown-importer/includes/Admin/SettingsPage.php @@ -38,6 +38,22 @@ final class SettingsPage $settings['design_accent_color'] = self::sanitizeHexColor((string) ($input['design_accent_color'] ?? '#F59C00'), '#F59C00'); $settings['design_radius'] = (string) max(0, min(32, (int) ($input['design_radius'] ?? 14))); $settings['custom_theme_css_url'] = esc_url_raw((string) ($input['custom_theme_css_url'] ?? '')); + $settings['docs_home_intro_title'] = sanitize_text_field((string) ($input['docs_home_intro_title'] ?? $settings['docs_home_intro_title'])); + $settings['docs_home_intro_content'] = wp_kses_post((string) ($input['docs_home_intro_content'] ?? $settings['docs_home_intro_content'])); + $settings['product_updates_source'] = in_array(($input['product_updates_source'] ?? 'rss'), ['rss', 'rest'], true) ? (string) $input['product_updates_source'] : 'rss'; + $settings['product_updates_feed_url'] = esc_url_raw((string) ($input['product_updates_feed_url'] ?? '')); + $settings['product_updates_feed_limit'] = (string) max(1, min(20, (int) ($input['product_updates_feed_limit'] ?? 5))); + $settings['product_updates_feed_item_path'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_item_path'] ?? 'channel/item'), 'channel/item'); + $settings['product_updates_feed_product_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_product_field'] ?? 'title'), 'title'); + $settings['product_updates_feed_version_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_version_field'] ?? 'category'), 'category'); + $settings['product_updates_feed_date_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_date_field'] ?? 'pubDate'), 'pubDate'); + $settings['product_updates_feed_changelog_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_changelog_field'] ?? 'description'), 'description'); + $settings['product_updates_rest_url'] = esc_url_raw((string) ($input['product_updates_rest_url'] ?? '')); + $settings['product_updates_rest_list_path'] = self::sanitizePathList((string) ($input['product_updates_rest_list_path'] ?? 'content,data,items'), 'content,data,items'); + $settings['product_updates_rest_product_field'] = self::sanitizePathList((string) ($input['product_updates_rest_product_field'] ?? 'product.name,productName,name'), 'product.name,productName,name'); + $settings['product_updates_rest_version_field'] = self::sanitizePathList((string) ($input['product_updates_rest_version_field'] ?? 'version,versionName,name'), 'version,versionName,name'); + $settings['product_updates_rest_date_field'] = self::sanitizePathList((string) ($input['product_updates_rest_date_field'] ?? 'releaseDate,date,updatedAt,createdAt'), 'releaseDate,date,updatedAt,createdAt'); + $settings['product_updates_rest_changelog_field'] = self::sanitizePathList((string) ($input['product_updates_rest_changelog_field'] ?? 'changelog,changeLog,description,changes'), 'changelog,changeLog,description,changes'); Plugin::syncCronSchedule($settings); if (($old['docs_base_slug'] ?? 'docs') !== $settings['docs_base_slug']) { @@ -57,6 +73,11 @@ final class SettingsPage self::handleConnectionTest(); } + $updatesTest = null; + if (isset($_POST['kb_markdown_test_product_updates']) && check_admin_referer('kb_markdown_test_product_updates')) { + $updatesTest = self::handleProductUpdatesTest(); + } + $settings = Plugin::settings(); ?>
@@ -113,6 +134,116 @@ final class SettingsPage +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

@@ -154,6 +285,21 @@ final class SettingsPage + + + +

+ + +
+

+

+
+ +

+ + + false, + 'title' => __('Keine Produktupdate-Quelle konfiguriert.', 'kb-markdown-importer'), + 'message' => __('Bitte zuerst eine RSS/XML- oder REST/JSON-URL speichern.', 'kb-markdown-importer'), + 'body' => '', + ]; + } + + $response = wp_remote_get($url, [ + 'timeout' => 12, + 'redirection' => 3, + 'user-agent' => 'KB Markdown Importer/' . KB_MARKDOWN_IMPORTER_VERSION, + ]); + + if (is_wp_error($response)) { + return [ + 'ok' => false, + 'title' => __('Produktupdate-Quelle nicht erreichbar.', 'kb-markdown-importer'), + 'message' => $response->get_error_message(), + 'body' => '', + ]; + } + + $status = (int) wp_remote_retrieve_response_code($response); + $contentType = (string) wp_remote_retrieve_header($response, 'content-type'); + $body = (string) wp_remote_retrieve_body($response); + $excerpt = substr($body, 0, 12000); + $validPayload = true; + $payloadNote = ''; + + if ('rest' === $source) { + json_decode($body, true); + $validPayload = JSON_ERROR_NONE === json_last_error(); + if (! $validPayload) { + $payloadNote = ' ' . sprintf( + /* translators: %s: JSON parser error message. */ + __('Die Antwort ist kein gültiges JSON: %s', 'kb-markdown-importer'), + json_last_error_msg() + ); + } + } + + $message = sprintf( + /* translators: 1: source type, 2: HTTP status code, 3: content type. */ + __('Quelle: %1$s | HTTP-Status: %2$d | Content-Type: %3$s', 'kb-markdown-importer'), + 'rest' === $source ? 'REST/JSON' : 'RSS/XML', + $status, + $contentType ?: '-' + ); + $message .= $payloadNote; + + if (strlen($body) > strlen($excerpt)) { + $message .= ' ' . __('Die Antwort wurde auf 12000 Zeichen gekürzt.', 'kb-markdown-importer'); + } + + $ok = $status >= 200 && $status < 300 && $validPayload; + + return [ + 'ok' => $ok, + 'title' => $ok ? __('Produktupdate-Quelle erreichbar.', 'kb-markdown-importer') : __('Produktupdate-Quelle nicht nutzbar.', 'kb-markdown-importer'), + 'message' => $message, + 'body' => $excerpt, + ]; + } + private static function formatConnectionError(\WP_Error $error): string { $message = $error->get_error_message(); @@ -206,4 +424,20 @@ final class SettingsPage return preg_match('/^#[0-9a-fA-F]{6}$/', $value) ? strtoupper($value) : $fallback; } + + private static function sanitizeXmlPath(string $value, string $fallback): string + { + $value = trim($value); + $value = preg_replace('/[^A-Za-z0-9_:@.\/-]/', '', $value) ?: ''; + + return '' !== $value ? $value : $fallback; + } + + private static function sanitizePathList(string $value, string $fallback): string + { + $value = trim($value); + $value = preg_replace('/[^A-Za-z0-9_:@.\/,-]/', '', $value) ?: ''; + + return '' !== $value ? $value : $fallback; + } } diff --git a/kb-markdown-importer/includes/Frontend/BreadcrumbBuilder.php b/kb-markdown-importer/includes/Frontend/BreadcrumbBuilder.php index 3d23d77..781ab91 100644 --- a/kb-markdown-importer/includes/Frontend/BreadcrumbBuilder.php +++ b/kb-markdown-importer/includes/Frontend/BreadcrumbBuilder.php @@ -11,7 +11,7 @@ final class BreadcrumbBuilder { $base = trim((string) Plugin::settings()['docs_base_slug'], '/'); $items = [ - sprintf('%s', esc_url(home_url('/' . $base . '/')), esc_html__('Docs', 'kb-markdown-importer')), + sprintf('%s', esc_url(home_url('/' . $base . '/')), esc_html__('Dokumentation', 'kb-markdown-importer')), ]; $path = $base; diff --git a/kb-markdown-importer/includes/Frontend/ProductUpdatesFeed.php b/kb-markdown-importer/includes/Frontend/ProductUpdatesFeed.php new file mode 100644 index 0000000..9565386 --- /dev/null +++ b/kb-markdown-importer/includes/Frontend/ProductUpdatesFeed.php @@ -0,0 +1,266 @@ + 8, + 'redirection' => 3, + 'user-agent' => 'KB Markdown Importer/' . KB_MARKDOWN_IMPORTER_VERSION, + ]); + + if (is_wp_error($response) || 200 !== (int) wp_remote_retrieve_response_code($response)) { + return []; + } + + $body = (string) wp_remote_retrieve_body($response); + $items = 'rest' === $source ? self::parseJson($body, $settings) : self::parseXml($body, $settings); + set_transient($cacheKey, $items, 15 * MINUTE_IN_SECONDS); + + return $items; + } + + private static function parseXml(string $xml, array $settings): array + { + if ('' === trim($xml) || ! class_exists(\DOMDocument::class)) { + return []; + } + + $previous = libxml_use_internal_errors(true); + $document = new \DOMDocument(); + $loaded = $document->loadXML($xml, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + + if (! $loaded) { + return []; + } + + $xpath = new \DOMXPath($document); + $itemNodes = $xpath->query(self::itemPath((string) ($settings['product_updates_feed_item_path'] ?? 'channel/item'))); + + if (! $itemNodes) { + return []; + } + + $limit = max(1, min(20, (int) ($settings['product_updates_feed_limit'] ?? 5))); + $items = []; + + foreach ($itemNodes as $itemNode) { + if (count($items) >= $limit) { + break; + } + + $date = self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_date_field'] ?? 'pubDate')); + $items[] = [ + 'product' => self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_product_field'] ?? 'title')), + 'version' => self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_version_field'] ?? 'category')), + 'date' => self::formatDate($date), + 'changelog' => self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_changelog_field'] ?? 'description')), + ]; + } + + return $items; + } + + private static function parseJson(string $json, array $settings): array + { + $data = json_decode($json, true); + + if (! is_array($data)) { + return []; + } + + $nodes = self::jsonList($data, (string) ($settings['product_updates_rest_list_path'] ?? 'content,data,items')); + + if (! $nodes) { + return []; + } + + $limit = max(1, min(20, (int) ($settings['product_updates_feed_limit'] ?? 5))); + $items = []; + + foreach ($nodes as $node) { + if (count($items) >= $limit || ! is_array($node)) { + break; + } + + $date = self::jsonField($node, (string) ($settings['product_updates_rest_date_field'] ?? 'releaseDate,date,updatedAt,createdAt')); + $items[] = [ + 'product' => self::jsonField($node, (string) ($settings['product_updates_rest_product_field'] ?? 'product.name,productName,name')), + 'version' => self::jsonField($node, (string) ($settings['product_updates_rest_version_field'] ?? 'version,versionName,name')), + 'date' => self::formatDate($date), + 'changelog' => self::jsonField($node, (string) ($settings['product_updates_rest_changelog_field'] ?? 'changelog,changeLog,description,changes')), + ]; + } + + return $items; + } + + private static function jsonList(array $data, string $paths): array + { + foreach (self::pathAlternatives($paths) as $path) { + $value = '' === $path ? $data : self::jsonValue($data, $path); + + if (is_array($value) && array_is_list($value)) { + return $value; + } + } + + return array_is_list($data) ? $data : []; + } + + private static function jsonField(array $data, string $paths): string + { + foreach (self::pathAlternatives($paths) as $path) { + $value = self::jsonValue($data, $path); + + if (is_scalar($value)) { + return trim(wp_strip_all_tags((string) $value)); + } + + if (is_array($value)) { + $text = implode(', ', array_filter(array_map(static fn ($item): string => is_scalar($item) ? (string) $item : '', $value))); + if ('' !== $text) { + return trim(wp_strip_all_tags($text)); + } + } + } + + return ''; + } + + private static function jsonValue(array $data, string $path): mixed + { + $value = $data; + + foreach (self::pathSegments($path) as $segment) { + if (! is_array($value) || ! array_key_exists($segment, $value)) { + return null; + } + + $value = $value[$segment]; + } + + return $value; + } + + private static function pathAlternatives(string $paths): array + { + return array_values(array_map('trim', explode(',', $paths))); + } + + private static function itemPath(string $path): string + { + $segments = self::pathSegments($path ?: 'channel/item'); + $first = (string) array_shift($segments); + $query = '//*[local-name()="' . self::localName($first ?: 'item') . '"]'; + + foreach ($segments as $segment) { + $query .= '/*[local-name()="' . self::localName((string) $segment) . '"]'; + } + + return $query; + } + + private static function fieldValue(\DOMXPath $xpath, \DOMNode $node, string $path): string + { + $segments = self::pathSegments($path); + + if (! $segments) { + return ''; + } + + $attribute = null; + $last = (string) end($segments); + + if (str_starts_with($last, '@')) { + $attribute = substr($last, 1); + array_pop($segments); + } + + $query = '.'; + foreach ($segments as $segment) { + $query .= '/*[local-name()="' . self::localName((string) $segment) . '"]'; + } + + $result = $xpath->query($query, $node); + $target = $result && $result->length > 0 ? $result->item(0) : null; + + if (! $target) { + return ''; + } + + if ($attribute && $target instanceof \DOMElement) { + return trim(wp_strip_all_tags(html_entity_decode($target->getAttribute($attribute), ENT_QUOTES | ENT_XML1, get_bloginfo('charset')))); + } + + return trim(wp_strip_all_tags(html_entity_decode($target->textContent, ENT_QUOTES | ENT_XML1, get_bloginfo('charset')))); + } + + private static function pathSegments(string $path): array + { + $path = trim(str_replace('.', '/', $path), '/ '); + + if ('' === $path) { + return []; + } + + return array_values(array_filter(array_map('trim', explode('/', $path)), static fn (string $segment): bool => '' !== $segment)); + } + + private static function localName(string $segment): string + { + $segment = trim($segment); + $parts = explode(':', $segment); + + return preg_replace('/[^A-Za-z0-9_-]/', '', (string) end($parts)) ?: ''; + } + + private static function formatDate(string $date): string + { + if ('' === $date) { + return ''; + } + + $timestamp = strtotime($date); + + if (! $timestamp) { + return $date; + } + + return wp_date((string) get_option('date_format'), $timestamp); + } +} diff --git a/kb-markdown-importer/includes/Frontend/Router.php b/kb-markdown-importer/includes/Frontend/Router.php index a20a3b7..f131932 100644 --- a/kb-markdown-importer/includes/Frontend/Router.php +++ b/kb-markdown-importer/includes/Frontend/Router.php @@ -194,9 +194,13 @@ final class Router private function captureIndex(): string { + $settings = Plugin::settings(); + return (new TemplateLoader())->capture('documentation-index', [ 'products' => self::productsWithVersions(), - 'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'), + 'settings' => $settings, + 'updates' => ProductUpdatesFeed::items($settings), + 'base_slug' => trim((string) $settings['docs_base_slug'], '/'), 'url_builder' => UrlBuilder::class, ]); } @@ -245,6 +249,7 @@ final class Router return (new TemplateLoader())->capture('version', [ 'product' => $product, 'version' => $version, + 'versions' => $this->versionsForProduct($productSlug), 'pages' => $this->pagesForVersion($productSlug, $versionSlug), 'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'), 'url_builder' => UrlBuilder::class, @@ -332,7 +337,7 @@ final class Router { status_header(404); (new TemplateLoader())->render('search', [ - 'title' => __('Documentation page not found.', 'kb-markdown-importer'), + 'title' => __('Dokumentationsseite nicht gefunden.', 'kb-markdown-importer'), 'results' => [], 'query' => '', ]); @@ -342,7 +347,7 @@ final class Router { status_header(404); return (new TemplateLoader())->capture('search', [ - 'title' => __('Documentation page not found.', 'kb-markdown-importer'), + 'title' => __('Dokumentationsseite nicht gefunden.', 'kb-markdown-importer'), 'results' => [], 'query' => '', ]); diff --git a/kb-markdown-importer/includes/Frontend/SearchController.php b/kb-markdown-importer/includes/Frontend/SearchController.php index fdf9334..f5d171e 100644 --- a/kb-markdown-importer/includes/Frontend/SearchController.php +++ b/kb-markdown-importer/includes/Frontend/SearchController.php @@ -17,7 +17,7 @@ final class SearchController $results = $query ? self::search($query, sanitize_text_field(wp_unslash((string) ($_GET['product'] ?? ''))), sanitize_text_field(wp_unslash((string) ($_GET['version'] ?? '')))) : []; return (new TemplateLoader())->capture('search', [ - 'title' => __('Search Documentation', 'kb-markdown-importer'), + 'title' => __('Dokumentation durchsuchen', 'kb-markdown-importer'), 'query' => $query, 'results' => $results, ]); diff --git a/kb-markdown-importer/includes/Frontend/TemplateLoader.php b/kb-markdown-importer/includes/Frontend/TemplateLoader.php index 0b397f1..b9860e4 100644 --- a/kb-markdown-importer/includes/Frontend/TemplateLoader.php +++ b/kb-markdown-importer/includes/Frontend/TemplateLoader.php @@ -11,7 +11,7 @@ final class TemplateLoader if (! is_readable($path)) { status_header(500); - echo esc_html__('Knowledgebase template missing.', 'kb-markdown-importer'); + echo esc_html__('Dokumentationsvorlage fehlt.', 'kb-markdown-importer'); return; } diff --git a/kb-markdown-importer/includes/Settings.php b/kb-markdown-importer/includes/Settings.php index c1e64ad..b6f68ae 100644 --- a/kb-markdown-importer/includes/Settings.php +++ b/kb-markdown-importer/includes/Settings.php @@ -22,6 +22,22 @@ final class Settings 'design_accent_color' => '#F59C00', 'design_radius' => '14', 'custom_theme_css_url' => '', + 'docs_home_intro_title' => 'So nutzt du die Dokumentation', + 'docs_home_intro_content' => '

Wähle links ein Produkt aus und öffne anschließend die passende Version. Innerhalb einer Version findest du die zugehörigen Seiten der Dokumentation in der Navigation.

', + 'product_updates_source' => 'rss', + 'product_updates_feed_url' => '', + 'product_updates_feed_limit' => '5', + 'product_updates_feed_item_path' => 'channel/item', + 'product_updates_feed_product_field' => 'title', + 'product_updates_feed_version_field' => 'category', + 'product_updates_feed_date_field' => 'pubDate', + 'product_updates_feed_changelog_field' => 'description', + 'product_updates_rest_url' => '', + 'product_updates_rest_list_path' => 'content,data,items', + 'product_updates_rest_product_field' => 'product.name,productName,name', + 'product_updates_rest_version_field' => 'version,versionName,name', + 'product_updates_rest_date_field' => 'releaseDate,date,updatedAt,createdAt', + 'product_updates_rest_changelog_field' => 'changelog,changeLog,description,changes', ]; } } diff --git a/kb-markdown-importer/templates/docs-app.php b/kb-markdown-importer/templates/docs-app.php index 83e902c..5b915dc 100644 --- a/kb-markdown-importer/templates/docs-app.php +++ b/kb-markdown-importer/templates/docs-app.php @@ -6,9 +6,9 @@ $active_version_slug = (string) ($active_version_slug ?? ''); $active_page_slug = (string) ($active_page_slug ?? ''); ?>
-