{"id":8058,"date":"2025-06-24T10:37:36","date_gmt":"2025-06-24T08:37:36","guid":{"rendered":"https:\/\/swissmakers.ch\/?p=8058"},"modified":"2025-07-07T12:43:25","modified_gmt":"2025-07-07T10:43:25","slug":"dynamic-landingpages-wikijs","status":"publish","type":"post","link":"https:\/\/swissmakers.ch\/en\/dynamische-landingpages-wikijs\/","title":{"rendered":"Dynamic landing pages with Wiki.js v2"},"content":{"rendered":"\n<p>Wer mit <a href=\"https:\/\/js.wiki\/\" target=\"_blank\" rel=\"noreferrer noopener\">Wiki.js v2<\/a> produktiv arbeitet, bemerkt schnell eine L\u00fccke: Es gibt keine eingebaute M\u00f6glichkeit, auf einer Seite automatisch alle Unterseiten aufzulisten. Jede Landingpage, ob f\u00fcr Team-Wikis, Projektbereiche oder Dokumentationen, muss heute manuell gepflegt werden. Sobald eine neue Unterseite angelegt, verschoben oder gel\u00f6scht wird, besteht die Gefahr, dass das zentrale Inhaltsverzeichnis veraltet. In einem lebendigen Unternehmens-Wiki wie unserem ist das nicht tragbar: Wir haben \u00fcber 800 Eintr\u00e4ge und t\u00e4glich kommen welche dazu.<\/p>\n\n\n\n<p>F\u00fcr <strong>Swissmakers GmbH<\/strong> war klar: Eine dynamische Landingpage, die sich ohne manuellen Aufwand aktualisiert, ist essenziell f\u00fcr konsistente Dokumentation und effizientes Auffinden von Informationen.<\/p>\n\n\n\n<h2 class=\"gb-headline gb-headline-2c3e5dcb gb-headline-text\">Warum Wiki.js das nicht von Haus aus mitliefert<\/h2>\n\n\n\n<p>Die Wiki.js-Version 2 basiert intern auf einem <strong>Vue.js Single-Page-Application-Ansatz<\/strong>. Alle Seiteninhalte werden clientseitig nachgeladen; serverseitig generierte Platzhalter wie \u201e[children]\u201c (bekannt aus anderen Markdown-Plugins) existieren nicht. Das Entwicklerteam von Wiki.js hat zwar f\u00fcr Version 3 ein modulares Blocks-System angek\u00fcndigt, doch f\u00fcr v2-Instanzen bleibt nur der Weg \u00fcber die \u00f6ffentliche <strong><a href=\"https:\/\/docs.requarks.io\/dev\/api\" target=\"_blank\" rel=\"noreferrer noopener\">GraphQL-API<\/a><\/strong> oder ein Upgrade. Letzteres ist jedoch im produktiven Umfeld definitiv nicht empfohlen da die v3 seit nun fast zwei Jahren lediglich als Alpha Version verf\u00fcgbar ist und bis heute als &#171;unstable&#187; gilt. Zudem sind die meisten Features noch gar nicht oder nicht richtig implementiert.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Unsere L\u00f6sung: Ein leichtgewichtiger GraphQL-Client im Frontend<\/h2>\n\n\n\n<p>Um das Feature umzusetzen, haben wir ein <strong>JavaScript-Snippet<\/strong> entwickelt, das wir in jeder gew\u00fcnschten Landingpage oder \u00fcbergeordnete &#171;Tech-Page&#187; einbinden k\u00f6nnen. Der Code wird dabei in die jeweilige Seite unter den &#171;Page Properties&#187; im Tab &#171;Script&#187; eingef\u00fcgt:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"580\" src=\"https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-1-1024x580.png\" alt=\"JavaScript-Snippet\" class=\"wp-image-8061\" srcset=\"https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-1-1024x580.png 1024w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-1-300x170.png 300w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-1-768x435.png 768w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-1-1536x870.png 1536w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-1-2048x1161.png 2048w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-1-18x10.png 18w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"gb-headline gb-headline-bcb9abae gb-headline-text\">Der Code folgt beim Aufruf der jeweiligen Seite dem folgendem Ablauf:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>holt einmalig alle Seiten aus der GraphQL-API (Standartlimit bis 1000 gesetzt),<\/li>\n\n\n\n<li>filtert ausschliesslich jene, deren Pfad unterhalb des automatisch erkannten Seitenpfads liegt (also unterhalb der aktuell ge\u00f6ffneten Seite),<\/li>\n\n\n\n<li>entfernt die Landingpage selbst aus der Ausgabe,<\/li>\n\n\n\n<li>rendert das Ergebnis als \u00fcbersichtliche Link-Liste mit dem bereits eingebauten <code>links-list<\/code>-Styling von Wiki.js.<\/li>\n<\/ol>\n\n\n\n<p>In der wiki-Seite selbst wird dann der gerenderte HTML-content innerhalb des<br><code>&lt;div id=\"pageTree\"&gt;Loading pages \u2026&lt;\/div&gt;<\/code> mit dem generierten Menu ersetzt.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"583\" src=\"https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-1024x583.png\" alt=\"Verwendung HTML-Tag\" class=\"wp-image-8060\" srcset=\"https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-1024x583.png 1024w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-300x171.png 300w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-768x437.png 768w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-1536x874.png 1536w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-2048x1165.png 2048w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-18x10.png 18w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"gb-headline gb-headline-3fd9caaa gb-headline-text\">Die Page kann dabei beliebig gestaltet oder erweitert werden, wichtig ist nur, dass das erw\u00e4hnte Div am gew\u00fcnschten Ort innerhalb der Seite platziert wird, wo die dynamische Sub-\u00dcbersicht gew\u00fcnscht ist.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">JavaScript Code<\/h2>\n\n\n\n<p><strong>Achtung<\/strong>: <em>F\u00fcr alle, die diese Seite auf Englisch lesen. Bitte kopiert den Code aus der deutschen Original-Seite. Unsere Deepl-\u00dcbersetzungsapp hat momentan einen Bug und l\u00f6scht teils Code aus Codebl\u00f6cken oder Newlines heraus. Ein Bugreport ist erstellt. Besten Dank und Sorry f\u00fcr die Umst\u00e4nde.<\/em><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript line-numbers\">&lt;script&gt;\n(() =&gt; {\n  const debug = false;               \/\/ auf true setzen, falls etwas nicht wie gew\u00fcnscht funktioniert.\n  const MAX_ATTEMPTS = 30;           \/\/ bis zu 30 \u00d7 200 ms auf #pageTree warten (timeout)\n  let attempts = 0;\n\n  const log   = (...a) =&gt; debug &amp;&amp; console.log(...a);\n  const warn  = (...a) =&gt; debug &amp;&amp; console.warn(...a);\n  const error = (...a) =&gt; debug &amp;&amp; console.error(...a);\n\n  function init() {\n    const container = document.getElementById('pageTree');\n    if (!container) {\n      attempts++;\n      warn(`#pageTree not found, attempt ${attempts}`);\n      if (attempts &lt; MAX_ATTEMPTS) return setTimeout(init, 200);\n      error('Gave up waiting for #pageTree');\n      return;\n    }\n\n    \/** Pfad der aktuellen Seite ohne f\u00fchrenden Locale-Teil *\/\n    const selfPath = location.pathname\n      .replace(\/^\\\/[^\/]+\\\/\/, '')     \/\/ z.B.  \/en\/  entfernen\n      .replace(\/^\\\/|\\\/$\/g, '');      \/\/ f\u00fchrenden \/ und trailing \/ entfernen\n\n    \/** Pr\u00e4fix, das alle childs gemeinsam haben (selfPath + \"\/\") *\/\n    const childPrefix = selfPath.endsWith('\/') ? selfPath : selfPath + '\/';\n\n    log('[Wiki.js] selfPath:', selfPath);\n    log('[Wiki.js] childPrefix:', childPrefix);\n\n    const query = `\n      query {\n        pages {\n          list(limit: 1000, orderBy: PATH, orderByDirection: ASC) {\n            path\n            title\n            locale\n          }\n        }\n      }`;\n\n    fetch('\/graphql', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\/json' },\n      body: JSON.stringify({ query })\n    })\n      .then(r =&gt; r.json())\n      .then(({ data }) =&gt; {\n        if (!data?.pages?.list) {\n          container.textContent = 'API-Fehler: keine Seitendaten.';\n          error('pages.list fehlt im Response', data);\n          return;\n        }\n\n        const pages = data.pages.list\n          .filter(p =&gt; p.path.startsWith(childPrefix)) \/\/ nur echte Unterseiten\n          .filter(p =&gt; p.path !== selfPath);           \/\/ Landingpage selbst soll raus\n\n        log('[Wiki.js] gefilterte Seiten:', pages);\n\n        if (!pages.length) {\n          container.textContent = 'Keine Unterseiten vorhanden.';\n          return;\n        }\n\n        const listHTML = pages.map(\n          p =&gt; `&lt;li&gt;&lt;a href=\"\/${p.locale}\/${p.path}\"&gt;${p.title}&lt;\/a&gt;&lt;\/li&gt;`\n        ).join('');\n\n        container.innerHTML = `&lt;ul class=\"links-list\"&gt;${listHTML}&lt;\/ul&gt;`;\n      })\n      .catch(err =&gt; {\n        container.textContent = 'Fehler beim Laden der Seitenliste.';\n        error(err);\n      });\n  }\n\n  init();\n})();\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Was passiert im Detail?<\/h3>\n\n\n\n<p>Nachfolgend haben wir f\u00fcr jene, die sich f\u00fcr JavaScript interessieren oder einfach genauer wissen m\u00f6chten, was Zeile f\u00fcr Zeile genau passiert, das Ganze auf die einzelnen Zeilen aufgeschl\u00fcsselt. <em>(Die Zeilennummern beziehen sich auf das komplette Snippet, inklusive <code>&lt;script&gt;<\/code>-Tag.)<\/em><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Zeile(n)<\/th><th>Code<\/th><th>Technische Erkl\u00e4rung<\/th><\/tr><\/thead><tbody><tr><td><strong>1<\/strong><\/td><td><code>&lt;script&gt;<\/code><\/td><td>\u00d6ffnet einen HTML-Script-Block \u2013 der Browser interpretiert alles bis <code>&lt;\/script&gt;<\/code> als JavaScript.<\/td><\/tr><tr><td><strong>2<\/strong><\/td><td><code>(() =&gt; {<\/code><\/td><td>Start eines IIFE (Immediately Invoked Function Expression). Dadurch liegt der gesamte Code in einem eigenen Scope; globale Namens\u00adkollisionen werden vermieden.<\/td><\/tr><tr><td><strong>3<\/strong><\/td><td><code>const debug = true;<\/code><\/td><td>Schalter f\u00fcr ausf\u00fchrliche Konsolen\u00adausgabe. Bei <code>false<\/code> werden alle Log-Aufrufe unterdr\u00fcckt.<\/td><\/tr><tr><td><strong>4<\/strong><\/td><td><code>const MAX_ATTEMPTS = 30;<\/code><\/td><td>Maximale Anzahl Wiederholungen beim Polling auf das DOM-Element.<\/td><\/tr><tr><td><strong>5<\/strong><\/td><td><code>let attempts = 0;<\/code><\/td><td>Z\u00e4hler f\u00fcr die bereits erfolgten Polling-Versuche.<\/td><\/tr><tr><td><strong>7 \u2013 9<\/strong><\/td><td><code>const log\u2026warn\u2026error\u2026<\/code><\/td><td>Drei Arrow Functions, die jeweils nur dann auf <code>console.*<\/code> zugreifen, wenn <code>debug<\/code> aktiv ist. So spart man if-Bl\u00f6cke in der Logik darunter.<\/td><\/tr><tr><td><strong>11<\/strong><\/td><td><code>function init() {<\/code><\/td><td>Hauptfunktion. Sie wird wiederholt aufgerufen, bis das Ziel-Element vorhanden ist.<\/td><\/tr><tr><td><strong>12<\/strong><\/td><td><code>const container = document.getElementById('pageTree');<\/code><\/td><td>Sucht das DIV, in dem sp\u00e4ter die Linkliste gerendert wird.<\/td><\/tr><tr><td><strong>13 \u2013 18<\/strong><\/td><td><code>if (!container) { \u2026 }<\/code><\/td><td>Polling-Logik:<br>\u2022 Z\u00e4hler erh\u00f6hen<br>\u2022 Warnung loggen<br>\u2022 Wenn <code>attempts &lt; MAX_ATTEMPTS<\/code>, mit <code>setTimeout(init, 200)<\/code> nach 200 ms erneut pr\u00fcfen.<br>\u2022 Nach dem Limit Abbruch mit Fehlermeldung.<\/td><\/tr><tr><td><strong>22 \u2013 24<\/strong><\/td><td><code>const selfPath = location.pathname.replace\u2026<\/code><\/td><td>Ermittelt den Pfad der aktuellen Wiki-Seite, entfernt dabei:<br>1. die Sprachpr\u00e4fix-Sektion (Regex <code>^\\\/[^\\\/]+\\\/<\/code>)<br>2. evtl. f\u00fchrenden bzw. abschliessenden Slash.<\/td><\/tr><tr><td><strong>27<\/strong><\/td><td><code>const childPrefix = selfPath.endsWith('\/') ? selfPath : selfPath + '\/';<\/code><\/td><td>Stellt sicher, dass der Child-Pr\u00e4fix immer mit <code>\/<\/code> endet. Beispiel: <code>07-internal-it\/it-services\/<\/code>.<\/td><\/tr><tr><td><strong>29 \u2013 30<\/strong><\/td><td><code>log('[Wiki.js] selfPath:' \u2026)<\/code><\/td><td>Debug-Ausgabe des aktuellen Pfades und des sub-Pr\u00e4fixes.<\/td><\/tr><tr><td><strong>32 \u2013 41<\/strong><\/td><td><code>const query = \\<\/code>\u2026`;`<\/td><td>Template Literal mit GraphQL-Query.<br>\u2022 <code>pages.list<\/code> liefert bis zu 1000 Seiten.<br>\u2022 Sortiert nach Pfad, damit childs automatisch gruppiert sind.<\/td><\/tr><tr><td><strong>43 \u2013 47<\/strong><\/td><td><code>fetch('\/graphql', { \u2026 })<\/code><\/td><td>Stellt einen HTTP-POST an den Wiki.js-GraphQL-Endpunkt. Header <code>Content-Type: application\/json<\/code> ist Pflicht.<\/td><\/tr><tr><td><strong>48<\/strong><\/td><td><code>}).then(r =&gt; r.json())<\/code><\/td><td>Erster Promise-Schritt: Die Antwort wird per <code>Response.json()<\/code> geparst.<\/td><\/tr><tr><td><strong>49<\/strong><\/td><td><code>.then(({ data }) =&gt; {<\/code><\/td><td>Zweiter Promise-Schritt: ES6-Destrukturierung, um <code>data<\/code> sofort herauszuziehen.<\/td><\/tr><tr><td><strong>50 \u2013 54<\/strong><\/td><td><code>if (!data?.pages?.list) { \u2026 }<\/code><\/td><td>Pr\u00fcfung auf Existenz der erwarteten Struktur unter Nutzung von Optional Chaining (<code>?.<\/code>). Bei Fehler: Meldung f\u00fcr den Benutzer und Logging.<\/td><\/tr><tr><td><strong>56 \u2013 58<\/strong><\/td><td><code>const pages = data.pages.list.filter\u2026<\/code><\/td><td>Zweistufiger Filter:<br>1. <code>startsWith(childPrefix)<\/code> \u2192 nur echte Unterseiten<br>2. <code>!== selfPath<\/code> \u2192 eigene Seite ausschliessen.<\/td><\/tr><tr><td><strong>60<\/strong><\/td><td><code>log('[Wiki.js] gefilterte Seiten:', pages);<\/code><\/td><td>Debug-Ausgabe des Ergebnisses nach dem Filter.<\/td><\/tr><tr><td><strong>62 \u2013 65<\/strong><\/td><td><code>if (!pages.length) { \u2026 }<\/code><\/td><td>Fehlermeldung, falls keine Unterseiten existieren (z. B. nach Migration).<\/td><\/tr><tr><td><strong>67 \u2013 69<\/strong><\/td><td><code>const listHTML = pages.map(p =&gt; `&lt;li&gt;\u2026`).join('');<\/code><\/td><td>Baut f\u00fcr jede Unterseite ein <code>&lt;li&gt;&lt;a \u2026&gt;&lt;\/a&gt;&lt;\/li&gt;<\/code> und verbindet alles zu einem HTML-String.<\/td><\/tr><tr><td><strong>71<\/strong><\/td><td><code>container.innerHTML = \\<\/code>&lt;ul class=&#187;links-list&#187;&gt;${listHTML}&lt;\/ul&gt;`;`<\/td><td>Rendert die Linkliste. Die Klasse <code>links-list<\/code> sorgt f\u00fcr das card-artige Styling, das Wiki.js mitliefert.<\/td><\/tr><tr><td><strong>73 \u2013 76<\/strong><\/td><td><code>.catch(err =&gt; { \u2026 })<\/code><\/td><td>Fehlerbehandlung der Fetch-Kette. Zeigt nutzerfreundliche Meldung und loggt das Exception-Objekt.<\/td><\/tr><tr><td><strong>79<\/strong><\/td><td><code>init();<\/code><\/td><td>Erster Aufruf der Init-Funktion \u2013 l\u00f6st das Polling aus.<\/td><\/tr><tr><td><strong>80<\/strong><\/td><td><code>})();<\/code><\/td><td>Schliesst das IIFE und ruft es sofort auf.<\/td><\/tr><tr><td><strong>81<\/strong><\/td><td><code>&lt;\/script&gt;<\/code><\/td><td>Ende des Script-Blocks.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Kleiner Zusatz: Alphabetisches Sortieren nach &#171;Page-Titel&#187; anstatt Pfad<\/h3>\n\n\n\n<p>Im Standard GraphQL-Respond sind die jeweiligen Pages nach dem Pfad (PATH URL) sortiert. Diese kann sich aber vom eigentlichen Titel unterscheiden und so kann es auch Sinn machen, die Seiten nach ihrem Titel alphabetisch zu sortieren.<\/p>\n\n\n\n<p>Daf\u00fcr wird im JavaScript-Code noch vor dem Rendern der Liste <code>const listHTML = pages.map<\/code> (Zeile 67) das Array der Seiten mit der Funktion sort() und localeCompare() nachsortiert. Der ben\u00f6tigte Code sieht so aus:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">pages.sort((a, b) =&gt; a.title.localeCompare(b.title));<\/code><\/pre>\n\n\n\n<p>Die n\u00f6tige \u00c4nderung, respektive Erg\u00e4nzung im Code m\u00fcsste also wie folgt aussehen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\">pages.sort((a, b) =&gt; a.title.localeCompare(b.title));\n\nconst listHTML = pages.map(\n  p =&gt; `&lt;li&gt;&lt;a href=\"\/${p.locale}\/${p.path}\"&gt;${p.title}&lt;\/a&gt;&lt;\/li&gt;`\n).join('');<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Praxiserfahrungen nach der Integration<\/h2>\n\n\n\n<p>Nach wenigen Minuten ist das Snippet in mehreren Bereichen aktiv. Seitdem<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>erscheinen neue Dokumente sofort in der \u00dcbersicht,<\/li>\n\n\n\n<li>entf\u00e4llt die manuelle Pflege der Landingpages \/ Subpages \u00dcbersicht,<\/li>\n\n\n\n<li>bleibt die Navigation auch bei tiefen Verschachtelungen konsistent.<\/li>\n<\/ul>\n\n\n\n<p>Gleichzeitig ist das Skript so schlank, dass es k\u00fcnftige Migrationen bis v3 (sollte diese Version dann doch noch irgendwann erscheinen) nicht behindert. Bis dahin sichert es die Funktionsf\u00e4higkeit unseres Wikis mit minimalem Aufwand.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"618\" src=\"https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-2-1024x618.png\" alt=\"Beispiel Table of Content\" class=\"wp-image-8064\" srcset=\"https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-2-1024x618.png 1024w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-2-300x181.png 300w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-2-768x463.png 768w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-2-1536x926.png 1536w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-2-2048x1235.png 2048w, https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-2-18x12.png 18w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"gb-headline gb-headline-5d2f0110 gb-headline-text\">Der folgende Screenshot zeigt ein Beispiel, bei dem eine alphabetische Sortierung nach dem Page-Namen mehr Sinn machen w\u00fcrde.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Fazit<\/h2>\n\n\n\n<p>Ein fehlendes Kernfeature muss nicht zwangsl\u00e4ufig auf das n\u00e4chste Major-Upgrade warten. Mit einem pr\u00e4zisen Blick in die GraphQL-API und wenigen Zeilen JavaScript konnten wir unsere Dokumentationsqualit\u00e4t deutlich erh\u00f6hen. Wer ebenfalls auf Wiki.js v2 setzt, kann das Snippet ohne weitere Abh\u00e4ngigkeiten \u00fcbernehmen. Fragen oder Verbesserungs\u00advorschl\u00e4ge bitte an <strong><a href=\"mailto:info@swissmakers.ch\">info@swissmakers.ch<\/a><\/strong>, wir tauschen uns gerne aus. F\u00fcr Unterst\u00fctzung bei der Installation und Konfiguration von Wiki.js <a href=\"https:\/\/swissmakers.ch\/kontakt\/\" target=\"_blank\" rel=\"noreferrer noopener\">kontaktieren<\/a> Sie uns ungeniert f\u00fcr ein Beratungsgespr\u00e4ch.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Anyone working productively with Wiki.js v2 will quickly notice a gap: There is no built-in option to ... <\/p>\n<p class=\"read-more-container\"><a title=\"Dynamic landing pages with Wiki.js v2\" class=\"read-more button\" href=\"https:\/\/swissmakers.ch\/en\/dynamische-landingpages-wikijs\/#more-8058\" aria-label=\"Read more about Dynamic landing pages with Wiki.js v2\">Read more<\/a><\/p>","protected":false},"author":2,"featured_media":8064,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_eb_attr":"","_kad_blocks_custom_css":"","_kad_blocks_head_custom_js":"","_kad_blocks_body_custom_js":"","_kad_blocks_footer_custom_js":"","footnotes":""},"categories":[72,1,26,50],"tags":[74,73],"class_list":["post-8058","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-wikijs","category-allgemein","category-enginering","category-programming","tag-javascript","tag-wikijs","generate-columns","tablet-grid-50","mobile-grid-100","grid-parent","grid-50","resize-featured-image"],"taxonomy_info":{"category":[{"value":72,"label":"wikijs"},{"value":1,"label":"Allgemein"},{"value":26,"label":"Engineering"},{"value":50,"label":"Programming"}],"post_tag":[{"value":74,"label":"javascript"},{"value":73,"label":"wikijs"}]},"featured_image_src_large":["https:\/\/swissmakers.ch\/wp-content\/uploads\/2025\/06\/image-2-1024x618.png",1024,618,true],"author_info":{"display_name":"Michael Reber","author_link":"https:\/\/swissmakers.ch\/en\/author\/michael\/"},"comment_info":1,"category_info":[{"term_id":72,"name":"wikijs","slug":"wikijs","term_group":0,"term_taxonomy_id":72,"taxonomy":"category","description":"","parent":0,"count":1,"filter":"raw","cat_ID":72,"category_count":1,"category_description":"","cat_name":"wikijs","category_nicename":"wikijs","category_parent":0},{"term_id":1,"name":"Allgemein","slug":"allgemein","term_group":0,"term_taxonomy_id":1,"taxonomy":"category","description":"","parent":0,"count":3,"filter":"raw","cat_ID":1,"category_count":3,"category_description":"","cat_name":"Allgemein","category_nicename":"allgemein","category_parent":0},{"term_id":26,"name":"Engineering","slug":"enginering","term_group":0,"term_taxonomy_id":26,"taxonomy":"category","description":"","parent":0,"count":5,"filter":"raw","cat_ID":26,"category_count":5,"category_description":"","cat_name":"Engineering","category_nicename":"enginering","category_parent":0},{"term_id":50,"name":"Programming","slug":"programming","term_group":0,"term_taxonomy_id":50,"taxonomy":"category","description":"","parent":0,"count":2,"filter":"raw","cat_ID":50,"category_count":2,"category_description":"","cat_name":"Programming","category_nicename":"programming","category_parent":0}],"tag_info":[{"term_id":74,"name":"javascript","slug":"javascript","term_group":0,"term_taxonomy_id":74,"taxonomy":"post_tag","description":"","parent":0,"count":1,"filter":"raw"},{"term_id":73,"name":"wikijs","slug":"wikijs","term_group":0,"term_taxonomy_id":73,"taxonomy":"post_tag","description":"","parent":0,"count":1,"filter":"raw"}],"_links":{"self":[{"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/posts\/8058","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/comments?post=8058"}],"version-history":[{"count":37,"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/posts\/8058\/revisions"}],"predecessor-version":[{"id":8113,"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/posts\/8058\/revisions\/8113"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/media\/8064"}],"wp:attachment":[{"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/media?parent=8058"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/categories?post=8058"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/swissmakers.ch\/en\/wp-json\/wp\/v2\/tags?post=8058"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}