diff --git a/includes/class-wmw-activator.php b/includes/class-wmw-activator.php
new file mode 100644
index 0000000..bc45328
--- /dev/null
+++ b/includes/class-wmw-activator.php
@@ -0,0 +1,88 @@
+get_charset_collate();
+
+ // Views table (track article views)
+ $views_table = $wpdb->prefix . 'wmw_views';
+ $sql_views = "CREATE TABLE IF NOT EXISTS $views_table (
+ id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+ article_id BIGINT(20) UNSIGNED NOT NULL,
+ view_count BIGINT(20) UNSIGNED NOT NULL DEFAULT 1,
+ PRIMARY KEY (id),
+ UNIQUE KEY article_id (article_id)
+ ) $charset;";
+
+ // Search index table
+ $search_table = $wpdb->prefix . 'wmw_search_index';
+ $sql_search = "CREATE TABLE IF NOT EXISTS $search_table (
+ id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+ article_id BIGINT(20) UNSIGNED NOT NULL,
+ wiki_id BIGINT(20) UNSIGNED NOT NULL,
+ keywords LONGTEXT NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY article_id (article_id),
+ KEY wiki_id (wiki_id)
+ ) $charset;";
+
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+ dbDelta( $sql_views );
+ dbDelta( $sql_search );
+
+ // Store DB version
+ update_option( 'wmw_db_version', '1.0.0' );
+
+ // Default settings
+ if ( ! get_option( 'wmw_settings' ) ) {
+ update_option( 'wmw_settings', array(
+ 'show_toc' => 1,
+ 'show_breadcrumbs' => 1,
+ 'show_related' => 1,
+ 'show_search' => 1,
+ 'articles_per_page' => 20,
+ 'sidebar_position' => 'left',
+ 'color_scheme' => 'auto',
+ ) );
+ }
+
+ // Flag to flush rewrite rules
+ update_option( 'wmw_flush_rewrite', 1 );
+
+ // Register post types early so permalinks work
+ $pt = new WMW_Post_Types();
+ $pt->register();
+ flush_rewrite_rules();
+ }
+
+ public static function deactivate() {
+ flush_rewrite_rules();
+ }
+
+ public static function uninstall() {
+ global $wpdb;
+
+ // Remove custom tables
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wmw_views" );
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wmw_search_index" );
+
+ // Remove options
+ delete_option( 'wmw_settings' );
+ delete_option( 'wmw_db_version' );
+
+ // Remove all wiki and article posts
+ $posts = get_posts( array(
+ 'post_type' => array( 'wmw_wiki', 'wmw_article' ),
+ 'post_status' => 'any',
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ ) );
+ foreach ( $posts as $post_id ) {
+ wp_delete_post( $post_id, true );
+ }
+ }
+}
diff --git a/includes/class-wmw-admin.php b/includes/class-wmw-admin.php
new file mode 100644
index 0000000..40504bb
--- /dev/null
+++ b/includes/class-wmw-admin.php
@@ -0,0 +1,303 @@
+ admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'wmw_admin_nonce' ),
+ 'confirm_delete' => __( 'Wirklich löschen?', 'wp-multi-wiki' ),
+ ) );
+
+ wp_enqueue_editor();
+ }
+
+ // ── Meta Boxes ────────────────────────────────────────────────────────────
+
+ public function add_meta_boxes() {
+ add_meta_box( 'wmw_wiki_options', 'Wiki-Optionen', array( $this, 'mb_wiki_options' ), 'wmw_wiki', 'side' );
+ add_meta_box( 'wmw_article_meta', 'Artikel-Einstellungen', array( $this, 'mb_article_meta' ), 'wmw_article', 'side' );
+ }
+
+ public function mb_wiki_options( $post ) {
+ wp_nonce_field( 'wmw_save_wiki', 'wmw_wiki_nonce' );
+ $icon = get_post_meta( $post->ID, '_wmw_icon', true ) ?: '📖';
+ $color = get_post_meta( $post->ID, '_wmw_color', true ) ?: '#2271b1';
+ $ver = get_post_meta( $post->ID, '_wmw_version', true ) ?: '1.0.0';
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ ID, '_wmw_wiki_id', true );
+ $order = (int) get_post_meta( $post->ID, '_wmw_order', true );
+ $wikis = wmw_get_wikis();
+ ?>
+
+
+
+
+
+
+
+
+ update_search_index( $post_id, $wiki_id );
+ }
+ }
+
+ private function update_search_index( $article_id, $wiki_id ) {
+ global $wpdb;
+ $post = get_post( $article_id );
+ $keywords = wp_strip_all_tags( $post->post_title . ' ' . $post->post_content . ' ' . $post->post_excerpt );
+ $table = $wpdb->prefix . 'wmw_search_index';
+ $wpdb->replace( $table, array(
+ 'article_id' => $article_id,
+ 'wiki_id' => $wiki_id,
+ 'keywords' => $keywords,
+ ) );
+ }
+
+ // ── Column Overrides ─────────────────────────────────────────────────────
+
+ public function article_columns( $columns ) {
+ unset( $columns['date'] );
+ $columns['wmw_wiki'] = 'Wiki';
+ $columns['wmw_category'] = 'Kategorie';
+ $columns['date'] = 'Datum';
+ return $columns;
+ }
+
+ public function article_column_data( $column, $post_id ) {
+ if ( 'wmw_wiki' === $column ) {
+ $wiki = wmw_get_article_wiki( $post_id );
+ echo $wiki ? '' . esc_html( $wiki->post_title ) . '' : '—';
+ }
+ if ( 'wmw_category' === $column ) {
+ $terms = get_the_terms( $post_id, 'wmw_category' );
+ echo $terms ? implode( ', ', wp_list_pluck( $terms, 'name' ) ) : '—';
+ }
+ }
+
+ public function wiki_columns( $columns ) {
+ unset( $columns['date'] );
+ $columns['wmw_articles'] = 'Artikel';
+ $columns['wmw_icon'] = 'Icon';
+ $columns['wmw_version'] = 'Version';
+ $columns['date'] = 'Datum';
+ return $columns;
+ }
+
+ public function wiki_column_data( $column, $post_id ) {
+ if ( 'wmw_articles' === $column ) {
+ $count = count( wmw_get_articles( $post_id ) );
+ echo '' . $count . '';
+ }
+ if ( 'wmw_icon' === $column ) {
+ echo '' . wmw_get_wiki_icon( $post_id ) . '';
+ }
+ if ( 'wmw_version' === $column ) {
+ echo esc_html( get_post_meta( $post_id, '_wmw_version', true ) ?: '1.0.0' );
+ }
+ }
+
+ // ── Admin Notices ─────────────────────────────────────────────────────────
+
+ public function admin_notices() {
+ if ( isset( $_GET['wmw_saved'] ) ) {
+ echo '';
+ }
+ if ( isset( $_GET['wmw_deleted'] ) ) {
+ echo '';
+ }
+ if ( isset( $_GET['wmw_error'] ) ) {
+ echo '❌ Fehler: ' . esc_html( $_GET['wmw_error'] ) . '
';
+ }
+ }
+
+ // ── Pages ─────────────────────────────────────────────────────────────────
+
+ public function page_dashboard() {
+ $wikis = get_posts( array( 'post_type' => 'wmw_wiki', 'post_status' => 'any', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC' ) );
+ $total_articles = count( get_posts( array( 'post_type' => 'wmw_article', 'post_status' => 'any', 'posts_per_page' => -1 ) ) );
+ include WMW_PLUGIN_DIR . 'templates/admin/dashboard.php';
+ }
+
+ public function page_new_wiki() {
+ include WMW_PLUGIN_DIR . 'templates/admin/wiki-form.php';
+ }
+
+ public function page_edit_wiki() {
+ $wiki_id = absint( $_GET['id'] ?? 0 );
+ $wiki = $wiki_id ? get_post( $wiki_id ) : null;
+ if ( ! $wiki || $wiki->post_type !== 'wmw_wiki' ) {
+ wp_die( 'Wiki nicht gefunden.' );
+ }
+ include WMW_PLUGIN_DIR . 'templates/admin/wiki-form.php';
+ }
+
+ public function page_articles() {
+ $wiki_filter = absint( $_GET['wiki_id'] ?? 0 );
+ $args = array( 'post_type' => 'wmw_article', 'post_status' => 'any', 'posts_per_page' => 50, 'orderby' => 'title', 'order' => 'ASC' );
+ if ( $wiki_filter ) {
+ $args['meta_query'] = array( array( 'key' => '_wmw_wiki_id', 'value' => $wiki_filter ) );
+ }
+ $articles = get_posts( $args );
+ $wikis = wmw_get_wikis();
+ include WMW_PLUGIN_DIR . 'templates/admin/article-list.php';
+ }
+
+ public function page_new_article() {
+ $wiki_id = absint( $_GET['wiki_id'] ?? 0 );
+ $wikis = wmw_get_wikis();
+ include WMW_PLUGIN_DIR . 'templates/admin/article-form.php';
+ }
+
+ public function page_edit_article() {
+ $article_id = absint( $_GET['id'] ?? 0 );
+ $article = $article_id ? get_post( $article_id ) : null;
+ if ( ! $article || $article->post_type !== 'wmw_article' ) wp_die( 'Artikel nicht gefunden.' );
+ $wikis = wmw_get_wikis();
+ include WMW_PLUGIN_DIR . 'templates/admin/article-form.php';
+ }
+
+ public function page_categories() {
+ include WMW_PLUGIN_DIR . 'templates/admin/categories.php';
+ }
+
+ public function page_gitea_import() {
+ include WMW_PLUGIN_DIR . 'templates/admin/gitea-importer.php';
+ }
+
+ public function page_settings() {
+
+ $saved = false;
+
+ // ── Speichern ────────────────────────────────────────────────────
+ if ( isset( $_POST['wmw_settings_nonce'] )
+ && wp_verify_nonce( $_POST['wmw_settings_nonce'], 'wmw_save_settings' )
+ && current_user_can( 'manage_options' ) ) {
+
+ $settings = array(
+ 'show_toc' => isset( $_POST['show_toc'] ) ? 1 : 0,
+ 'show_breadcrumbs' => isset( $_POST['show_breadcrumbs'] ) ? 1 : 0,
+ 'show_related' => isset( $_POST['show_related'] ) ? 1 : 0,
+ 'show_search' => isset( $_POST['show_search'] ) ? 1 : 0,
+ 'articles_per_page' => absint( $_POST['articles_per_page'] ?? 20 ),
+ 'sidebar_position' => sanitize_key( $_POST['sidebar_position'] ?? 'left' ),
+ 'color_scheme' => sanitize_key( $_POST['color_scheme'] ?? 'auto' ),
+ 'custom_css' => wp_strip_all_tags( $_POST['custom_css'] ?? '' ),
+ );
+
+ update_option( 'wmw_settings', $settings );
+ $saved = true;
+ }
+
+ // ── Seite anzeigen (nach Speichern ODER normalem Aufruf) ─────────
+ $settings = get_option( 'wmw_settings', array() );
+
+ // Erfolgsmeldung direkt hier ausgeben, kein Redirect nötig
+ if ( $saved ) {
+ echo '✅ Einstellungen gespeichert!
';
+ }
+
+ include WMW_PLUGIN_DIR . 'templates/admin/settings.php';
+ }
+}
\ No newline at end of file
diff --git a/includes/class-wmw-ajax.php b/includes/class-wmw-ajax.php
new file mode 100644
index 0000000..22543af
--- /dev/null
+++ b/includes/class-wmw-ajax.php
@@ -0,0 +1,208 @@
+ 'Ungültige Anfrage.' ) );
+ }
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( array( 'message' => 'Keine Berechtigung.' ) );
+ }
+ }
+
+ public function save_wiki() {
+ $this->verify_nonce();
+
+ $id = absint( $_POST['id'] ?? 0 );
+ $title = sanitize_text_field( $_POST['title'] ?? '' );
+ $desc = wp_kses_post( $_POST['description'] ?? '' );
+ $icon = sanitize_text_field( $_POST['icon'] ?? '📖' );
+ $color = sanitize_hex_color( $_POST['color'] ?? '#2271b1' );
+ $ver = sanitize_text_field( $_POST['version'] ?? '1.0.0' );
+
+ if ( empty( $title ) ) {
+ wp_send_json_error( array( 'message' => 'Titel darf nicht leer sein.' ) );
+ }
+
+ $data = array(
+ 'post_type' => 'wmw_wiki',
+ 'post_title' => $title,
+ 'post_content' => $desc,
+ 'post_status' => 'publish',
+ );
+
+ if ( $id ) {
+ $data['ID'] = $id;
+ $result = wp_update_post( $data, true );
+ } else {
+ $result = wp_insert_post( $data, true );
+ }
+
+ if ( is_wp_error( $result ) ) {
+ wp_send_json_error( array( 'message' => $result->get_error_message() ) );
+ }
+
+ update_post_meta( $result, '_wmw_icon', $icon );
+ update_post_meta( $result, '_wmw_color', $color );
+ update_post_meta( $result, '_wmw_version', $ver );
+
+ wp_send_json_success( array(
+ 'id' => $result,
+ 'title' => $title,
+ 'url' => get_permalink( $result ),
+ ) );
+ }
+
+ public function delete_wiki() {
+ $this->verify_nonce();
+ $id = absint( $_POST['id'] ?? 0 );
+ if ( ! $id ) wp_send_json_error();
+
+ // Delete all articles in this wiki
+ $articles = wmw_get_articles( $id );
+ foreach ( $articles as $article ) {
+ wp_delete_post( $article->ID, true );
+ }
+
+ wp_delete_post( $id, true );
+ wp_send_json_success();
+ }
+
+ public function save_article() {
+ $this->verify_nonce();
+
+ $id = absint( $_POST['id'] ?? 0 );
+ $title = sanitize_text_field( $_POST['title'] ?? '' );
+ $content = wp_kses_post( $_POST['content'] ?? '' );
+ $excerpt = sanitize_textarea_field( $_POST['excerpt'] ?? '' );
+ $wiki_id = absint( $_POST['wiki_id'] ?? 0 );
+ $order = absint( $_POST['order'] ?? 0 );
+ $cats = array_map( 'absint', (array) ( $_POST['categories'] ?? array() ) );
+ $tags = sanitize_text_field( $_POST['tags'] ?? '' );
+ $status = in_array( $_POST['status'] ?? 'publish', array( 'publish', 'draft' ) ) ? $_POST['status'] : 'publish';
+
+ if ( empty( $title ) ) {
+ wp_send_json_error( array( 'message' => 'Titel darf nicht leer sein.' ) );
+ }
+
+ $data = array(
+ 'post_type' => 'wmw_article',
+ 'post_title' => $title,
+ 'post_content' => $content,
+ 'post_excerpt' => $excerpt,
+ 'post_status' => $status,
+ );
+
+ if ( $id ) {
+ $data['ID'] = $id;
+ $result = wp_update_post( $data, true );
+ } else {
+ $result = wp_insert_post( $data, true );
+ }
+
+ if ( is_wp_error( $result ) ) {
+ wp_send_json_error( array( 'message' => $result->get_error_message() ) );
+ }
+
+ update_post_meta( $result, '_wmw_wiki_id', $wiki_id );
+ update_post_meta( $result, '_wmw_order', $order );
+
+ if ( ! empty( $cats ) ) {
+ wp_set_object_terms( $result, $cats, 'wmw_category' );
+ }
+ if ( ! empty( $tags ) ) {
+ $tag_arr = array_map( 'trim', explode( ',', $tags ) );
+ wp_set_object_terms( $result, $tag_arr, 'wmw_tag' );
+ }
+
+ wp_send_json_success( array(
+ 'id' => $result,
+ 'title' => $title,
+ 'url' => get_permalink( $result ),
+ ) );
+ }
+
+ public function delete_article() {
+ $this->verify_nonce();
+ $id = absint( $_POST['id'] ?? 0 );
+ if ( ! $id ) wp_send_json_error();
+ wp_delete_post( $id, true );
+ wp_send_json_success();
+ }
+
+ public function reorder_articles() {
+ $this->verify_nonce();
+ $order = (array) ( $_POST['order'] ?? array() );
+ foreach ( $order as $position => $article_id ) {
+ update_post_meta( absint( $article_id ), '_wmw_order', absint( $position ) );
+ }
+ wp_send_json_success();
+ }
+
+ public function ajax_search() {
+ $query = sanitize_text_field( $_POST['query'] ?? $_GET['query'] ?? '' );
+ $wiki_id = absint( $_POST['wiki_id'] ?? $_GET['wiki_id'] ?? 0 );
+
+ if ( strlen( $query ) < 2 ) {
+ wp_send_json_success( array( 'results' => array(), 'count' => 0 ) );
+ }
+
+ $results = WMW_Search::search( $query, $wiki_id );
+ $output = array();
+
+ foreach ( $results as $post ) {
+ $wiki = wmw_get_article_wiki( $post->ID );
+ $output[] = array(
+ 'id' => $post->ID,
+ 'title' => $post->wmw_title,
+ 'excerpt' => $post->wmw_excerpt,
+ 'url' => get_permalink( $post->ID ),
+ 'wiki' => $wiki ? $wiki->post_title : '',
+ 'icon' => $wiki ? wmw_get_wiki_icon( $wiki->ID ) : '📄',
+ );
+ }
+
+ wp_send_json_success( array( 'results' => $output, 'count' => count( $output ) ) );
+ }
+
+ public function reindex() {
+ $this->verify_nonce();
+ $count = WMW_Search::reindex_all();
+ wp_send_json_success( array( 'count' => $count, 'message' => $count . ' Artikel neu indiziert.' ) );
+ }
+
+ public function track_view() {
+ global $wpdb;
+ $article_id = absint( $_POST['article_id'] ?? 0 );
+ if ( ! $article_id ) wp_send_json_error();
+
+ $table = $wpdb->prefix . 'wmw_views';
+ $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table WHERE article_id = %d", $article_id ) );
+
+ if ( $existing ) {
+ $wpdb->query( $wpdb->prepare( "UPDATE $table SET view_count = view_count + 1 WHERE article_id = %d", $article_id ) );
+ } else {
+ $wpdb->insert( $table, array( 'article_id' => $article_id, 'view_count' => 1 ), array( '%d', '%d' ) );
+ }
+
+ wp_send_json_success();
+ }
+}
\ No newline at end of file
diff --git a/includes/class-wmw-core.php b/includes/class-wmw-core.php
new file mode 100644
index 0000000..32b34a5
--- /dev/null
+++ b/includes/class-wmw-core.php
@@ -0,0 +1,116 @@
+is_wiki_context() ) return;
+
+ // Dashicons on frontend
+ wp_enqueue_style( 'dashicons' );
+
+ wp_enqueue_style(
+ 'wmw-frontend',
+ WMW_URL . 'assets/css/frontend.css',
+ [ 'dashicons' ],
+ WMW_VERSION
+ );
+
+ wp_enqueue_script(
+ 'wmw-frontend',
+ WMW_URL . 'assets/js/frontend.js',
+ [ 'jquery' ],
+ WMW_VERSION,
+ true
+ );
+
+ wp_localize_script( 'wmw-frontend', 'WMW', [
+ 'ajaxurl' => admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'wmw_search' ),
+ 'l10n' => [
+ 'no_results' => __( 'Keine Ergebnisse gefunden.', 'wp-multi-wiki' ),
+ 'searching' => __( 'Suche …', 'wp-multi-wiki' ),
+ 'navigation' => __( 'Navigation', 'wp-multi-wiki' ),
+ ],
+ ] );
+ }
+
+ /* ── Admin Assets ───────────────────────────────────────────── */
+
+ public function admin_assets( string $hook ): void {
+ $screen = get_current_screen();
+ if ( ! $screen ) return;
+
+ $is_wmw = str_contains( $hook, 'wp-multi-wiki' )
+ || str_contains( $hook, 'wmw-settings' )
+ || in_array( $screen->post_type, [ 'wmw_wiki', 'wmw_article' ], true )
+ || in_array( $screen->taxonomy ?? '', [ 'wmw_category', 'wmw_tag' ], true );
+
+ if ( ! $is_wmw ) return;
+
+ wp_enqueue_style(
+ 'wmw-admin',
+ WMW_URL . 'assets/css/admin.css',
+ [],
+ WMW_VERSION
+ );
+
+ wp_enqueue_script(
+ 'wmw-admin',
+ WMW_URL . 'assets/js/admin.js',
+ [ 'jquery' ],
+ WMW_VERSION,
+ true
+ );
+ }
+
+ /* ── Helpers ────────────────────────────────────────────────── */
+
+ private function is_wiki_context(): bool {
+ return is_singular( [ 'wmw_wiki', 'wmw_article' ] )
+ || is_post_type_archive( 'wmw_wiki' )
+ || is_tax( [ 'wmw_category', 'wmw_tag' ] );
+ }
+
+ public function action_links( array $links ): array {
+ array_unshift(
+ $links,
+ '' . __( 'Dashboard', 'wp-multi-wiki' ) . ''
+ );
+ return $links;
+ }
+}
diff --git a/includes/class-wmw-cpt.php b/includes/class-wmw-cpt.php
new file mode 100644
index 0000000..14dde68
--- /dev/null
+++ b/includes/class-wmw-cpt.php
@@ -0,0 +1,114 @@
+ [
+ 'name' => _x( 'Wikis', 'post type general name', 'wp-multi-wiki' ),
+ 'singular_name' => _x( 'Wiki', 'post type singular name', 'wp-multi-wiki' ),
+ 'add_new' => __( 'Neues Wiki', 'wp-multi-wiki' ),
+ 'add_new_item' => __( 'Neues Wiki erstellen', 'wp-multi-wiki' ),
+ 'edit_item' => __( 'Wiki bearbeiten', 'wp-multi-wiki' ),
+ 'view_item' => __( 'Wiki ansehen', 'wp-multi-wiki' ),
+ 'all_items' => __( 'Alle Wikis', 'wp-multi-wiki' ),
+ 'search_items' => __( 'Wikis suchen', 'wp-multi-wiki' ),
+ 'not_found' => __( 'Keine Wikis gefunden.', 'wp-multi-wiki' ),
+ 'not_found_in_trash' => __( 'Keine Wikis im Papierkorb.', 'wp-multi-wiki' ),
+ ],
+ 'public' => true,
+ 'show_ui' => true,
+ 'show_in_menu' => 'wp-multi-wiki',
+ 'show_in_rest' => true,
+ 'has_archive' => 'wikis',
+ 'rewrite' => [ 'slug' => 'wikis', 'with_front' => false ],
+ 'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt' ],
+ 'menu_icon' => 'dashicons-book-alt',
+ 'capability_type' => 'post',
+ ] );
+ }
+
+ /* ── Wiki Article ───────────────────────────────────────────── */
+
+ private static function register_article(): void {
+ register_post_type( 'wmw_article', [
+ 'labels' => [
+ 'name' => _x( 'Wiki Artikel', 'post type general name', 'wp-multi-wiki' ),
+ 'singular_name' => _x( 'Wiki Artikel', 'post type singular name', 'wp-multi-wiki' ),
+ 'add_new' => __( 'Neuer Artikel', 'wp-multi-wiki' ),
+ 'add_new_item' => __( 'Neuen Artikel erstellen', 'wp-multi-wiki' ),
+ 'edit_item' => __( 'Artikel bearbeiten', 'wp-multi-wiki' ),
+ 'view_item' => __( 'Artikel ansehen', 'wp-multi-wiki' ),
+ 'all_items' => __( 'Alle Artikel', 'wp-multi-wiki' ),
+ 'search_items' => __( 'Artikel suchen', 'wp-multi-wiki' ),
+ 'not_found' => __( 'Keine Artikel gefunden.', 'wp-multi-wiki' ),
+ 'parent_item_colon' => __( 'Übergeordneter Artikel:', 'wp-multi-wiki' ),
+ ],
+ 'public' => true,
+ 'show_ui' => true,
+ 'show_in_menu' => 'wp-multi-wiki',
+ 'show_in_rest' => true,
+ 'has_archive' => false,
+ 'hierarchical' => true, // Enables Chapter → Subchapter structure
+ 'rewrite' => [ 'slug' => 'wiki-artikel', 'with_front' => false ],
+ 'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt', 'page-attributes', 'revisions', 'custom-fields' ],
+ 'capability_type' => 'post',
+ ] );
+ }
+
+ /* ── Taxonomies ─────────────────────────────────────────────── */
+
+ private static function register_category(): void {
+ register_taxonomy( 'wmw_category', 'wmw_article', [
+ 'labels' => [
+ 'name' => _x( 'Kategorien', 'taxonomy general name', 'wp-multi-wiki' ),
+ 'singular_name' => _x( 'Kategorie', 'taxonomy singular name', 'wp-multi-wiki' ),
+ 'add_new_item' => __( 'Neue Kategorie erstellen', 'wp-multi-wiki' ),
+ 'edit_item' => __( 'Kategorie bearbeiten', 'wp-multi-wiki' ),
+ 'all_items' => __( 'Alle Kategorien', 'wp-multi-wiki' ),
+ 'parent_item' => __( 'Übergeordnete Kategorie', 'wp-multi-wiki' ),
+ 'parent_item_colon' => __( 'Übergeordnete Kategorie:', 'wp-multi-wiki' ),
+ ],
+ 'hierarchical' => true,
+ 'public' => true,
+ 'show_ui' => true,
+ 'show_admin_column' => true,
+ 'show_in_rest' => true,
+ 'rewrite' => [ 'slug' => 'wiki-kategorie', 'with_front' => false ],
+ ] );
+ }
+
+ private static function register_tag(): void {
+ register_taxonomy( 'wmw_tag', 'wmw_article', [
+ 'labels' => [
+ 'name' => _x( 'Tags', 'taxonomy general name', 'wp-multi-wiki' ),
+ 'singular_name' => _x( 'Tag', 'taxonomy singular name', 'wp-multi-wiki' ),
+ 'add_new_item' => __( 'Neuen Tag erstellen', 'wp-multi-wiki' ),
+ 'all_items' => __( 'Alle Tags', 'wp-multi-wiki' ),
+ ],
+ 'hierarchical' => false,
+ 'public' => true,
+ 'show_ui' => true,
+ 'show_admin_column' => true,
+ 'show_in_rest' => true,
+ 'rewrite' => [ 'slug' => 'wiki-tag', 'with_front' => false ],
+ ] );
+ }
+}
diff --git a/includes/class-wmw-frontend.php b/includes/class-wmw-frontend.php
new file mode 100644
index 0000000..d0035f1
--- /dev/null
+++ b/includes/class-wmw-frontend.php
@@ -0,0 +1,189 @@
+ admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'wmw_public_nonce' ),
+ 'settings' => array(
+ 'show_toc' => $settings['show_toc'] ?? 1,
+ ),
+ ) );
+ }
+
+ public function inject_css_vars() {
+ global $post;
+ if ( ! $post ) return;
+
+ $wiki = null;
+ if ( $post->post_type === 'wmw_wiki' ) {
+ $wiki = $post;
+ } elseif ( $post->post_type === 'wmw_article' ) {
+ $wiki = wmw_get_article_wiki( $post->ID );
+ }
+
+ if ( $wiki ) {
+ $color = wmw_get_wiki_color( $wiki->ID );
+ echo "\n";
+ }
+ }
+
+ public function filter_content( $content ) {
+ global $post;
+ if ( ! $post || ! is_singular() ) return $content;
+
+ $settings = get_option( 'wmw_settings', array() );
+
+ if ( $post->post_type === 'wmw_article' && ( $settings['show_toc'] ?? 1 ) ) {
+ $processed = WMW_TOC::process( $content );
+ if ( ! empty( $processed['toc'] ) ) {
+ $content = $processed['toc'] . $processed['content'];
+ }
+ }
+
+ return $content;
+ }
+
+ public function filter_title( $title, $id = null ) {
+ return $title;
+ }
+
+ public function load_template( $template ) {
+ global $post;
+ if ( ! $post ) return $template;
+
+ if ( $post->post_type === 'wmw_wiki' ) {
+ $custom = WMW_PLUGIN_DIR . 'templates/single-wmw-wiki.php';
+ if ( file_exists( $custom ) ) return $custom;
+ }
+ if ( $post->post_type === 'wmw_article' ) {
+ $custom = WMW_PLUGIN_DIR . 'templates/single-wmw-article.php';
+ if ( file_exists( $custom ) ) return $custom;
+ }
+
+ return $template;
+ }
+
+ // ── Static Render Methods ─────────────────────────────────────────────────
+
+ public static function render_wiki_index( $wiki, $layout = 'grid' ) {
+ $settings = get_option( 'wmw_settings', array() );
+ $articles = wmw_get_articles( $wiki->ID );
+ $categories = get_terms( array( 'taxonomy' => 'wmw_category', 'hide_empty' => true ) );
+ $color = wmw_get_wiki_color( $wiki->ID );
+ $icon = wmw_get_wiki_icon( $wiki->ID );
+
+ // Group by category
+ $grouped = array( 'uncategorized' => array() );
+ $cat_map = array();
+ foreach ( (array) $categories as $cat ) {
+ $grouped[ $cat->term_id ] = array();
+ $cat_map[ $cat->term_id ] = $cat;
+ }
+
+ foreach ( $articles as $article ) {
+ $cats = get_the_terms( $article->ID, 'wmw_category' );
+ if ( $cats && ! is_wp_error( $cats ) ) {
+ foreach ( $cats as $cat ) {
+ $grouped[ $cat->term_id ][] = $article;
+ }
+ } else {
+ $grouped['uncategorized'][] = $article;
+ }
+ }
+
+ include WMW_PLUGIN_DIR . 'templates/wiki-index.php';
+ }
+
+ public static function render_article( $article ) {
+ $wiki = wmw_get_article_wiki( $article->ID );
+ $settings = get_option( 'wmw_settings', array() );
+ $content = apply_filters( 'the_content', $article->post_content );
+
+ // Related articles
+ $related = array();
+ if ( $settings['show_related'] ?? 1 ) {
+ $cats = get_the_terms( $article->ID, 'wmw_category' );
+ if ( $cats && ! is_wp_error( $cats ) ) {
+ $cat_ids = wp_list_pluck( $cats, 'term_id' );
+ $related = get_posts( array(
+ 'post_type' => 'wmw_article',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 5,
+ 'post__not_in' => array( $article->ID ),
+ 'tax_query' => array( array( 'taxonomy' => 'wmw_category', 'field' => 'term_id', 'terms' => $cat_ids ) ),
+ ) );
+ }
+ }
+
+ include WMW_PLUGIN_DIR . 'templates/article-single.php';
+ }
+
+ public static function render_search_box( $wiki_id = 0, $placeholder = '' ) {
+ $placeholder = $placeholder ?: 'Wiki durchsuchen…';
+ include WMW_PLUGIN_DIR . 'templates/search-box.php';
+ }
+
+ public static function render_wiki_list( $wikis, $columns = 3 ) {
+ include WMW_PLUGIN_DIR . 'templates/wiki-list.php';
+ }
+
+ public static function render_breadcrumb( $post ) {
+ $items = array();
+ $items[] = array( 'label' => 'Home', 'url' => home_url() );
+
+ if ( $post->post_type === 'wmw_article' ) {
+ $wiki = wmw_get_article_wiki( $post->ID );
+ if ( $wiki ) {
+ $items[] = array( 'label' => 'Wiki', 'url' => get_post_type_archive_link( 'wmw_wiki' ) );
+ $items[] = array( 'label' => $wiki->post_title, 'url' => get_permalink( $wiki->ID ) );
+ }
+ $items[] = array( 'label' => $post->post_title, 'url' => '' );
+ } elseif ( $post->post_type === 'wmw_wiki' ) {
+ $items[] = array( 'label' => 'Wiki', 'url' => get_post_type_archive_link( 'wmw_wiki' ) );
+ $items[] = array( 'label' => $post->post_title, 'url' => '' );
+ }
+
+ include WMW_PLUGIN_DIR . 'templates/breadcrumb.php';
+ }
+
+ public static function get_wiki_sidebar( $wiki ) {
+ $articles = wmw_get_articles( $wiki->ID );
+ $categories = get_terms( array( 'taxonomy' => 'wmw_category', 'hide_empty' => false ) );
+ ob_start();
+ include WMW_PLUGIN_DIR . 'templates/sidebar.php';
+ return ob_get_clean();
+ }
+}
diff --git a/includes/class-wmw-gitea-importer.php b/includes/class-wmw-gitea-importer.php
new file mode 100644
index 0000000..7a2d852
--- /dev/null
+++ b/includes/class-wmw-gitea-importer.php
@@ -0,0 +1,351 @@
+api_base = $base . '/api/v1';
+ $this->owner = $owner;
+ $this->repo = $repo;
+ $this->token = $token;
+ }
+
+ // ── API ───────────────────────────────────────────────────────────────────
+
+ private function api_get( $endpoint ) {
+ $url = $this->api_base . $endpoint;
+ $args = array(
+ 'timeout' => 20,
+ 'headers' => array( 'Accept' => 'application/json' ),
+ );
+ if ( $this->token ) {
+ $args['headers']['Authorization'] = 'token ' . $this->token;
+ }
+ $resp = wp_remote_get( $url, $args );
+ if ( is_wp_error( $resp ) ) {
+ return $resp;
+ }
+ $code = wp_remote_retrieve_response_code( $resp );
+ if ( 200 !== $code ) {
+ return new WP_Error( 'api_error', 'Gitea antwortet mit Status ' . $code . ' – URL: ' . $url );
+ }
+ $body = json_decode( wp_remote_retrieve_body( $resp ), true );
+ if ( JSON_ERROR_NONE !== json_last_error() ) {
+ return new WP_Error( 'json_error', 'Ungültige JSON-Antwort von: ' . $url );
+ }
+ return $body;
+ }
+
+ // ── Öffentliche Methoden ──────────────────────────────────────────────────
+
+ public function list_pages() {
+ $endpoint = '/repos/' . rawurlencode( $this->owner ) . '/' . rawurlencode( $this->repo ) . '/wiki/pages?limit=50';
+ $result = $this->api_get( $endpoint );
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+ if ( ! is_array( $result ) ) {
+ return new WP_Error( 'invalid_response', 'Ungültige API-Antwort.' );
+ }
+ $pages = array();
+ foreach ( $result as $page ) {
+ if ( ! is_array( $page ) ) {
+ continue;
+ }
+ $last = '';
+ if ( isset( $page['last_commit']['created'] ) ) {
+ $last = $page['last_commit']['created'];
+ }
+ $pages[] = array(
+ 'title' => isset( $page['title'] ) ? $page['title'] : '',
+ 'last_updated' => $last,
+ );
+ }
+ return $pages;
+ }
+
+ public function get_page( $page_name ) {
+ $endpoint = '/repos/' . rawurlencode( $this->owner ) . '/' . rawurlencode( $this->repo ) . '/wiki/page/' . rawurlencode( $page_name );
+ $result = $this->api_get( $endpoint );
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+ if ( ! is_array( $result ) ) {
+ return new WP_Error( 'invalid_response', 'Ungültige Seitenantwort für: ' . $page_name );
+ }
+ $markdown = '';
+ if ( ! empty( $result['content_base64'] ) ) {
+ $markdown = base64_decode( $result['content_base64'] ); // phpcs:ignore
+ }
+ if ( empty( $markdown ) && ! empty( $result['content'] ) ) {
+ $markdown = $result['content'];
+ }
+ $title = isset( $result['title'] ) ? $result['title'] : $page_name;
+ return array(
+ 'title' => $title,
+ 'content' => self::markdown_to_html( $markdown ),
+ 'raw' => $markdown,
+ );
+ }
+
+ public function import_all( $wiki_id, $skip_pages = array( 'Home', '_Sidebar' ), $update_existing = false ) {
+ $log = array( 'imported' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => array() );
+
+ $pages = $this->list_pages();
+ if ( is_wp_error( $pages ) ) {
+ $log['errors'][] = $pages->get_error_message();
+ return $log;
+ }
+
+ foreach ( $pages as $p ) {
+ $title = $p['title'];
+
+ if ( in_array( $title, $skip_pages, true ) ) {
+ $log['skipped']++;
+ continue;
+ }
+
+ $existing = get_posts( array(
+ 'post_type' => 'wmw_article',
+ 'post_status' => 'any',
+ 'title' => $title,
+ 'posts_per_page' => 1,
+ 'meta_query' => array(
+ array( 'key' => '_wmw_wiki_id', 'value' => $wiki_id ),
+ ),
+ ) );
+
+ if ( $existing && ! $update_existing ) {
+ $log['skipped']++;
+ continue;
+ }
+
+ $page_data = $this->get_page( $title );
+ if ( is_wp_error( $page_data ) ) {
+ $log['errors'][] = 'Fehler bei "' . $title . '": ' . $page_data->get_error_message();
+ continue;
+ }
+
+ $post_data = array(
+ 'post_type' => 'wmw_article',
+ 'post_title' => $page_data['title'],
+ 'post_content' => $page_data['content'],
+ 'post_status' => 'publish',
+ );
+
+ if ( $existing && $update_existing ) {
+ $post_data['ID'] = $existing[0]->ID;
+ $result = wp_update_post( $post_data, true );
+ if ( ! is_wp_error( $result ) ) {
+ $log['updated']++;
+ }
+ } else {
+ $result = wp_insert_post( $post_data, true );
+ if ( ! is_wp_error( $result ) ) {
+ $log['imported']++;
+ }
+ }
+
+ if ( is_wp_error( $result ) ) {
+ $log['errors'][] = 'WP-Fehler bei "' . $title . '": ' . $result->get_error_message();
+ continue;
+ }
+
+ update_post_meta( $result, '_wmw_wiki_id', absint( $wiki_id ) );
+ update_post_meta(
+ $result,
+ '_wmw_gitea_source',
+ $this->api_base . '/repos/' . rawurlencode( $this->owner ) . '/' . rawurlencode( $this->repo ) . '/wiki/page/' . rawurlencode( $title )
+ );
+
+ $auto_cat = self::guess_category( $title );
+ if ( $auto_cat ) {
+ $term = term_exists( $auto_cat, 'wmw_category' );
+ if ( ! $term ) {
+ $term = wp_insert_term( $auto_cat, 'wmw_category' );
+ }
+ if ( ! is_wp_error( $term ) ) {
+ $term_id = is_array( $term ) ? (int) $term['term_id'] : (int) $term;
+ wp_set_object_terms( $result, $term_id, 'wmw_category', true );
+ }
+ }
+ }
+
+ return $log;
+ }
+
+ // ── Markdown → HTML ───────────────────────────────────────────────────────
+
+ public static function markdown_to_html( $md ) {
+ if ( empty( $md ) ) {
+ return '';
+ }
+
+ // Gitea [[Wiki-Links]]
+ $md = preg_replace( '/\[\[([^\]|]+)\|([^\]]+)\]\]/', '[$2]($1)', $md );
+ $md = preg_replace( '/\[\[([^\]]+)\]\]/', '[$1]($1)', $md );
+
+ // Code-Blöcke sichern
+ $code_blocks = array();
+ $md = preg_replace_callback(
+ '/```(\w*)\n(.*?)```/s',
+ function ( $m ) use ( &$code_blocks ) {
+ $lang = '';
+ if ( ! empty( $m[1] ) ) {
+ $lang = ' class="language-' . esc_attr( $m[1] ) . '"';
+ }
+ $token = '%%CB_' . count( $code_blocks ) . '%%';
+ $code_blocks[ $token ] = '' . esc_html( $m[2] ) . '
';
+ return $token;
+ },
+ $md
+ );
+
+ // Inline Code
+ $md = preg_replace( '/`([^`\n]+)`/', '$1', $md );
+
+ // Überschriften
+ $md = preg_replace( '/^######\s+(.+)$/m', '$1
', $md );
+ $md = preg_replace( '/^#####\s+(.+)$/m', '$1
', $md );
+ $md = preg_replace( '/^####\s+(.+)$/m', '$1
', $md );
+ $md = preg_replace( '/^###\s+(.+)$/m', '$1
', $md );
+ $md = preg_replace( '/^##\s+(.+)$/m', '$1
', $md );
+ $md = preg_replace( '/^#\s+(.+)$/m', '$1
', $md );
+
+ // Formatierung
+ $md = preg_replace( '/\*\*\*(.+?)\*\*\*/s', '$1', $md );
+ $md = preg_replace( '/\*\*(.+?)\*\*/s', '$1', $md );
+ $md = preg_replace( '/\*(.+?)\*/s', '$1', $md );
+ $md = preg_replace( '/__(.+?)__/s', '$1', $md );
+ $md = preg_replace( '/_([^_\s][^_]*)_/', '$1', $md );
+ $md = preg_replace( '/~~(.+?)~~/s', '$1', $md );
+
+ // Bilder & Links
+ $md = preg_replace( '/!\[([^\]]*)\]\(([^)]+)\)/', '
', $md );
+ $md = preg_replace( '/\[([^\]]+)\]\(([^)]+)\)/', '$1', $md );
+
+ // Blockquote & HR
+ $md = preg_replace( '/^>\s+(.+)$/m', '$1
', $md );
+ $md = preg_replace( '/^(\-{3,}|\*{3,}|_{3,})$/m', '
', $md );
+
+ // Tabellen
+ $md = preg_replace_callback(
+ '/^(\|.+\|\n)+/m',
+ array( 'WMW_Gitea_Importer', 'cb_table' ),
+ $md
+ );
+
+ // Ungeordnete Listen
+ $md = preg_replace_callback(
+ '/^([ \t]*[\*\-\+] .+\n?)+/m',
+ array( 'WMW_Gitea_Importer', 'cb_ul' ),
+ $md
+ );
+
+ // Geordnete Listen
+ $md = preg_replace_callback(
+ '/^([ \t]*\d+\. .+\n?)+/m',
+ array( 'WMW_Gitea_Importer', 'cb_ol' ),
+ $md
+ );
+
+ // Absätze
+ $blocks = preg_split( '/\n{2,}/', $md );
+ $html = '';
+ foreach ( $blocks as $block ) {
+ $block = trim( $block );
+ if ( empty( $block ) ) {
+ continue;
+ }
+ if ( preg_match( '/^<(h[1-6]|ul|ol|blockquote|pre|table|hr|img|%%CB_)/i', $block ) ) {
+ $html .= $block . "\n";
+ } else {
+ $html .= '' . nl2br( $block ) . "
\n";
+ }
+ }
+
+ // Code-Blöcke zurücksetzen
+ foreach ( $code_blocks as $token => $code_html ) {
+ $html = str_replace( $token, $code_html, $html );
+ }
+
+ return $html;
+ }
+
+ /** @param array $m */
+ private static function cb_table( $m ) {
+ $rows = array_filter( explode( "\n", trim( $m[0] ) ) );
+ $html = '';
+ $first = true;
+ foreach ( $rows as $row ) {
+ $row = trim( $row );
+ if ( empty( $row ) ) {
+ continue;
+ }
+ if ( preg_match( '/^\|[-|:\s]+\|$/', $row ) ) {
+ continue; // Trennzeile
+ }
+ $cells = array_map( 'trim', explode( '|', trim( $row, '|' ) ) );
+ $tag = $first ? 'th' : 'td';
+ $html .= '';
+ foreach ( $cells as $cell ) {
+ $html .= '<' . $tag . '>' . $cell . '' . $tag . '>';
+ }
+ $html .= '
';
+ $first = false;
+ }
+ return $html . '
';
+ }
+
+ /** @param array $m */
+ private static function cb_ul( $m ) {
+ $lines = array_filter( explode( "\n", trim( $m[0] ) ) );
+ $html = '';
+ foreach ( $lines as $line ) {
+ $text = preg_replace( '/^[ \t]*[\*\-\+] /', '', $line );
+ $html .= '- ' . $text . '
';
+ }
+ return $html . '
';
+ }
+
+ /** @param array $m */
+ private static function cb_ol( $m ) {
+ $lines = array_filter( explode( "\n", trim( $m[0] ) ) );
+ $html = '';
+ foreach ( $lines as $line ) {
+ $text = preg_replace( '/^[ \t]*\d+\. /', '', $line );
+ $html .= '- ' . $text . '
';
+ }
+ return $html . '
';
+ }
+
+ /** @param string $title */
+ private static function guess_category( $title ) {
+ $map = array(
+ 'Installation' => 'Grundlagen',
+ 'Befehle' => 'Grundlagen',
+ 'Berechtigungen' => 'Grundlagen',
+ 'config' => 'Konfiguration',
+ 'settings' => 'Konfiguration',
+ 'visuals' => 'Konfiguration',
+ 'PlaceholderAPI' => 'Extras',
+ 'Sicherheit' => 'Extras',
+ );
+ return isset( $map[ $title ] ) ? $map[ $title ] : '';
+ }
+}
diff --git a/includes/class-wmw-metaboxes.php b/includes/class-wmw-metaboxes.php
new file mode 100644
index 0000000..3a2b15d
--- /dev/null
+++ b/includes/class-wmw-metaboxes.php
@@ -0,0 +1,165 @@
+ID, '_wmw_wiki_id', true );
+ $order = get_post_meta( $post->ID, '_wmw_order', true ) ?: 0;
+ $hide_toc = get_post_meta( $post->ID, '_wmw_hide_toc', true );
+
+ // Alle Wikis laden
+ $wikis = get_posts( [
+ 'post_type' => 'wmw_wiki',
+ 'numberposts' => -1,
+ 'orderby' => 'title',
+ 'order' => 'ASC',
+ ] );
+ ?>
+
+
+
+
+
+
+
+ Niedrigere Zahl = weiter oben
+
+
+
+
+ ID, '_wmw_icon_url', true );
+ $icon_class = get_post_meta( $post->ID, '_wmw_icon_class', true ) ?: 'dashicons-book-alt';
+ $color = get_post_meta( $post->ID, '_wmw_color', true ) ?: '#0073aa';
+ $version = get_post_meta( $post->ID, '_wmw_version', true );
+ ?>
+
+ 0 ) {
+ update_post_meta( $post_id, '_wmw_wiki_id', $wiki_id );
+ } else {
+ delete_post_meta( $post_id, '_wmw_wiki_id' );
+ }
+ }
+
+ if ( isset( $_POST['wmw_order'] ) ) {
+ update_post_meta( $post_id, '_wmw_order', intval( $_POST['wmw_order'] ) );
+ }
+
+ if ( isset( $_POST['wmw_hide_toc'] ) ) {
+ update_post_meta( $post_id, '_wmw_hide_toc', '1' );
+ } else {
+ delete_post_meta( $post_id, '_wmw_hide_toc' );
+ }
+ }
+
+ // ── Speichern: Wiki ─────────────────────────────────────────────────────
+ public static function save_wiki( $post_id, $post ) {
+ if ( ! isset( $_POST['wmw_wiki_nonce'] ) ) return;
+ if ( ! wp_verify_nonce( $_POST['wmw_wiki_nonce'], 'wmw_save_wiki' ) ) return;
+ if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) return;
+ if ( ! current_user_can( 'manage_options' ) ) return;
+
+ $fields = [ 'wmw_icon_url', 'wmw_icon_class', 'wmw_color', 'wmw_version' ];
+ foreach ( $fields as $field ) {
+ if ( isset( $_POST[ $field ] ) ) {
+ update_post_meta( $post_id, "_$field", sanitize_text_field( $_POST[ $field ] ) );
+ }
+ }
+ }
+}
diff --git a/includes/class-wmw-post-types.php b/includes/class-wmw-post-types.php
new file mode 100644
index 0000000..c386635
--- /dev/null
+++ b/includes/class-wmw-post-types.php
@@ -0,0 +1,95 @@
+ array(
+ 'name' => 'Wikis',
+ 'singular_name' => 'Wiki',
+ 'add_new' => 'Neues Wiki',
+ 'add_new_item' => 'Neues Wiki erstellen',
+ 'edit_item' => 'Wiki bearbeiten',
+ 'view_item' => 'Wiki ansehen',
+ 'search_items' => 'Wikis durchsuchen',
+ 'not_found' => 'Keine Wikis gefunden',
+ 'menu_name' => 'WP Multi Wiki',
+ 'all_items' => 'Alle Wikis',
+ ),
+ 'public' => true,
+ 'has_archive' => true,
+ 'show_in_menu' => false,
+ 'show_in_rest' => true,
+ 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ),
+ 'rewrite' => array( 'slug' => 'wiki', 'with_front' => false ),
+ 'menu_icon' => 'dashicons-book-alt',
+ 'capability_type' => 'post',
+ ) );
+
+ // ── Wiki Article ───────────────────────────────────────────────────
+ register_post_type( 'wmw_article', array(
+ 'labels' => array(
+ 'name' => 'Wiki-Artikel',
+ 'singular_name' => 'Wiki-Artikel',
+ 'add_new' => 'Neuer Artikel',
+ 'add_new_item' => 'Neuen Artikel erstellen',
+ 'edit_item' => 'Artikel bearbeiten',
+ 'view_item' => 'Artikel ansehen',
+ 'search_items' => 'Artikel durchsuchen',
+ 'not_found' => 'Keine Artikel gefunden',
+ 'all_items' => 'Alle Artikel',
+ ),
+ 'public' => true,
+ 'has_archive' => false,
+ 'show_in_menu' => false,
+ 'show_in_rest' => true,
+ 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields', 'revisions' ),
+ 'rewrite' => array( 'slug' => 'wiki-artikel', 'with_front' => false ),
+ 'menu_icon' => 'dashicons-media-document',
+ 'capability_type' => 'post',
+ ) );
+ }
+
+ public function register_taxonomies() {
+
+ // ── Wiki Category ──────────────────────────────────────────────────
+ register_taxonomy( 'wmw_category', 'wmw_article', array(
+ 'labels' => array(
+ 'name' => 'Wiki-Kategorien',
+ 'singular_name' => 'Kategorie',
+ 'add_new_item' => 'Neue Kategorie',
+ 'edit_item' => 'Kategorie bearbeiten',
+ 'all_items' => 'Alle Kategorien',
+ ),
+ 'public' => true,
+ 'hierarchical' => true,
+ 'show_in_rest' => true,
+ 'show_admin_column' => true,
+ 'rewrite' => array( 'slug' => 'wiki-kategorie' ),
+ ) );
+
+ // ── Wiki Tag ───────────────────────────────────────────────────────
+ register_taxonomy( 'wmw_tag', 'wmw_article', array(
+ 'labels' => array(
+ 'name' => 'Wiki-Tags',
+ 'singular_name' => 'Tag',
+ 'add_new_item' => 'Neues Tag',
+ 'edit_item' => 'Tag bearbeiten',
+ 'all_items' => 'Alle Tags',
+ ),
+ 'public' => true,
+ 'hierarchical' => false,
+ 'show_in_rest' => true,
+ 'show_admin_column' => true,
+ 'rewrite' => array( 'slug' => 'wiki-tag' ),
+ ) );
+ }
+}
diff --git a/includes/class-wmw-search.php b/includes/class-wmw-search.php
new file mode 100644
index 0000000..0e340b6
--- /dev/null
+++ b/includes/class-wmw-search.php
@@ -0,0 +1,169 @@
+post_status !== 'publish' ) return;
+
+ $wiki_id = (int) get_post_meta( $post_id, '_wmw_wiki_id', true );
+ $keywords = wp_strip_all_tags( $post->post_title . ' ' . $post->post_content . ' ' . $post->post_excerpt );
+
+ $wpdb->replace(
+ $wpdb->prefix . 'wmw_search_index',
+ array(
+ 'article_id' => $post_id,
+ 'wiki_id' => $wiki_id,
+ 'keywords' => $keywords,
+ ),
+ array( '%d', '%d', '%s' )
+ );
+ }
+
+ /**
+ * Artikel aus dem Index entfernen.
+ */
+ public function remove_from_index( $post_id ) {
+ global $wpdb;
+ $wpdb->delete( $wpdb->prefix . 'wmw_search_index', array( 'article_id' => $post_id ), array( '%d' ) );
+ }
+
+ /**
+ * Alle vorhandenen Artikel in den Index aufnehmen.
+ * Wird aufgerufen wenn der Index leer ist (z.B. nach Gitea-Import).
+ */
+ public static function reindex_all() {
+ global $wpdb;
+
+ $articles = get_posts( array(
+ 'post_type' => 'wmw_article',
+ 'post_status' => 'publish',
+ 'posts_per_page' => -1,
+ ) );
+
+ foreach ( $articles as $post ) {
+ $wiki_id = (int) get_post_meta( $post->ID, '_wmw_wiki_id', true );
+ $keywords = wp_strip_all_tags( $post->post_title . ' ' . $post->post_content . ' ' . $post->post_excerpt );
+
+ $wpdb->replace(
+ $wpdb->prefix . 'wmw_search_index',
+ array(
+ 'article_id' => $post->ID,
+ 'wiki_id' => $wiki_id,
+ 'keywords' => $keywords,
+ ),
+ array( '%d', '%d', '%s' )
+ );
+ }
+
+ return count( $articles );
+ }
+
+ /**
+ * Artikel suchen.
+ * 1. Versucht den schnellen Custom-Index.
+ * 2. Fällt auf WP_Query zurück, wenn der Index leer ist (z.B. nach Gitea-Import).
+ * 3. Baut den Index automatisch auf, wenn er leer war.
+ */
+ public static function search( $query, $wiki_id = 0, $limit = 20 ) {
+ global $wpdb;
+
+ $table = $wpdb->prefix . 'wmw_search_index';
+ $like = '%' . $wpdb->esc_like( $query ) . '%';
+
+ // ── Prüfen ob der Index überhaupt Einträge hat ────────────────────
+ $index_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $table" );
+
+ $ids = array();
+
+ if ( $index_count > 0 ) {
+ // ── Custom-Index nutzen ───────────────────────────────────────
+ if ( $wiki_id ) {
+ $rows = $wpdb->get_results( $wpdb->prepare(
+ "SELECT article_id FROM $table WHERE wiki_id = %d AND keywords LIKE %s LIMIT %d",
+ $wiki_id, $like, $limit
+ ) );
+ } else {
+ $rows = $wpdb->get_results( $wpdb->prepare(
+ "SELECT article_id FROM $table WHERE keywords LIKE %s LIMIT %d",
+ $like, $limit
+ ) );
+ }
+ $ids = wp_list_pluck( $rows, 'article_id' );
+ }
+
+ // ── Fallback: WP_Query (wenn Index leer oder keine Treffer) ───────
+ if ( empty( $ids ) ) {
+ $args = array(
+ 'post_type' => 'wmw_article',
+ 'post_status' => 'publish',
+ 'posts_per_page' => $limit,
+ 's' => $query,
+ 'orderby' => 'relevance',
+ );
+
+ if ( $wiki_id ) {
+ $args['meta_query'] = array(
+ array(
+ 'key' => '_wmw_wiki_id',
+ 'value' => $wiki_id,
+ ),
+ );
+ }
+
+ $fallback = get_posts( $args );
+
+ // Index war leer → jetzt automatisch aufbauen (im Hintergrund)
+ if ( $index_count === 0 ) {
+ self::reindex_all();
+ }
+
+ foreach ( $fallback as &$post ) {
+ $excerpt = $post->post_excerpt ?: wp_trim_words( $post->post_content, 25 );
+ $post->wmw_excerpt = self::highlight( $excerpt, $query );
+ $post->wmw_title = self::highlight( $post->post_title, $query );
+ }
+
+ return $fallback;
+ }
+
+ // ── Artikel-Objekte aus IDs laden ─────────────────────────────────
+ $posts = get_posts( array(
+ 'post_type' => 'wmw_article',
+ 'post_status' => 'publish',
+ 'post__in' => $ids,
+ 'posts_per_page' => $limit,
+ 'orderby' => 'post_title',
+ 'order' => 'ASC',
+ ) );
+
+ foreach ( $posts as &$post ) {
+ $excerpt = $post->post_excerpt ?: wp_trim_words( $post->post_content, 25 );
+ $post->wmw_excerpt = self::highlight( $excerpt, $query );
+ $post->wmw_title = self::highlight( $post->post_title, $query );
+ }
+
+ return $posts;
+ }
+
+ /**
+ * Suchtreffer hervorheben.
+ */
+ public static function highlight( $text, $query ) {
+ $words = explode( ' ', preg_quote( trim( $query ), '/' ) );
+ foreach ( $words as $word ) {
+ if ( strlen( $word ) < 2 ) continue;
+ $text = preg_replace( '/(' . $word . ')/iu', '$1', $text );
+ }
+ return $text;
+ }
+}
\ No newline at end of file
diff --git a/includes/class-wmw-shortcodes.php b/includes/class-wmw-shortcodes.php
new file mode 100644
index 0000000..1eac0c9
--- /dev/null
+++ b/includes/class-wmw-shortcodes.php
@@ -0,0 +1,88 @@
+ 0,
+ 'slug' => '',
+ 'layout' => 'grid', // grid | list
+ ), $atts, 'wmw_wiki' );
+
+ $wiki = null;
+ if ( $atts['id'] ) {
+ $wiki = get_post( absint( $atts['id'] ) );
+ } elseif ( $atts['slug'] ) {
+ $wiki = get_page_by_path( $atts['slug'], OBJECT, 'wmw_wiki' );
+ }
+
+ if ( ! $wiki || $wiki->post_type !== 'wmw_wiki' ) return '';
+
+ ob_start();
+ WMW_Frontend::render_wiki_index( $wiki, $atts['layout'] );
+ return ob_get_clean();
+ }
+
+ /**
+ * [wmw_article id="5"]
+ */
+ public function single_article( $atts ) {
+ $atts = shortcode_atts( array( 'id' => 0 ), $atts, 'wmw_article' );
+ $article = get_post( absint( $atts['id'] ) );
+ if ( ! $article || $article->post_type !== 'wmw_article' ) return '';
+
+ ob_start();
+ WMW_Frontend::render_article( $article );
+ return ob_get_clean();
+ }
+
+ /**
+ * [wmw_search wiki_id="1" placeholder="Suche..."]
+ */
+ public function search_box( $atts ) {
+ $atts = shortcode_atts( array(
+ 'wiki_id' => 0,
+ 'placeholder' => 'Wiki durchsuchen…',
+ ), $atts, 'wmw_search' );
+
+ ob_start();
+ WMW_Frontend::render_search_box( absint( $atts['wiki_id'] ), $atts['placeholder'] );
+ return ob_get_clean();
+ }
+
+ /**
+ * [wmw_wiki_list columns="3"]
+ */
+ public function wiki_list( $atts ) {
+ $atts = shortcode_atts( array( 'columns' => 3 ), $atts, 'wmw_wiki_list' );
+ $wikis = wmw_get_wikis();
+ if ( empty( $wikis ) ) return '';
+
+ ob_start();
+ WMW_Frontend::render_wiki_list( $wikis, absint( $atts['columns'] ) );
+ return ob_get_clean();
+ }
+
+ /**
+ * [wmw_breadcrumb]
+ */
+ public function breadcrumb( $atts ) {
+ global $post;
+ if ( ! $post ) return '';
+ ob_start();
+ WMW_Frontend::render_breadcrumb( $post );
+ return ob_get_clean();
+ }
+}
diff --git a/includes/class-wmw-toc.php b/includes/class-wmw-toc.php
new file mode 100644
index 0000000..b392aae
--- /dev/null
+++ b/includes/class-wmw-toc.php
@@ -0,0 +1,68 @@
+ ..., 'toc' => ...]
+ */
+ public static function process( $content ) {
+ if ( empty( $content ) ) return array( 'content' => $content, 'toc' => '' );
+
+ $headings = array();
+ $counter = array();
+
+ // Add IDs to h2, h3, h4
+ $content = preg_replace_callback(
+ '/<(h[234])([^>]*)>(.*?)<\/h[234]>/si',
+ function( $matches ) use ( &$headings, &$counter ) {
+ $tag = $matches[1];
+ $attrs = $matches[2];
+ $text = wp_strip_all_tags( $matches[3] );
+ $slug = sanitize_title( $text );
+
+ // Make unique
+ if ( isset( $counter[ $slug ] ) ) {
+ $counter[ $slug ]++;
+ $slug .= '-' . $counter[ $slug ];
+ } else {
+ $counter[ $slug ] = 0;
+ }
+
+ $headings[] = array(
+ 'level' => (int) substr( $tag, 1 ),
+ 'id' => $slug,
+ 'text' => $text,
+ );
+
+ return "<{$tag}{$attrs} id=\"{$slug}\">{$matches[3]}{$tag}>";
+ },
+ $content
+ );
+
+ if ( empty( $headings ) || count( $headings ) < 2 ) {
+ return array( 'content' => $content, 'toc' => '' );
+ }
+
+ // Build TOC HTML
+ $toc = '';
+ $toc .= '
' . __( 'Inhalt', 'wp-multi-wiki' ) . '
';
+ $toc .= '
';
+
+ $prev_level = 2;
+ foreach ( $headings as $h ) {
+ if ( $h['level'] > $prev_level ) {
+ $toc .= '';
+ } elseif ( $h['level'] < $prev_level ) {
+ $toc .= '
';
+ }
+ $toc .= '- ' . esc_html( $h['text'] ) . '
';
+ $prev_level = $h['level'];
+ }
+
+ $toc .= '
';
+
+ return array( 'content' => $content, 'toc' => $toc );
+ }
+}