Style anpassungen für die Dokumentationsseite und die Produktseite. Hinzufügen eines Feeds für Produktaktualisierungen. Aktualisierung der Router- und Suchcontroller-Logik, um die neuen Seiten zu unterstützen. Anpassung der Admin-Einstellungen für die Dokumentationsseite.

This commit is contained in:
Sven Steinert
2026-05-20 15:07:59 +02:00
parent 3ff9146a63
commit 1d4cf6e727
17 changed files with 737 additions and 65 deletions

View File

@@ -186,7 +186,10 @@ Shortcodes:
``` ```
`[kb_docs]` bindet die Dokumentation in eine normale WordPress-Seite ein. `[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 ## 10. Admin-Einstellungen
@@ -215,6 +218,9 @@ Einstellungen:
- Automatische Synchronisation - Automatische Synchronisation
- Frontend-Design - Frontend-Design
- Optionale eigene `theme.css` - 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. 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. - Bilder aus `images/` werden importiert und im HTML ersetzt.
- Interne `.md`-Links funktionieren. - Interne `.md`-Links funktionieren.
- `/docs/` zeigt die Dokumentationsuebersicht. - `/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. - `/docs/{product}/{version}/` zeigt die Startseite.
- Im Backend koennen Produkte bei fehlerhaften Importen verwaltet und entfernt werden. - Im Backend koennen Produkte bei fehlerhaften Importen verwaltet und entfernt werden.
- Synchronisation dupliziert unveraenderte Seiten nicht. - Synchronisation dupliziert unveraenderte Seiten nicht.

Binary file not shown.

View File

@@ -77,7 +77,6 @@
} }
.kb-app-product__link, .kb-app-product__link,
.kb-app-version-list a,
.kb-app-page-list a { .kb-app-page-list a {
display: block; display: block;
border-radius: 6px; border-radius: 6px;
@@ -90,24 +89,16 @@
font-weight: 700; font-weight: 700;
} }
.kb-app-version-list a { .kb-app-page-list a {
padding: 6px 10px 6px 22px; padding: 6px 10px 6px 22px;
color: var(--kb-muted); color: var(--kb-muted);
font-size: 14px; font-size: 14px;
}
.kb-app-page-list a {
padding: 5px 10px 5px 36px;
color: var(--kb-muted);
font-size: 13px;
line-height: 1.35; line-height: 1.35;
} }
.kb-app-product__link:hover, .kb-app-product__link:hover,
.kb-app-version-list a:hover,
.kb-app-page-list a:hover, .kb-app-page-list a:hover,
.kb-app-product.is-active > .kb-app-product__link, .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 { .kb-app-page-list li.is-active > a {
background: var(--kb-accent-soft); background: var(--kb-accent-soft);
color: var(--kb-accent); color: var(--kb-accent);
@@ -129,6 +120,7 @@
gap: 16px; gap: 16px;
} }
.kb-home-card,
.kb-product-card, .kb-product-card,
.kb-search { .kb-search {
border: 1px solid var(--kb-border); border: 1px solid var(--kb-border);
@@ -138,6 +130,57 @@
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04); 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 { .kb-product-card h2 {
margin-top: 0; margin-top: 0;
font-size: 20px; font-size: 20px;
@@ -173,6 +216,7 @@
padding: 8px 22px 16px 0; padding: 8px 22px 16px 0;
} }
.kb-version-switcher select,
.kb-sidebar select, .kb-sidebar select,
.kb-search input { .kb-search input {
width: 100%; width: 100%;
@@ -235,18 +279,48 @@
border-radius: 6px; border-radius: 6px;
} }
.kb-doc-content { .kb-doc-content,
.kb-docs-version {
max-width: 860px; max-width: 860px;
min-width: 0; 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-top: 0;
margin-bottom: 22px; margin-bottom: 0;
font-size: clamp(28px, 4vw, 42px); font-size: clamp(28px, 4vw, 42px);
line-height: 1.1; 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 { .kb-rendered-content h1 {
display: none; display: none;
} }
@@ -307,6 +381,10 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.kb-docs-home-grid {
grid-template-columns: 1fr;
}
.kb-app-sidebar { .kb-app-sidebar {
position: static; position: static;
max-height: none; max-height: none;
@@ -324,4 +402,12 @@
padding-right: 0; padding-right: 0;
padding-bottom: 18px; padding-bottom: 18px;
} }
.kb-doc-header {
display: block;
}
.kb-version-switcher {
margin-top: 16px;
}
} }

View File

@@ -61,14 +61,17 @@
color: var(--kb-text); color: var(--kb-text);
} }
.kb-doc-content > h1 { .kb-doc-header h1 {
font-family: var(--kb-font-strong); font-family: var(--kb-font-strong);
font-weight: 600; font-weight: 600;
letter-spacing: 0; letter-spacing: 0;
} }
.kb-product-card, .kb-product-card,
.kb-home-card,
.kb-search, .kb-search,
.kb-doc-content,
.kb-docs-version,
.kb-app-sidebar { .kb-app-sidebar {
border-radius: var(--kb-radius); border-radius: var(--kb-radius);
border-color: var(--kb-border); border-color: var(--kb-border);
@@ -85,16 +88,13 @@
} }
.kb-app-product__link, .kb-app-product__link,
.kb-app-version-list a,
.kb-app-page-list a { .kb-app-page-list a {
border-radius: 11px; border-radius: 11px;
} }
.kb-app-product__link:hover, .kb-app-product__link:hover,
.kb-app-version-list a:hover,
.kb-app-page-list a:hover, .kb-app-page-list a:hover,
.kb-app-product.is-active > .kb-app-product__link, .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 { .kb-app-page-list li.is-active > a {
background: rgba(0, 167, 230, 0.12); background: rgba(0, 167, 230, 0.12);
color: var(--kb-primary-shade); color: var(--kb-primary-shade);
@@ -113,6 +113,7 @@
border-right-color: var(--kb-border); border-right-color: var(--kb-border);
} }
.kb-version-switcher select,
.kb-sidebar select { .kb-sidebar select {
border-radius: 11px; border-radius: 11px;
font-family: var(--kb-font-body); font-family: var(--kb-font-body);

View File

@@ -38,6 +38,22 @@ final class SettingsPage
$settings['design_accent_color'] = self::sanitizeHexColor((string) ($input['design_accent_color'] ?? '#F59C00'), '#F59C00'); $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['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['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); Plugin::syncCronSchedule($settings);
if (($old['docs_base_slug'] ?? 'docs') !== $settings['docs_base_slug']) { if (($old['docs_base_slug'] ?? 'docs') !== $settings['docs_base_slug']) {
@@ -57,6 +73,11 @@ final class SettingsPage
self::handleConnectionTest(); 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(); $settings = Plugin::settings();
?> ?>
<div class="wrap"> <div class="wrap">
@@ -113,6 +134,116 @@ final class SettingsPage
</td> </td>
</tr> </tr>
</table> </table>
<h2><?php esc_html_e('Dokumentations-Startseite', 'kb-markdown-importer'); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="docs_home_intro_title"><?php esc_html_e('Anleitungs-Titel', 'kb-markdown-importer'); ?></label></th>
<td><input class="regular-text" id="docs_home_intro_title" name="kb_markdown_importer_settings[docs_home_intro_title]" type="text" value="<?php echo esc_attr($settings['docs_home_intro_title']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="docs_home_intro_content"><?php esc_html_e('Anleitungstext', 'kb-markdown-importer'); ?></label></th>
<td>
<?php
wp_editor((string) $settings['docs_home_intro_content'], 'docs_home_intro_content', [
'textarea_name' => 'kb_markdown_importer_settings[docs_home_intro_content]',
'textarea_rows' => 7,
'media_buttons' => false,
'teeny' => true,
]);
?>
<p class="description"><?php esc_html_e('Dieser Text erscheint oben auf der Dokumentations-Startseite.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="product_updates_source"><?php esc_html_e('Update-Quelle', 'kb-markdown-importer'); ?></label></th>
<td>
<select id="product_updates_source" name="kb_markdown_importer_settings[product_updates_source]">
<option value="rss" <?php selected($settings['product_updates_source'], 'rss'); ?>><?php esc_html_e('RSS/XML', 'kb-markdown-importer'); ?></option>
<option value="rest" <?php selected($settings['product_updates_source'], 'rest'); ?>><?php esc_html_e('REST/JSON', 'kb-markdown-importer'); ?></option>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="product_updates_feed_url"><?php esc_html_e('RSS/XML-Feed URL', 'kb-markdown-importer'); ?></label></th>
<td>
<input class="regular-text" id="product_updates_feed_url" name="kb_markdown_importer_settings[product_updates_feed_url]" type="url" value="<?php echo esc_attr($settings['product_updates_feed_url']); ?>" placeholder="https://example.com/updates.xml">
<p class="description"><?php esc_html_e('RSS- oder XML-Feed mit den neuesten Produktupdates. Wird nur genutzt, wenn RSS/XML als Quelle ausgewählt ist.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="product_updates_rest_url"><?php esc_html_e('REST/JSON URL', 'kb-markdown-importer'); ?></label></th>
<td>
<input class="regular-text" id="product_updates_rest_url" name="kb_markdown_importer_settings[product_updates_rest_url]" type="url" value="<?php echo esc_attr($settings['product_updates_rest_url']); ?>" placeholder="https://example.com/api/product-versions">
<p class="description"><?php esc_html_e('REST-Endpunkt mit JSON-Antwort. Wird nur genutzt, wenn REST/JSON als Quelle ausgewählt ist.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="product_updates_feed_limit"><?php esc_html_e('Anzahl Updates', 'kb-markdown-importer'); ?></label></th>
<td><input id="product_updates_feed_limit" name="kb_markdown_importer_settings[product_updates_feed_limit]" type="number" min="1" max="20" value="<?php echo esc_attr($settings['product_updates_feed_limit']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="product_updates_feed_item_path"><?php esc_html_e('Eintrag-Pfad', 'kb-markdown-importer'); ?></label></th>
<td>
<input class="regular-text" id="product_updates_feed_item_path" name="kb_markdown_importer_settings[product_updates_feed_item_path]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_item_path']); ?>" placeholder="channel/item">
<p class="description"><?php esc_html_e('Pfad zum wiederholten Feed-Eintrag, zum Beispiel channel/item.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="product_updates_rest_list_path"><?php esc_html_e('REST Listenpfad', 'kb-markdown-importer'); ?></label></th>
<td>
<input class="regular-text" id="product_updates_rest_list_path" name="kb_markdown_importer_settings[product_updates_rest_list_path]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_list_path']); ?>" placeholder="content,data,items">
<p class="description"><?php esc_html_e('Pfad zur Liste in der JSON-Antwort. Mehrere Alternativen mit Komma trennen. Leer lassen, wenn die Antwort direkt ein Array ist.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('XML-Felder', 'kb-markdown-importer'); ?></th>
<td>
<fieldset class="kb-feed-fields">
<p>
<label for="product_updates_feed_product_field"><?php esc_html_e('Produktname', 'kb-markdown-importer'); ?></label><br>
<input class="regular-text" id="product_updates_feed_product_field" name="kb_markdown_importer_settings[product_updates_feed_product_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_product_field']); ?>" placeholder="title">
</p>
<p>
<label for="product_updates_feed_version_field"><?php esc_html_e('Version', 'kb-markdown-importer'); ?></label><br>
<input class="regular-text" id="product_updates_feed_version_field" name="kb_markdown_importer_settings[product_updates_feed_version_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_version_field']); ?>" placeholder="category">
</p>
<p>
<label for="product_updates_feed_date_field"><?php esc_html_e('Datum', 'kb-markdown-importer'); ?></label><br>
<input class="regular-text" id="product_updates_feed_date_field" name="kb_markdown_importer_settings[product_updates_feed_date_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_date_field']); ?>" placeholder="pubDate">
</p>
<p>
<label for="product_updates_feed_changelog_field"><?php esc_html_e('Changelog', 'kb-markdown-importer'); ?></label><br>
<input class="regular-text" id="product_updates_feed_changelog_field" name="kb_markdown_importer_settings[product_updates_feed_changelog_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_changelog_field']); ?>" placeholder="description">
</p>
</fieldset>
<p class="description"><?php esc_html_e('Feldpfade relativ zum Eintrag, zum Beispiel product/name, version oder changelog. Namespaces wie dc:date werden unterstützt.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('REST-Felder', 'kb-markdown-importer'); ?></th>
<td>
<fieldset class="kb-rest-fields">
<p>
<label for="product_updates_rest_product_field"><?php esc_html_e('Produktname', 'kb-markdown-importer'); ?></label><br>
<input class="regular-text" id="product_updates_rest_product_field" name="kb_markdown_importer_settings[product_updates_rest_product_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_product_field']); ?>" placeholder="product.name,productName,name">
</p>
<p>
<label for="product_updates_rest_version_field"><?php esc_html_e('Version', 'kb-markdown-importer'); ?></label><br>
<input class="regular-text" id="product_updates_rest_version_field" name="kb_markdown_importer_settings[product_updates_rest_version_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_version_field']); ?>" placeholder="version,versionName,name">
</p>
<p>
<label for="product_updates_rest_date_field"><?php esc_html_e('Datum', 'kb-markdown-importer'); ?></label><br>
<input class="regular-text" id="product_updates_rest_date_field" name="kb_markdown_importer_settings[product_updates_rest_date_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_date_field']); ?>" placeholder="releaseDate,date,updatedAt,createdAt">
</p>
<p>
<label for="product_updates_rest_changelog_field"><?php esc_html_e('Changelog', 'kb-markdown-importer'); ?></label><br>
<input class="regular-text" id="product_updates_rest_changelog_field" name="kb_markdown_importer_settings[product_updates_rest_changelog_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_changelog_field']); ?>" placeholder="changelog,changeLog,description,changes">
</p>
</fieldset>
<p class="description"><?php esc_html_e('JSON-Feldpfade relativ zu einem Eintrag. Verschachtelte Felder mit Punkt oder Slash angeben, Alternativen mit Komma trennen.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
</table>
<h2><?php esc_html_e('Frontend Design', 'kb-markdown-importer'); ?></h2> <h2><?php esc_html_e('Frontend Design', 'kb-markdown-importer'); ?></h2>
<table class="form-table" role="presentation"> <table class="form-table" role="presentation">
<tr> <tr>
@@ -154,6 +285,21 @@ final class SettingsPage
<?php wp_nonce_field('kb_markdown_test_connection'); ?> <?php wp_nonce_field('kb_markdown_test_connection'); ?>
<?php submit_button(__('Test GitLab Connection', 'kb-markdown-importer'), 'secondary', 'kb_markdown_test_connection'); ?> <?php submit_button(__('Test GitLab Connection', 'kb-markdown-importer'), 'secondary', 'kb_markdown_test_connection'); ?>
</form> </form>
<form method="post">
<?php wp_nonce_field('kb_markdown_test_product_updates'); ?>
<?php submit_button(__('Produktupdate-Quelle testen', 'kb-markdown-importer'), 'secondary', 'kb_markdown_test_product_updates'); ?>
<p class="description"><?php esc_html_e('Der Test nutzt die gespeicherten Einstellungen der ausgewählten Update-Quelle. Bitte Änderungen vorher speichern.', 'kb-markdown-importer'); ?></p>
</form>
<?php if (is_array($updatesTest)) : ?>
<div class="notice notice-<?php echo $updatesTest['ok'] ? 'success' : 'error'; ?>">
<p><strong><?php echo esc_html($updatesTest['title']); ?></strong></p>
<p><?php echo esc_html($updatesTest['message']); ?></p>
</div>
<?php if ('' !== $updatesTest['body']) : ?>
<h2><?php esc_html_e('Antwort der Produktupdate-Quelle', 'kb-markdown-importer'); ?></h2>
<textarea class="large-text code" rows="16" readonly><?php echo esc_textarea($updatesTest['body']); ?></textarea>
<?php endif; ?>
<?php endif; ?>
</div> </div>
<?php <?php
} }
@@ -176,6 +322,78 @@ final class SettingsPage
settings_errors('kb_markdown_importer'); settings_errors('kb_markdown_importer');
} }
private static function handleProductUpdatesTest(): array
{
$settings = Plugin::settings();
$source = (string) ($settings['product_updates_source'] ?? 'rss');
$url = esc_url_raw((string) ('rest' === $source ? ($settings['product_updates_rest_url'] ?? '') : ($settings['product_updates_feed_url'] ?? '')));
if ('' === $url) {
return [
'ok' => 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 private static function formatConnectionError(\WP_Error $error): string
{ {
$message = $error->get_error_message(); $message = $error->get_error_message();
@@ -206,4 +424,20 @@ final class SettingsPage
return preg_match('/^#[0-9a-fA-F]{6}$/', $value) ? strtoupper($value) : $fallback; 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;
}
} }

View File

@@ -11,7 +11,7 @@ final class BreadcrumbBuilder
{ {
$base = trim((string) Plugin::settings()['docs_base_slug'], '/'); $base = trim((string) Plugin::settings()['docs_base_slug'], '/');
$items = [ $items = [
sprintf('<a href="%s">%s</a>', esc_url(home_url('/' . $base . '/')), esc_html__('Docs', 'kb-markdown-importer')), sprintf('<a href="%s">%s</a>', esc_url(home_url('/' . $base . '/')), esc_html__('Dokumentation', 'kb-markdown-importer')),
]; ];
$path = $base; $path = $base;

View File

@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace KbMarkdownImporter\Frontend;
use KbMarkdownImporter\Plugin;
final class ProductUpdatesFeed
{
public static function items(array $settings = []): array
{
$settings = $settings ?: Plugin::settings();
$source = (string) ($settings['product_updates_source'] ?? 'rss');
$url = esc_url_raw((string) ('rest' === $source ? ($settings['product_updates_rest_url'] ?? '') : ($settings['product_updates_feed_url'] ?? '')));
if ('' === $url) {
return [];
}
$cacheKey = 'kb_product_updates_' . md5($source . $url . wp_json_encode([
$settings['product_updates_feed_item_path'] ?? '',
$settings['product_updates_feed_product_field'] ?? '',
$settings['product_updates_feed_version_field'] ?? '',
$settings['product_updates_feed_date_field'] ?? '',
$settings['product_updates_feed_changelog_field'] ?? '',
$settings['product_updates_rest_list_path'] ?? '',
$settings['product_updates_rest_product_field'] ?? '',
$settings['product_updates_rest_version_field'] ?? '',
$settings['product_updates_rest_date_field'] ?? '',
$settings['product_updates_rest_changelog_field'] ?? '',
]));
$cached = get_transient($cacheKey);
if (is_array($cached)) {
return $cached;
}
$response = wp_remote_get($url, [
'timeout' => 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);
}
}

View File

@@ -194,9 +194,13 @@ final class Router
private function captureIndex(): string private function captureIndex(): string
{ {
$settings = Plugin::settings();
return (new TemplateLoader())->capture('documentation-index', [ return (new TemplateLoader())->capture('documentation-index', [
'products' => self::productsWithVersions(), '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, 'url_builder' => UrlBuilder::class,
]); ]);
} }
@@ -245,6 +249,7 @@ final class Router
return (new TemplateLoader())->capture('version', [ return (new TemplateLoader())->capture('version', [
'product' => $product, 'product' => $product,
'version' => $version, 'version' => $version,
'versions' => $this->versionsForProduct($productSlug),
'pages' => $this->pagesForVersion($productSlug, $versionSlug), 'pages' => $this->pagesForVersion($productSlug, $versionSlug),
'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'), 'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'),
'url_builder' => UrlBuilder::class, 'url_builder' => UrlBuilder::class,
@@ -332,7 +337,7 @@ final class Router
{ {
status_header(404); status_header(404);
(new TemplateLoader())->render('search', [ (new TemplateLoader())->render('search', [
'title' => __('Documentation page not found.', 'kb-markdown-importer'), 'title' => __('Dokumentationsseite nicht gefunden.', 'kb-markdown-importer'),
'results' => [], 'results' => [],
'query' => '', 'query' => '',
]); ]);
@@ -342,7 +347,7 @@ final class Router
{ {
status_header(404); status_header(404);
return (new TemplateLoader())->capture('search', [ return (new TemplateLoader())->capture('search', [
'title' => __('Documentation page not found.', 'kb-markdown-importer'), 'title' => __('Dokumentationsseite nicht gefunden.', 'kb-markdown-importer'),
'results' => [], 'results' => [],
'query' => '', 'query' => '',
]); ]);

View File

@@ -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'] ?? '')))) : []; $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', [ return (new TemplateLoader())->capture('search', [
'title' => __('Search Documentation', 'kb-markdown-importer'), 'title' => __('Dokumentation durchsuchen', 'kb-markdown-importer'),
'query' => $query, 'query' => $query,
'results' => $results, 'results' => $results,
]); ]);

View File

@@ -11,7 +11,7 @@ final class TemplateLoader
if (! is_readable($path)) { if (! is_readable($path)) {
status_header(500); status_header(500);
echo esc_html__('Knowledgebase template missing.', 'kb-markdown-importer'); echo esc_html__('Dokumentationsvorlage fehlt.', 'kb-markdown-importer');
return; return;
} }

View File

@@ -22,6 +22,22 @@ final class Settings
'design_accent_color' => '#F59C00', 'design_accent_color' => '#F59C00',
'design_radius' => '14', 'design_radius' => '14',
'custom_theme_css_url' => '', 'custom_theme_css_url' => '',
'docs_home_intro_title' => 'So nutzt du die Dokumentation',
'docs_home_intro_content' => '<p>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.</p>',
'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',
]; ];
} }
} }

View File

@@ -6,9 +6,9 @@ $active_version_slug = (string) ($active_version_slug ?? '');
$active_page_slug = (string) ($active_page_slug ?? ''); $active_page_slug = (string) ($active_page_slug ?? '');
?> ?>
<div class="kb-docs-wrap kb-docs-app"> <div class="kb-docs-wrap kb-docs-app">
<aside class="kb-app-sidebar" aria-label="<?php esc_attr_e('Documentation navigation', 'kb-markdown-importer'); ?>"> <aside class="kb-app-sidebar" aria-label="<?php esc_attr_e('Dokumentationsnavigation', 'kb-markdown-importer'); ?>">
<div class="kb-app-sidebar__brand"> <div class="kb-app-sidebar__brand">
<a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Knowledgebase', 'kb-markdown-importer'); ?></a> <a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Dokumentation', 'kb-markdown-importer'); ?></a>
</div> </div>
<div class="kb-app-sidebar__search"> <div class="kb-app-sidebar__search">
<?php echo do_shortcode('[kb_search]'); ?> <?php echo do_shortcode('[kb_search]'); ?>
@@ -25,25 +25,15 @@ $active_page_slug = (string) ($active_page_slug ?? '');
<a class="kb-app-product__link" href="<?php echo esc_url($latest ? $url_builder::version($term->slug, $latest->slug) : $url_builder::product($term->slug)); ?>"> <a class="kb-app-product__link" href="<?php echo esc_url($latest ? $url_builder::version($term->slug, $latest->slug) : $url_builder::product($term->slug)); ?>">
<?php echo esc_html($term->name); ?> <?php echo esc_html($term->name); ?>
</a> </a>
<?php if ($versions) : ?> <?php if ($isActiveProduct && $active_version_slug && ! empty($active_pages)) : ?>
<ul class="kb-app-version-list"> <ul class="kb-app-page-list">
<?php foreach ($versions as $version) : ?> <?php foreach ((array) $active_pages as $page) : ?>
<?php $isActiveVersion = $isActiveProduct && $version->slug === $active_version_slug; ?> <?php
<li class="<?php echo $isActiveVersion ? 'is-active' : ''; ?>"> $pageSlug = (string) get_post_meta($page->ID, '_kb_page_slug', true);
<a href="<?php echo esc_url($url_builder::version($term->slug, $version->slug)); ?>"><?php echo esc_html($version->name); ?></a> $isActivePage = $pageSlug === $active_page_slug || ('' === $active_page_slug && in_array($pageSlug, ['', 'index'], true));
<?php if ($isActiveVersion && ! empty($active_pages)) : ?> ?>
<ul class="kb-app-page-list"> <li class="<?php echo $isActivePage ? 'is-active' : ''; ?>">
<?php foreach ((array) $active_pages as $page) : ?> <a href="<?php echo esc_url($url_builder::page($term->slug, $active_version_slug, $pageSlug)); ?>"><?php echo esc_html(get_the_title($page)); ?></a>
<?php
$pageSlug = (string) get_post_meta($page->ID, '_kb_page_slug', true);
$isActivePage = $pageSlug === $active_page_slug || ('' === $active_page_slug && in_array($pageSlug, ['', 'index'], true));
?>
<li class="<?php echo $isActivePage ? 'is-active' : ''; ?>">
<a href="<?php echo esc_url($url_builder::page($term->slug, $version->slug, $pageSlug)); ?>"><?php echo esc_html(get_the_title($page)); ?></a>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>

View File

@@ -1,8 +1,40 @@
<?php <?php
defined('ABSPATH') || exit; defined('ABSPATH') || exit;
$settings = (array) ($settings ?? []);
$updates = (array) ($updates ?? []);
?> ?>
<section class="kb-docs-home"> <section class="kb-docs-home">
<h1><?php esc_html_e('Knowledgebase', 'kb-markdown-importer'); ?></h1> <h1><?php esc_html_e('Dokumentation', 'kb-markdown-importer'); ?></h1>
<div class="kb-docs-home-grid">
<section class="kb-home-card kb-home-intro">
<h2><?php echo esc_html((string) ($settings['docs_home_intro_title'] ?? __('So nutzt du die Dokumentation', 'kb-markdown-importer'))); ?></h2>
<div class="kb-home-intro__content">
<?php echo wp_kses_post(wpautop((string) ($settings['docs_home_intro_content'] ?? ''))); ?>
</div>
</section>
<section class="kb-home-card kb-product-updates">
<h2><?php esc_html_e('Neueste Produktupdates', 'kb-markdown-importer'); ?></h2>
<?php if ($updates) : ?>
<ul class="kb-product-updates__list">
<?php foreach ($updates as $update) : ?>
<li>
<div class="kb-product-updates__meta">
<strong><?php echo esc_html((string) ($update['product'] ?: __('Produkt', 'kb-markdown-importer'))); ?></strong>
<?php if (! empty($update['version'])) : ?><span><?php echo esc_html((string) $update['version']); ?></span><?php endif; ?>
<?php if (! empty($update['date'])) : ?><time><?php echo esc_html((string) $update['date']); ?></time><?php endif; ?>
</div>
<?php if (! empty($update['changelog'])) : ?>
<p><?php echo esc_html((string) $update['changelog']); ?></p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php else : ?>
<p class="kb-empty-state"><?php esc_html_e('Es wurden noch keine Produktupdates gefunden.', 'kb-markdown-importer'); ?></p>
<?php endif; ?>
</section>
</div>
<div class="kb-product-list"> <div class="kb-product-list">
<?php foreach ((array) $products as $item) : ?> <?php foreach ((array) $products as $item) : ?>
<?php $term = $item['term']; ?> <?php $term = $item['term']; ?>

View File

@@ -2,12 +2,28 @@
defined('ABSPATH') || exit; defined('ABSPATH') || exit;
?> ?>
<article class="kb-doc-content"> <article class="kb-doc-content">
<nav class="kb-breadcrumbs"> <header class="kb-doc-header">
<a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Docs', 'kb-markdown-importer'); ?></a><span>/</span> <div class="kb-doc-header__main">
<a href="<?php echo esc_url($url_builder::product($product_slug)); ?>"><?php echo esc_html($product ? $product->name : $product_slug); ?></a><span>/</span> <nav class="kb-breadcrumbs">
<a href="<?php echo esc_url($url_builder::version($product_slug, $version_slug)); ?>"><?php echo esc_html($version ? $version->name : $version_slug); ?></a> <a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Dokumentation', 'kb-markdown-importer'); ?></a><span>/</span>
</nav> <a href="<?php echo esc_url($url_builder::product($product_slug)); ?>"><?php echo esc_html($product ? $product->name : $product_slug); ?></a><span>/</span>
<h1><?php echo esc_html(get_the_title($post)); ?></h1> <span><?php echo esc_html($version ? $version->name : $version_slug); ?></span>
</nav>
<h1><?php echo esc_html(get_the_title($post)); ?></h1>
</div>
<?php if (! empty($versions)) : ?>
<div class="kb-version-switcher">
<label for="kb-version-switcher"><?php esc_html_e('Version', 'kb-markdown-importer'); ?></label>
<select id="kb-version-switcher">
<?php foreach ((array) $versions as $available_version) : ?>
<option value="<?php echo esc_attr($available_version->slug); ?>" data-url="<?php echo esc_url($url_builder::version($product_slug, $available_version->slug)); ?>" <?php selected($available_version->slug, $version_slug); ?>>
<?php echo esc_html($available_version->name); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
</header>
<div class="kb-rendered-content"> <div class="kb-rendered-content">
<?php <?php
$rendered_content = $url_builder::rewriteHtml(apply_filters('the_content', $post->post_content)); $rendered_content = $url_builder::rewriteHtml(apply_filters('the_content', $post->post_content));

View File

@@ -2,14 +2,14 @@
defined('ABSPATH') || exit; defined('ABSPATH') || exit;
?> ?>
<section class="kb-docs-product"> <section class="kb-docs-product">
<nav class="kb-breadcrumbs"><a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Docs', 'kb-markdown-importer'); ?></a><span>/</span><?php echo esc_html($product->name); ?></nav> <nav class="kb-breadcrumbs"><a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Dokumentation', 'kb-markdown-importer'); ?></a><span>/</span><?php echo esc_html($product->name); ?></nav>
<h1><?php echo esc_html($product->name); ?></h1> <h1><?php echo esc_html($product->name); ?></h1>
<h2><?php esc_html_e('Available Versions', 'kb-markdown-importer'); ?></h2> <h2><?php esc_html_e('Verfügbare Versionen', 'kb-markdown-importer'); ?></h2>
<ul class="kb-version-list"> <ul class="kb-version-list">
<?php foreach ((array) $versions as $index => $version) : ?> <?php foreach ((array) $versions as $index => $version) : ?>
<li> <li>
<a href="<?php echo esc_url($url_builder::version($product->slug, $version->slug)); ?>"><?php echo esc_html($version->name); ?></a> <a href="<?php echo esc_url($url_builder::version($product->slug, $version->slug)); ?>"><?php echo esc_html($version->name); ?></a>
<?php if (0 === $index) : ?><span class="kb-current-version"><?php esc_html_e('current', 'kb-markdown-importer'); ?></span><?php endif; ?> <?php if (0 === $index) : ?><span class="kb-current-version"><?php esc_html_e('aktuell', 'kb-markdown-importer'); ?></span><?php endif; ?>
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>

View File

@@ -2,10 +2,10 @@
defined('ABSPATH') || exit; defined('ABSPATH') || exit;
?> ?>
<section class="kb-search"> <section class="kb-search">
<h2><?php echo esc_html($title ?? __('Search Documentation', 'kb-markdown-importer')); ?></h2> <h2><?php echo esc_html($title ?? __('Dokumentation durchsuchen', 'kb-markdown-importer')); ?></h2>
<form method="get" class="kb-search-form"> <form method="get" class="kb-search-form">
<input type="search" name="kbq" value="<?php echo esc_attr($query ?? ''); ?>" placeholder="<?php esc_attr_e('Search documentation', 'kb-markdown-importer'); ?>"> <input type="search" name="kbq" value="<?php echo esc_attr($query ?? ''); ?>" placeholder="<?php esc_attr_e('Dokumentation durchsuchen', 'kb-markdown-importer'); ?>">
<button type="submit"><?php esc_html_e('Search', 'kb-markdown-importer'); ?></button> <button type="submit"><?php esc_html_e('Suchen', 'kb-markdown-importer'); ?></button>
</form> </form>
<?php if (! empty($results)) : ?> <?php if (! empty($results)) : ?>
<ul class="kb-search-results"> <ul class="kb-search-results">

View File

@@ -2,12 +2,28 @@
defined('ABSPATH') || exit; defined('ABSPATH') || exit;
?> ?>
<section class="kb-docs-version"> <section class="kb-docs-version">
<nav class="kb-breadcrumbs"> <header class="kb-doc-header">
<a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Docs', 'kb-markdown-importer'); ?></a><span>/</span> <div class="kb-doc-header__main">
<a href="<?php echo esc_url($url_builder::product($product->slug)); ?>"><?php echo esc_html($product->name); ?></a><span>/</span> <nav class="kb-breadcrumbs">
<?php echo esc_html($version->name); ?> <a href="<?php echo esc_url($url_builder::docsIndex()); ?>"><?php esc_html_e('Dokumentation', 'kb-markdown-importer'); ?></a><span>/</span>
</nav> <a href="<?php echo esc_url($url_builder::product($product->slug)); ?>"><?php echo esc_html($product->name); ?></a><span>/</span>
<h1><?php echo esc_html($product->name . ' ' . $version->name); ?></h1> <span><?php echo esc_html($version->name); ?></span>
</nav>
<h1><?php echo esc_html($product->name . ' ' . $version->name); ?></h1>
</div>
<?php if (! empty($versions)) : ?>
<div class="kb-version-switcher">
<label for="kb-version-switcher"><?php esc_html_e('Version', 'kb-markdown-importer'); ?></label>
<select id="kb-version-switcher">
<?php foreach ((array) $versions as $available_version) : ?>
<option value="<?php echo esc_attr($available_version->slug); ?>" data-url="<?php echo esc_url($url_builder::version($product->slug, $available_version->slug)); ?>" <?php selected($available_version->slug, $version->slug); ?>>
<?php echo esc_html($available_version->name); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
</header>
<ul class="kb-page-list"> <ul class="kb-page-list">
<?php foreach ((array) $pages as $page) : ?> <?php foreach ((array) $pages as $page) : ?>
<?php $slug = (string) get_post_meta($page->ID, '_kb_page_slug', true); ?> <?php $slug = (string) get_post_meta($page->ID, '_kb_page_slug', true); ?>