<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="data:text/xsl;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48eHNsOnN0eWxlc2hlZXQgdmVyc2lvbj0iMy4wIiB4bWxuczp4c2w9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvWFNML1RyYW5zZm9ybSIgeG1sbnM6YXRvbT0iaHR0cDovL3d3dy53My5vcmcvMjAwNS9BdG9tIj48eHNsOm91dHB1dCBtZXRob2Q9Imh0bWwiIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04IiBpbmRlbnQ9InllcyIvPjx4c2w6dGVtcGxhdGUgbWF0Y2g9Ii8iPjx4c2w6dmFyaWFibGUgbmFtZT0ib3duZXJfdXJsIj48eHNsOnZhbHVlLW9mIHNlbGVjdD0iL3Jzcy9jaGFubmVsL2xpbmsiLz48L3hzbDp2YXJpYWJsZT48aHRtbD48aGVhZD48bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEiLz48bWV0YSBuYW1lPSJyZWZlcnJlciIgY29udGVudD0idW5zYWZlLXVybCIvPjx0aXRsZT48eHNsOnZhbHVlLW9mIHNlbGVjdD0iL3Jzcy9jaGFubmVsL3RpdGxlIi8+PC90aXRsZT48bGluayByZWw9InN0eWxlc2hlZXQiIGhyZWY9Imh0dHBzOi8vd3d3LmZlZWQuc3R5bGUvY3NzL3dhdGVyLm1pbi5jc3MiLz48L2hlYWQ+PGJvZHk+PGgxPjxpbWcgYWx0PSJmZWVkIGljb24iIHNyYz0iaHR0cHM6Ly93d3cudmVjdG9ybG9nby56b25lL2xvZ29zL3Jzcy9yc3MtdGlsZS5zdmciIHN0eWxlPSJoZWlnaHQ6MWVtO3ZlcnRpY2FsLWFsaWduOm1pZGRsZTsiLz4mI3hhMDs8eHNsOnZhbHVlLW9mIHNlbGVjdD0iL3Jzcy9jaGFubmVsL3RpdGxlIi8+PC9oMT48cD48eHNsOnZhbHVlLW9mIHNlbGVjdD0iL3Jzcy9jaGFubmVsL2Rlc2NyaXB0aW9uIi8+PC9wPjxwPlRoaXMgaXMgdGhlIFJTUzxhIGhyZWY9Imh0dHBzOi8vd3d3LmZlZWQuc3R5bGUvd2hhdC1pcy1hLWZlZWQuaHRtbCI+bmV3cyBmZWVkPC9hPiBmb3IgdGhlJiN4YTA7PGE+PHhzbDphdHRyaWJ1dGUgbmFtZT0iaHJlZiI+PHhzbDp2YWx1ZS1vZiBzZWxlY3Q9Ii9yc3MvY2hhbm5lbC9saW5rIi8+PC94c2w6YXR0cmlidXRlPjx4c2w6dmFsdWUtb2Ygc2VsZWN0PSIvcnNzL2NoYW5uZWwvdGl0bGUiLz48L2E+JiN4YTA7CndlYnNpdGUuPC9wPjxwPkl0IGlzIG1lYW50IGZvcjxhIGhyZWY9Imh0dHBzOi8vd3d3LmZlZWQuc3R5bGUvbmV3c3JlYWRlcnMuaHRtbCI+bmV3cyByZWFkZXJzPC9hPiwgbm90IGh1bWFucy4gUGxlYXNlIGNvcHktYW5kLXBhc3RlIHRoZSBVUkwgaW50byB5b3VyIG5ld3MgcmVhZGVyITwvcD48cD48cHJlPjxjb2RlIGlkPSJmZWVkdXJsIj48eHNsOnZhbHVlLW9mIHNlbGVjdD0iL3Jzcy9jaGFubmVsL2F0b206bGluay9AaHJlZiIvPjwvY29kZT48L3ByZT48YnV0dG9uIGNsYXNzPSJjbGlwYm9hcmQiIGRhdGEtY2xpcGJvYXJkLXRhcmdldD0iI2ZlZWR1cmwiPgpDb3B5IHRvIGNsaXBib2FyZDwvYnV0dG9uPjwvcD48eHNsOmZvci1lYWNoIHNlbGVjdD0iL3Jzcy9jaGFubmVsL2l0ZW0iPjxkZXRhaWxzPjxzdW1tYXJ5PjxhPjx4c2w6YXR0cmlidXRlIG5hbWU9ImhyZWYiPjx4c2w6dmFsdWUtb2Ygc2VsZWN0PSJsaW5rIi8+PC94c2w6YXR0cmlidXRlPjx4c2w6dmFsdWUtb2Ygc2VsZWN0PSJ0aXRsZSIvPjwvYT4mI3hhMDstJiN4YTA7PHhzbDp2YWx1ZS1vZiBzZWxlY3Q9InB1YkRhdGUiLz48L3N1bW1hcnk+PHhzbDp2YWx1ZS1vZiBzZWxlY3Q9ImRlc2NyaXB0aW9uIiBkaXNhYmxlLW91dHB1dC1lc2NhcGluZz0ieWVzIi8+PC9kZXRhaWxzPjwveHNsOmZvci1lYWNoPjxwPjx4c2w6dmFsdWUtb2Ygc2VsZWN0PSJjb3VudCgvcnNzL2NoYW5uZWwvaXRlbSkiLz4gbmV3cyBpdGVtcy48L3A+PHA+PHNtYWxsPlBvd2VyZWQgYnk8YSBocmVmPSJodHRwczovL3d3dy5mZWVkLnN0eWxlLyI+PGltZyByZWZlcnJlcnBvbGljeT0ib3JpZ2luIiBzcmM9Imh0dHBzOi8vd3d3LmZlZWQuc3R5bGUvZmF2aWNvbi5zdmciIHN0eWxlPSJoZWlnaHQ6MWVtO3BhZGRpbmctcmlnaHQ6MC4yNWVtO3ZlcnRpY2FsLWFsaWduOm1pZGRsZTsiLz5GZWVkLlN0eWxlPC9hPjwvc21hbGw+PC9wPjxzY3JpcHQgc3JjPSJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvbnBtL2NsaXBib2FyZEAyLjAuMTEvZGlzdC9jbGlwYm9hcmQubWluLmpzIi8+PHNjcmlwdD4KbmV3IENsaXBib2FyZEpTKCcuY2xpcGJvYXJkJyk7PC9zY3JpcHQ+PC9ib2R5PjwvaHRtbD48L3hzbDp0ZW1wbGF0ZT48L3hzbDpzdHlsZXNoZWV0Pg==" ?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:wfw="http://wellformedweb.org/CommentAPI/">
    <channel>
        <title>Maurice Renck - Alle Beiträge</title>
        <link>https://maurice-renck.de/de</link>
        <description>Dieser Feed enthält alle Beiträge aus allen Bereichen der Webseite von Maurice.</description>
        <language>de</language>
        <lastBuildDate>Thu, 22 Feb 2024 11:49:00 +0100</lastBuildDate>
                    <image>
                <url>https://maurice-renck.de/media/pages/about/a31163a045-1687256752/pxl_20230620_101557907-4-144x.jpg</url>
                <title>Maurice Renck - Alle Beiträge</title>
                <link>https://maurice-renck.de/de</link>
                <width>144</width>
                <height>144</height>
            </image>
                <atom:link href="https://maurice-renck.de/de/feed.rss" rel="self" type="application/rss+xml" />

                                                        <item>
                <title>Nazis bei Substack</title>
                <link>https://maurice-renck.de/de/blog/2024/nazis-bei-substack</link>
                <guid>https://maurice-renck.de/de/blog/2024/nazis-bei-substack</guid>
                <pubDate>Thu, 22 Feb 2024 11:49:00 +0100</pubDate>
                <dc:creator>Maurice Renck</dc:creator>

                                    <category>Newsletterarchiv</category>
                
                <description>
                    <![CDATA[
<p>Hallo</p>
<p>und willkommen zur neuen Ausgabe meines <a href="https://maurice-renck.de/newsletter">Newsletters</a>, den ich künftig wieder regelmäßig, nämlich alle zwei Wochen, verschicken werde. Ich nutze einen neuen Dienstleister und werde in den kommenden Wochen sicherlich noch an den Stellschrauben drehen müssen. Ich freue mich, wenn du weiterhin dabei bleibst.</p>
<p>Diese Woche werfe ich einen Blick auf Substack und deren Umgang mit Nazis auf der Plattform und wie man, meiner Meinung nach, in einem offenen Web damit umgehen sollte.</p>
<!--cut-->
<p class="spacer-dots" aria-hidden="true">. . .</p>
<p>Ein offenes Web schließt alle mit ein, warum regen sich dann gerade alle darüber auf, dass auch Nazis Dienste und Plattformen nutzen, um Geld zu verdienen und Reichweite zu bekommen?</p>
<p>Vor wenigen Wochen schwappte eine Welle der Empörung durch das Netz, als herauskam, dass Substack verwendet wird, um antisemitische Inhalte zu verbreiten. Nazis im Web sind nichts Neues, warum also die Aufregung, schließlich darf in einem offenen Web doch jeder sagen, was er will?</p>
<p>Der Reihe nach.</p>
<p>Substack, was ist das eigentlich? Substack ist eine Newsletter-Plattform, die sich in den vergangenen Jahren zunehmender Beliebtheit erfreut. </p>
<p>Denn Substack hat sich vom reinen Dienstleister zum Verschicken von Newslettern weiterentwickelt und einen großen Fokus darauf gelegt, Newsletter besser auffindbar zu machen, Inhalte zu empfehlen und – ähnlich wie medium.com – eine Infrastruktur geschaffen, die es ermöglicht mit dem eigenen Newsletter Geld zu verdienen.</p>
<p>Wer einen Substack-Newsletter betreibt, kann die Empfängerinnen zu Abonnentinnen machen. Nur wer Geld bezahlt, bekommt dann auch die E-Mails zugeschickt. Bald machten die ersten Erzählungen die Runde, wie toll man doch mit Substack Geld verdienen kann. Das lockt natürlich viele Interessenten.</p>
<p>Nun konnten aber nicht nur nette, zuvorkommende Autorinnen mit Substack Geld verdienen, sondern auch Nazis. Das fand <a href="https://www.theatlantic.com/ideas/archive/2023/11/substack-extremism-nazi-white-supremacy-newsletters/676156/">The Atlantic</a> heraus. Antisemitische Inhalte wurden nicht nur verbreitet, sondern hätten im schlimmsten Fall sogar weiterempfohlen oder gar finanziell unterstützt werden können.<br />
Substack hat nicht eingegriffen und will <a href="https://substack.com/@hamish/note/c-45811343">nach eigener Aussage</a> möglichst viel Redefreiheit und möglichst wenig Zensur bieten, im Gegenteil, man halte das für einen Fehler, der alles nur noch schlimmer mache:</p>
<p class="spacer-dots" aria-hidden="true">. . .</p>
<blockquote>
<p>I just want to make it clear that we don’t like Nazis either—we wish no-one held those views. But some people do hold those and other extreme views. Given that, we don’t think that censorship (including through demonetizing publications) makes the problem go away—in fact, it makes it worse.</p>
</blockquote>
<p class="spacer-dots" aria-hidden="true">. . .</p>
<p>Die Reaktionen auf diese Aussagen vielen nicht besonders positiv aus. Die Argumente sind auch nicht neu, wir kennen sie zum Beispiel auch von Ex-Twitter und sogar von Facebook, die aber inzwischen ihre Richtlinie angepasst haben. </p>
<p>Aber wir sind doch im offenen Web, in dem jeder sagen kann, was er will. Das ist natürlich richtig und wichtig. Redefreiheit ist ein hohes Gut und sollte gewahrt werden. Das schließt auch Meinungen ein, die einem nicht gefallen oder die unangenehm sind.</p>
<p>Dass das ebenfalls Antisemitismus und Nazipropaganda einschließt, ist jedoch vollkommener Quatsch.</p>
<p>Wie alles, hat auch die Redefreiheit ihre Grenzen. Und eine dieser Grenzen, eine Grenze, die unüberwindbar sein sollte, eine Grenze, die nicht debattierbar ist, ist die Menschenwürde. Im Grundgesetzt klingt das so:</p>
<p class="spacer-dots" aria-hidden="true">. . .</p>
<blockquote>
<p>(1) Die Würde des Menschen ist unantastbar. Sie zu achten und zu schützen ist Verpflichtung aller staatlichen Gewalt.</p>
<p>(2) Das Deutsche Volk bekennt sich darum zu unverletzlichen und unveräußerlichen Menschenrechten als Grundlage jeder menschlichen Gemeinschaft, des Friedens und der Gerechtigkeit in der Welt.</p>
</blockquote>
<p><a href="https://www.bpb.de/themen/politisches-system/politik-einfach-fuer-alle/236747/grundgesetz-fuer-die-bundesrepublik-deutschland-artikel-1-19/#node-content-title-0">Grundgesetz für die Bundesrepublik Deutschland</a></p>
<p class="spacer-dots" aria-hidden="true">. . .</p>
<p>Jegliche Form von Antisemitismus ist damit zu bekämpfen.</p>
<p>Und auch Artikel 2 zementiert dies noch einmal:</p>
<p class="spacer-dots" aria-hidden="true">. . .</p>
<blockquote>
<p>(1) Jeder hat das Recht auf die freie Entfaltung seiner Persönlichkeit, soweit er nicht die Rechte anderer verletzt und nicht gegen die verfassungsmäßige Ordnung oder das Sittengesetz verstößt.  </p>
<p>(2) Jeder hat das Recht auf Leben und körperliche Unversehrtheit. Die Freiheit der Person ist unverletzlich. In diese Rechte darf nur aufgrund eines Gesetzes eingegriffen werden.</p>
</blockquote>
<p><a href="https://www.bpb.de/themen/politisches-system/politik-einfach-fuer-alle/236747/grundgesetz-fuer-die-bundesrepublik-deutschland-artikel-1-19/#node-content-title-1">Ebda</a></p>
<p class="spacer-dots" aria-hidden="true">. . .</p>
<p>Hätten wir das also geklärt. </p>
<p>Nun gibt es wahrscheinlich auf jeder größeren Plattform auch Nazis, die sich dort breit machen. Bei manchen Plattformen fallen sie mehr auf als auf anderen.</p>
<p>Je größer eine Plattform wird, desto höher ist auch die Wahrscheinlichkeit, dass dort Nazis und antisemitische Inhalte zu finden sind. Je größer eine Plattform wird, desto schwieriger wird es auch, Inhalte zu moderieren und zu filtern. Viele Plattformen versuchen sich dieser schwierigen Verantwortung zu entziehen, indem sie sagen, dass sie nur Infrastruktur bereitstellen und mit den eigentlichen Inhalten nichts zu tun haben.</p>
<p>Wer aber anfängt, Inhalte zu empfehlen, Algorithmen zu entwickeln, die ähnliche Accounts vorschlagen, stellt ebene nicht mehr nur Infrastruktur zur Verfügung. Und muss seiner Moderationspflicht nachkommen. Wenn das passiert nicht passiert, weil man nicht "zensieren" will, ist das schlimm genug, richtig widerlich wird es dann, wenn auch nach einer Meldung von Beiträgen oder Profilen nichts passiert.</p>
<p>Über 200 Substack-Autor:innen haben sich zusammengetan und <a href="https://docs.google.com/document/d/1-IFF6pyxKkgG3CWuyNmZVE8L1EL6Khxo3QPAqrHOTaw/">einen Brief an Substack geschrieben</a>, der auf die Situation aufmerksam machen sollte.</p>
<p>Reagiert hat Substack so schlecht, wie es eben geht. Man wolle eben nicht zensieren. Erst nach massivem Druck von außen hat sich Substack überwunden und eine Handvoll Accounts gesperrt.</p>
<p>Wirklich ändern wolle man aber nichts, ein wenig genauer hinschauen vielleicht. Weiterhin wolle man für Dezentralität und Freiheit stehen. Die Redefreiheit endet <a href="https://www.theverge.com/2024/1/8/24030756/substack-nazi-newsletter-content-moderation">laut The Verge</a> bei Substack scheinbar nicht bei Nazis, sehr wohl aber untenherum, denn der dezentrale Moderationsansatz </p>
<p class="spacer-dots" aria-hidden="true">. . .</p>
<blockquote>
<p>[…] does not allow content it deems as spam, or written by sex workers, but does allow Nazis.</p>
</blockquote>
<p class="spacer-dots" aria-hidden="true">. . .</p>
<p><a href="https://www.platformer.news/why-substack-is-at-a-crossroads/">The Platformer schrieb dazu noch ganz richtig</a>: </p>
<p class="spacer-dots" aria-hidden="true">. . .</p>
<blockquote>
<p>If it won’t remove the Nazis, why should we expect the platform to remove any other harm?</p>
</blockquote>
<p class="spacer-dots" aria-hidden="true">. . .</p>
<p>Viele Autor:innen wechseln nun also den Dienst. Denn es gibt einige Alternativen zu Substack.</p>
<p>Aber natürlich sind wir auch dort nicht vor Antisemitismus gefeit. Da, wo wir Demokratinnen uns registrieren können, können sich auch Nazis registrieren. Es bleibt aber zu hoffen, dass die nächste Plattform einen besseren Umgang pflegt und konsequent durchgreift.</p>
<p>Die größte Sicherheit vor Antisemiten in unserer virtuellen Nachbarschaft ist wahrscheinlich die eigene Webseite. Damit machen wir uns maximal unabhängig, müssen aber an einigen Stellen ggf. auf etwas Komfort verzichten. Das ist es meiner Meinung nach aber wert.</p>
<p>Ganz sicher können wir jedoch auch dann nicht sein, denn schließlich könnte es auch sein, dass der gewählte Hoster ebenfalls von Nazis genutzt wird. Wir müssen also wachsam bleiben, den Finger immer wieder in die Wunde legen und für ein echtes offenes Web kämpfen. Ein Web, das auf dem wichtigsten Grundsatz fußt, dass die Menschenwürde unantastbar ist.</p>
<p>Danke, dass Du meinen RSS-Feed abonniert hast!</p>]]>
                </description>
            </item>
                                                        <item>
                <title>Art in the age of content</title>
                <link>https://maurice-renck.de/de/notes/2024/art-in-the-age-of-content</link>
                <guid>https://maurice-renck.de/de/notes/2024/art-in-the-age-of-content</guid>
                <pubDate>Wed, 21 Feb 2024 11:25:00 +0100</pubDate>
                <dc:creator>Maurice Renck</dc:creator>

                                    <category>Blog</category>
                
                <description>
                    <![CDATA[
<p><a href="https://www.youtube.com/watch?v=rCWafMqJCnU">https://www.youtube.com/watch?v=rCWafMqJCnU</a></p>
<p>Danke, dass Du meinen RSS-Feed abonniert hast!</p>]]>
                </description>
            </item>
                                                        <item>
                <title>Related Pages</title>
                <link>https://maurice-renck.de/de/hub/so-funktioniert-meine-webseite/related-pages</link>
                <guid>https://maurice-renck.de/de/hub/so-funktioniert-meine-webseite/related-pages</guid>
                <pubDate>Fri, 16 Feb 2024 09:15:00 +0100</pubDate>
                <dc:creator>Maurice Renck</dc:creator>

                                    <category>Kirby CMS</category>
                
                <description>
                    <![CDATA[<img alt="" src="https://maurice-renck.de/media/pages/hub/so-funktioniert-meine-webseite/related-pages/8bec386033-1708028782/frontend-related-1000x.png">
<p>Unter den Beitr&auml;gen auf meiner Webseite, m&ouml;chte ich weitere Artikel empfehlen, die zum Thema passen. In den ersten Versionen meiner Webseite habe ich diese Auswahl noch selbst vorgenommen. Das ist nat&uuml;rlich mit der Zeit etwas umst&auml;ndlich, denn es kommen schlie&szlig;lich st&auml;ndig neue Beitr&auml;ge hinzu und die Relevanz untereinander &auml;ndert sich laufend. Ich m&uuml;sste also immer wieder alte Artikel bearbeiten, damit die Zuordnungen wieder stimmen.</p>
<p>Nat&uuml;rlich war und bin ich nicht der Einzige, der so etwas auf seiner Webseiten hat. Weshalb es schnell entsprechende <a href="https://getkirby-plugins.com/">Kirby-Plugins</a> und diesen <a href="https://getkirby.com/docs/cookbook/content/related-articles">Cookbook-Artikel</a> gab.</p>
<p>Nach einiger Zeit stelle sich f&uuml;r mich allerdings heraus, dass die angebotenen Plugins und Beispiele nicht die Ergebnisse lieferten, die ich mich vorgestellt habe. Deshalb habe mich dazu entschlossen, mich genauer mit dem Thema auseinanderzusetzen und eine eigene L&ouml;sung zu bauen.</p>
<p>Auch abseits von "Related Posts" finde ich das Thema "Auffindbarkeit" extrem spannend. Wir alle haven vermutlich irgendwo Inhalte im Netz, von denen wir wollen, dass sie nicht sang- und klanglos in der Masse untergehen. Im gro&szlig;en Ma&szlig;stab m&ouml;chte man in Suchmaschinen gefunden werden, im kleineren Ma&szlig;stab sind es vielleicht Empfehlungen zu anderen Beitr&auml;gen auf der eigenen Webseite.</p>
<p>F&uuml;r Kirby gibt es aktuell ein Plugin, was sich dem Problem annimmt: <a href="https://getkirby.com/plugins/texnixe/similar">Similar</a>.</p>
<p>Similar hatte ich eine ganze Weile im Einsatz, an einigem Stellen, war es mir aber nicht genau genug und das habe ich auch durchs Verfeinern von Einstellungen nicht in den Griff bekommen. Ich habe mir dann angesehen, wie Sonja die Sache angeht und mir ein paar Ideen gestohlen und auf meine Seite &uuml;bertragen.</p>
<p>Um &auml;hnliche Beitr&auml;ge zu finden, werde ich auf drei Content-Felder zur&uuml;ckgreifen:</p>
<ol>
<li>Tags</li>
<li>Text</li>
<li>Titel</li>
</ol>
<h2>Tags</h2>
<p>Ich verpasse jedem Beitrag, den ich ver&ouml;ffentliche, mehr oder weniger viele Tags. Dazu verwende ich Kirbys <a href="https://getkirby.com/docs/reference/panel/fields/tags">Tag-Field</a>. Tags sind bei mir global, das hei&szlig;t, dass ich mir von allen Seiten alle Tags hole und mir diese dann als Autocomplete anzeigen lasse.</p>
<p>Im Panel sieht das so aus:</p>
<figure><img alt="" src="https://maurice-renck.de/media/pages/hub/so-funktioniert-meine-webseite/related-pages/4beef0d018-1708028782/panel-tags-800x.webp" title="" width="800"></figure>
<p>Das passende Blueprint dazu wie folgt:</p>
<pre class="hljs"><code data-language="yaml"><span class="hljs-attr">label:</span> <span class="hljs-string">Tags</span>
<span class="hljs-attr">type:</span> <span class="hljs-string">tags</span>
<span class="hljs-attr">options:</span> <span class="hljs-string">query</span>
<span class="hljs-attr">query:</span> <span class="hljs-string">site.index.pluck("tags",</span> <span class="hljs-string">","</span><span class="hljs-string">,</span> <span class="hljs-literal">true</span><span class="hljs-string">)</span>
<span class="hljs-attr">translate:</span> <span class="hljs-literal">false</span></code></pre>
<p>Via <code>query</code> hole ich mir den Inhalt aller Tag-Felder, um diese als Autocomplete zu haben. Eine &Uuml;bersetzung soll nicht m&ouml;glich sein, die Tags gelten f&uuml;r jede Sprache.</p>
<p>Diese Tags als Quelle f&uuml;r relevante Seiten zu verwenden, ist einfach. Zun&auml;chst hole ich mir die Tags der aktuellen Seite. Dann schaue ich in allen anderen ver&ouml;ffentlichten Seiten nach gleichen Tags:</p>
<pre class="hljs"><code data-language="php">$tags = <span class="hljs-keyword">$this</span>-&gt;tags()-&gt;split(<span class="hljs-string">','</span>);
$pagesWithTags = site()-&gt;index()-&gt;published()-&gt;filterBy(<span class="hljs-string">'tags'</span>, <span class="hljs-string">'in'</span>, $tags, <span class="hljs-string">','</span>)-&gt;not(<span class="hljs-keyword">$this</span>);</code></pre>
<p>Da ich die Suche nach Related Pages in einem <a href="https://getkirby.com/docs/reference/plugins/extensions/page-methods">Page-Method-Plugin</a> aufrufe, verweist in all diesen Beispielen <code>$this</code> auf die aktuelle Seite. Nachdem ich alle Tags der aktuellen Seite habe, filtere ich alle anderen Seiten nach diesen Tags. Als Resultat bekomme ich eine Liste an Seiten, die alle mindestens einen Tag mit meiner aktuellen Seite gemeinsam haben.</p>
<p>Da die aktuelle Seite auf jeden Fall immer im Resultat vorkommen w&uuml;rde, schlie&szlig;e ich sie noch aus, ich will nat&uuml;rlich nicht auf dieselbe Seite verweisen.</p>
<p>Jetzt k&ouml;nnte ich schon alle Seiten mit denselben Tags auflisten, das ist mir aber nicht genau genug. Viel mehr m&ouml;chte ich noch in der Lage sein, den drei oben genannten Feldern eine Gewichtung zu geben. Das habe ich von Sonja geklaut.</p>
<p>Dazu gehe ich alle gefunden Seite noch einmal durch, hole mir die einzelnen Tags jeder Seite und lasse mir nur die Tags zur&uuml;ckgeben, die auch in der aktuellen Seite hinterlegt sind.</p>
<p>Hat meine aktuelle Seite die Tags <code>kirby</code>, <code>cms</code> und <code>plugin</code>, die &auml;hnliche Seite <code>kirby</code>, <code>cms</code> und <code>theme</code> bekomme ich auf diese Weise die beiden identischen Tags <code>kirby</code> und <code>cms</code> als Ergebnis zur&uuml;ck.</p>
<p>Um mir alle &auml;hnlichen Seiten zu merken, f&uuml;lle ich ein Array. Ich will an dieser Stelle nicht die gesamte Seite speichern, sondern lediglich ihre ID, &uuml;ber die ich dann sp&auml;ter wieder auf sie zugreifen kann.</p>
<p>Au&szlig;erdem m&ouml;chte ich wissen, wie relevant jede einzelne Seite wirklich ist, dazu z&auml;hle ich die identischen Tags und errechne daraus eine Gewichtung. Wie stark jedes Feld gewichtet werden soll, kann ich sp&auml;ter in meiner <code>config.php</code> &auml;ndern:</p>
<pre class="hljs"><code data-language="php"><span class="hljs-keyword">foreach</span>($pagesWithTags <span class="hljs-keyword">as</span> $page) {
    $pageTags = $page-&gt;tags()-&gt;split(<span class="hljs-string">','</span>);
    $similarTags = array_intersect($pageTags, $tags);
    $uuid = $page-&gt;uuid()-&gt;toString();

    $similarPages[] = [
        <span class="hljs-string">'page'</span> =&gt; $uuid,
        <span class="hljs-string">'weight'</span> =&gt; count($similarTags) * option(<span class="hljs-string">'mauricerenck.similar-related.tags.weight'</span>, <span class="hljs-number">1</span>),
    ];
}</code></pre>
<p>Als Ergebnis habe ich nun eine Liste von Seiten und ihre jeweilige Gewichtung.</p>
<h2>Titel</h2>
<p>Im n&auml;chsten Schritt schnappe ich mir den Titel, um auch hier &Auml;hnlichkeiten zu erkennen. Das ist nicht ganz so naheliegend, wie es bei den Tags war, aber auch recht unkompliziert umzusetzen.</p>
<p>Um &Auml;hnlichkeiten zu finden, vergleiche ich alle Titel Wort f&uuml;r Wort. Dazu hole ich mir den Titel der aktuellen Seite und teile ihn in seine Bestandteile. Dann geht es wieder an das Filtern aller ver&ouml;ffentlichten Seiten. Dieses Mal mit einem eigenen Filter.</p>
<p>F&uuml;r jede dieser Seiten hole ich mir den Titel und teile ihn in einzelne Worte. Ich habe nun ein Array mit Worten von beiden Titeln und kann diese vergleichen. Wie bei den Tags schaue ich jetzt, ob es mindestens eine &Uuml;bereinstimmung gibt. Gibt es die, bekomme ich die Seite als Ergebnis zur&uuml;ck. Und nat&uuml;rlich m&ouml;chte ich auch hier die aktuelle Seite ausschlie&szlig;en:</p>
<pre class="hljs"><code data-language="php">$wordsFromTitle = <span class="hljs-keyword">$this</span>-&gt;title()-&gt;split(<span class="hljs-string">' '</span>);
$pagesWithTitle = site()-&gt;index()-&gt;published()-&gt;filter(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">($child)</span> <span class="hljs-title">use</span><span class="hljs-params">($wordsFromTitle)</span> </span>{
    $wordsFromChildTitle = $child-&gt;title()-&gt;split(<span class="hljs-string">' '</span>);
    <span class="hljs-keyword">return</span> count(array_intersect($wordsFromTitle, $wordsFromChildTitle)) &gt; <span class="hljs-number">0</span>;
})-&gt;not(<span class="hljs-keyword">$this</span>);</code></pre>
<p>Im n&auml;chsten Schritt geht es wieder daran, das Ergebnis-Array zu f&uuml;llen. Wieder gehe ich durch alle gefundenen Seiten und gewichte sie. Das funktioniert genauso wie bei den Tags:</p>
<pre class="hljs"><code data-language="php"><span class="hljs-keyword">foreach</span>($pagesWithTitle <span class="hljs-keyword">as</span> $page) {
    $wordsFromPageTitle = $page-&gt;title()-&gt;split(<span class="hljs-string">' '</span>);
    $similarWords = array_intersect($wordsFromTitle, $wordsFromPageTitle);

    $uuid = $page-&gt;uuid()-&gt;toString();

    $similarPages[] = [
        <span class="hljs-string">'page'</span> =&gt; $uuid,
        <span class="hljs-string">'weight'</span> =&gt; count($similarWords) * option(<span class="hljs-string">'mauricerenck.similar-related.title.weight'</span>, <span class="hljs-number">0.5</span>),
    ];
}</code></pre>
<h2>Text</h2>
<p>Schlie&szlig;lich kommen wir zum aufw&auml;ndigsten Feldtypen. Ich m&ouml;chte so weit gehen, dass ich sogar jeden einzelnen Text Wort f&uuml;r Wort miteinander vergleiche. Hier wird es etwas knifflig, denn ich muss auch hier den gesamten Text in seine einzelnen Worte aufteilen. Das kann bei einem langen Text nat&uuml;rlich eine ziemlich lange Wortliste werden und tr&auml;gt sicherlich nicht dazu bei, dass die Seite schneller l&auml;dt. Dazu aber sp&auml;ter mehr.</p>
<p>Ich hole mir auch hier das Feld als Quelle und teile es in einzelne Worte. In diesem Fall m&ouml;chte ich nur Worte haben, die l&auml;nger als ein Zeichen sind. Das schlie&szlig;t im Englischen schon mal ein paar F&uuml;llw&ouml;rter wie <em>I</em> oder <em>a</em> aus:</p>
<pre class="hljs"><code data-language="php">$wordsFromText = <span class="hljs-keyword">$this</span>-&gt;text()-&gt;split(<span class="hljs-string">' '</span>);
$wordsFromText = array_filter($wordsFromText, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">($word)</span> </span>{
    <span class="hljs-keyword">return</span> strlen($word) &gt; <span class="hljs-number">1</span>;
});</code></pre>
<p>Nun geht es ans Aufr&auml;umen, ich m&ouml;chte nur "echte" W&ouml;rter haben:</p>
<pre class="hljs"><code data-language="php">$wordsFromText = array_map(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">($word)</span> </span>{
    <span class="hljs-keyword">return</span> preg_replace(<span class="hljs-string">'/[^A-Za-z0-9\-]/'</span>, <span class="hljs-string">''</span>, $word);
}, $wordsFromText);</code></pre>
<p>Jetzt wird es interessant. Um ein m&ouml;glichst genaues Ergebnis zu bekommen, werde ich bestimmte Worte ausschlie&szlig;en. Das sind Worte, die im eigentlichen Sinne nichts mit dem Inhalt zu tun haben. Schwer zu beschreiben. Hier ein Beispiel:</p>
<p>Meine Seite besteht aus diesem Text: </p>
<pre class="hljs"><code data-language="plaintext">Ich m&ouml;chte eine Webseite mit dem CMS Kirby erstellen, dazu schreibe ich mir ein Blueprint und ein Template</code></pre>
<p>F&uuml;r meinen Vergleich interessieren mich bestimmte Worte gar nicht, etwa <em>Ich</em>, <em>eine</em>, <em>mit</em>, <em>dem</em>, <em>ein</em>, &hellip; Diese Worte kommen in so gut wie jedem Text vor und w&uuml;rden das Ergebnis verw&auml;ssern. Wirklich interessant sind hier nur Worte wie <em>Webseite</em>, <em>CMS</em>, <em>Kirby</em>, <em>Blueprint</em> und <em>Template</em>.</p>
<p>W&uuml;rde ich ausschlie&szlig;lich auf Deutsch schreiben, k&ouml;nnte ich mir das Leben recht einfach machen und einfach alle gro&szlig;geschriebenen Worte holen. Im Englischen funktioniert das allerdings nicht.</p>
<p>Wie also vorgehen? Ich habe eine sehr lange Liste von sogenannten <em>Stopwords</em>. Das sind solche F&uuml;llw&ouml;rter, wie oben beschrieben. Zum Gl&uuml;ck bin ich nicht der Einzige, der vor so einem Problem steht und es gibt ein paar gut gepflegte Listen da drau&szlig;en im Netz. Ich habe mich f&uuml;r die <a href="https://github.com/stopwords-iso/stopwords-iso?tab=readme-ov-file">ISO-Stopwords</a> entschieden, die gleich in mehreren Sprachen daher kommen.</p>
<p>Die Daten liegen mir als JSON-Datei vor. Ich muss die Datei laden. Vorher hole ich mir die Sprache der aktuellen Seite. In meinem Fall ist das entweder Deutsch oder Englisch:</p>
<pre class="hljs"><code data-language="php">$pageLanguage = kirby()-&gt;language()-&gt;code() ?? <span class="hljs-string">'en'</span>;
$stopWordsForLanguage = [];

$languagesJson = file_get_contents(<span class="hljs-keyword">__DIR__</span> . <span class="hljs-string">'/stopwords-iso.json'</span>);

<span class="hljs-keyword">if</span>($languagesJson !== <span class="hljs-keyword">false</span>) {
    $stopWords = json_decode($languagesJson);
    $stopWordsForLanguage = (<span class="hljs-keyword">isset</span>($stopWords-&gt;$pageLanguage)) ? $stopWords-&gt;$pageLanguage : $stopWords-&gt;en;
}</code></pre>
<p>Sicherheitshalber pr&uuml;fe ich noch, ob die Datei geladen werden konnte. Wenn nicht, gibt es einfach eine leere Liste. Ansonsten schaue ich nach, ob die aktuelle Sprache in den Daten vorhanden ist. Sollte das nicht der Fall sein, greift mein Fallback und ich nutze Englisch.</p>
<p>Nun geht es wieder ans Filtern. Ich entferne alle Stopwords aus der Wortliste der aktuellen Seite:</p>
<pre class="hljs"><code data-language="php">$wordsFromText = array_filter($wordsFromText, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">($word)</span> <span class="hljs-title">use</span><span class="hljs-params">($stopWordsForLanguage)</span> </span>{
    <span class="hljs-keyword">return</span> !in_array(strtolower($word), $stopWordsForLanguage);
});</code></pre>
<p>Und jetzt gehe ich den &uuml;blichen Weg mit einem eigenen Filter &uuml;ber alle Seiten. Dieses Mal vergleiche die Worte des Textes. Da ich in meinem Quelldaten jetzt schon keine Stopwords mehr habe, muss ich sie nicht auch noch bei jeder einzelnen Seite herausfiltern:</p>
<pre class="hljs"><code data-language="php">$pagesWithText = site()-&gt;index()-&gt;published()-&gt;filter(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">($child)</span> <span class="hljs-title">use</span><span class="hljs-params">($wordsFromText)</span> </span>{

    <span class="hljs-keyword">if</span>($child-&gt;text()-&gt;isEmpty()) <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    $wordsFromChildText = $child-&gt;text()-&gt;split(<span class="hljs-string">' '</span>);

    <span class="hljs-keyword">return</span> count(array_intersect($wordsFromText, $wordsFromChildText)) &gt; <span class="hljs-number">0</span>;

})-&gt;not(<span class="hljs-keyword">$this</span>);</code></pre>
<p>Ich pr&uuml;fe ich noch, ob die Seite auch wirklich einen Text hat. Es k&ouml;nnte Seiten geben, die nur ein Listing sind oder Bl&ouml;cke oder Layouts nutzen. Die will ich nicht in meinem Ergebnis haben und schlie&szlig;e sie direkt aus. Den Rest pr&uuml;fe ich wieder auf mindestens eine &Uuml;bereinstimmung und nehme sie entsprechend in die Liste auf.</p>
<p>Jetzt geht es wieder in die Schleife durch alle Resultate und ans F&uuml;llen des Ergebnis-Arrays:</p>
<pre class="hljs"><code data-language="php"><span class="hljs-keyword">foreach</span>($pagesWithText <span class="hljs-keyword">as</span> $page) {

    $wordsFromPageText = $child-&gt;text()-&gt;split(<span class="hljs-string">' '</span>);
    $similarWords = array_intersect($wordsFromText, $wordsFromPageText);
    $uuid = $page-&gt;uuid()-&gt;toString();

    $similarPages[] = [
        <span class="hljs-string">'page'</span> =&gt; $uuid,
        <span class="hljs-string">'weight'</span> =&gt; count($similarWords) * option(<span class="hljs-string">'mauricerenck.similar-related.text.weight'</span>, <span class="hljs-number">0.95</span>),
    ];

}</code></pre>
<p>Nun habe ich im besten Fall eine sehr lange Liste an &auml;hnlichen Seiten. Manche dieser Seiten haben dieselben <em>Tags</em>, manche einen &auml;hnlichen <em>Titel</em> oder <em>Text</em>. Es kann sein, dass Seiten mehrfach vorkommen, das ist sogar sehr wahrscheinlich, denn wenn die Tags sich schon gleichen, dann werden sich vermutlich auch Textfragmente gleichen. Hat eine Seite den Tag <code>kirby</code>, ist die Wahrscheinlichkeit ziemlich hoch, dass auch im Flie&szlig;text das Wort <code>Kirby</code> vorkommt und die Seite demnach zweimal in der Liste zu finden ist.</p>
<p>Es gibt zwei M&ouml;glichkeiten, damit umzugehen:</p>
<p>Ich k&ouml;nnte von Anfang an, doppelte Seiten ausschlie&szlig;en. Habe ich schon eine Liste an Seiten, die &auml;hnliche Tags haben, k&ouml;nnte ich diese beim Abfragen &auml;hnlicher Titel bereits ausschlie&szlig;en. Beim Text k&ouml;nnte ich dann bereits Seiten ausschlie&szlig;en, die sowohl &auml;hnliche Tags als auch Titel haben.</p>
<p>Ich m&ouml;chte hier aber etwas schlauer damit umgehen. Ich gehe davon aus, dass eine Seite, die &auml;hnliche Tags <strong>und</strong> einen &auml;hnlichen Titel <strong>und</strong> einen &auml;hnlichen Text hat, viel relevanter ist, als eine Seite, die sich nur einen Tag teilt oder in der sich ein paar gleiche Worte wiederfinden.</p>
<p>Deshalb ist der n&auml;chste Schritt das Zusammenf&uuml;hren der Daten und die Gewichtung der jeweiligen Seite unter R&uuml;cksichtnahme verschiedener Vorkommen:</p>
<pre class="hljs"><code data-language="php">$result = [];
<span class="hljs-keyword">foreach</span>($similarPages <span class="hljs-keyword">as</span> $page) {
    $uuid = $page[<span class="hljs-string">'page'</span>];

    <span class="hljs-keyword">if</span>(!<span class="hljs-keyword">isset</span>($result[$uuid])) {
        $result[$uuid] = [
            <span class="hljs-string">'page'</span> =&gt; $uuid,
            <span class="hljs-string">'weight'</span> =&gt; $page[<span class="hljs-string">'weight'</span>],
        ];
    } <span class="hljs-keyword">else</span> {
        $result[$uuid][<span class="hljs-string">'weight'</span>] += $page[<span class="hljs-string">'weight'</span>];
    }
}</code></pre>
<p>Zuerst erstelle ich mir ein leeres Array f&uuml;r mein Ergebnis, dann gehe ich durch alle &auml;hnlichen Seiten und hole mir ihre UUID. Taucht die Seite bisher nicht in meiner Ergebnisliste auf, f&uuml;ge ich sie hinzu. Als Array-Schl&uuml;ssel nutze ich ihre UUID, wieder merke ich mir die UUID und die Gewichtung.</p>
<p>Taucht die Seite bereits in der Liste auf, f&uuml;ge ich sie nicht erneut hinzu, sondern addiere die Gewichtung. Steht eine Seite also dreimal in der Liste, n&auml;mlich mit Tags, Titel und Text, so werden alle drei Gewichtungen addiert.</p>
<p>Schlie&szlig;lich habe ich eine Liste aller Seiten, ohne Duplikate, mit der Summe Ihrer jeweiligen Gewichtungen. Jetzt m&ouml;chte ich sie noch nach Gewicht sortieren:</p>
<pre class="hljs"><code data-language="php">usort($result, fn($a, $b) =&gt; $a[<span class="hljs-string">'weight'</span>] &lt;=&gt; $b[<span class="hljs-string">'weight'</span>]);</code></pre>
<p>Da ich in meinem Template wenig mit einem Array von UUIDs anfangen kann, wird mein Ergebnis im letzten Schritt noch in eine Page Collection umgewandelt:</p>
<pre class="hljs"><code data-language="php">$pages = array_map(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">($page)</span> </span>{
    <span class="hljs-keyword">return</span> page($page[<span class="hljs-string">'page'</span>]);
}, array_reverse($result)) ?? [];</code></pre>
<p>Als Ergebnis habe ich nun eine Collection aller passenden Seite, mit der ich nun im Template arbeiten kann. Ich kann mir also von jeder Seite den Titel mit <code>$pageFromCollection-&gt;title();</code> ausgeben lassen, oder meine Collection noch einmal filtern.</p>
<p>Ich muss meine Collection noch ans Template zur&uuml;ckgeben: </p>
<pre class="hljs"><code data-language="php"><span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Collection($pages);</code></pre>
<p>Das ganze Konstrukt habe ich in ein Plugin verpackt und als pageMethod zur Verf&uuml;gung gestellt:</p>
<pre class="hljs"><code data-language="php">kirby::plugin(<span class="hljs-string">'mauricerenck/related-pages'</span>, [
    <span class="hljs-string">'pageMethods'</span> =&gt; [
    <span class="hljs-string">'relatedPages'</span> =&gt; <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">()</span> </span>{
        <span class="hljs-comment">// CODE</span>
    }
];</code></pre>
<p>In meinem Template kann ich indessen einfach diese Methode aufrufen, bekomme eine Collection von Seiten zur&uuml;ck und kann damit etwas anstellen. Ich limitiere sie noch auf drei Eintr&auml;ge und zeige diese dann in einer Schleife an:</p>
<pre class="hljs"><code data-language="php">$related = $page-&gt;relatedPages()-&gt;limit(<span class="hljs-number">3</span>);

<span class="hljs-comment">// Render related pages</span></code></pre>
<h2>Ein Wort der Warnung</h2>
<p>Ich bin mit dem Ergebnis sehr zufrieden. Die Related-Pages, die auf den Seiten angezeigt werden, passen meist ziemlich gut. Ich &uuml;berlege noch, ob den ich den zeitlichen Aspekt einbringe, also neuere Seiten h&ouml;her gewichte, als &auml;ltere Seiten.</p>
<p>Was jedoch gesagt werden muss: Bei einer Webseite mit ziemlich vielen Seiten und/oder langen Texten k&ouml;nnte der Ansatz zu Problemen f&uuml;hren. Da passiert ziemlich viel, gerade beim Textvergleich und das kann unter Umst&auml;nden lange dauern, im schlimmsten Fall zu Timeouts oder Speicher&uuml;berl&auml;ufen f&uuml;hren.</p>
<p>Vermutlich w&auml;re es daher schlauer, den ganzen Prozess nicht bei jedem Seitenaufruf durchzuf&uuml;hren, sondern via Cronjob oder Hook. Das Ergebnis k&ouml;nnte man dann in der jeweiligen Seite speichern. Beim Aufruf der Seite muss dann nichts mehr berechnet werden.</p>
<p>F&uuml;r mich w&auml;re das der n&auml;chste Schritt meines kleinen Plugins. Derzeit l&auml;uft es theoretisch noch bei jedem Seitenaufruf. Das sehe ich bei mir als nicht so kritisch an, weil ich alle Seiten cache. Der Cache wird nur geleert, wenn ich den Code der Seite aktualisiere, oder der Inhalt sich &auml;ndert. Dann wird beim Seitenaufruf das obige Prozedere durchlaufen, danach aber nur noch statisches HTML ausgeliefert. Geschwindigkeitseinbu&szlig;en kann ich auf meiner Seite bisher nicht feststellen.</p>
<p>Ich &uuml;berlege, meinen Code noch etwas zu sortieren, vielleicht obige Anmerkungen noch einzubauen und dann zu ver&ouml;ffentlichen. Allerdings nur, wenn Interesse besteht &ndash; das Plugin von Sonja funktioniert ja hervorragend.</p>
<p>Lass mich doch wissen, ob du das Plugin nutzen w&uuml;rdest!</p>
<p>Danke, dass Du meinen RSS-Feed abonniert hast!</p>]]>
                </description>
            </item>
                                                        <item>
                <title>Feedle</title>
                <link>https://maurice-renck.de/de/notes/2024/feedle</link>
                <guid>https://maurice-renck.de/de/notes/2024/feedle</guid>
                <pubDate>Mon, 12 Feb 2024 15:26:00 +0100</pubDate>
                <dc:creator>Maurice Renck</dc:creator>

                                    <category>Blog</category>
                
                <description>
                    <![CDATA[
<p>Matthias hat in seiner aktuellen Ausgabe von <a href="https://buttondown.email/ownyourweb/archive/issue-09/">Own Your Web</a> viele schöne RSS-Tools aufgelistet, ich finde Feedle noch ein gute Sache und ergänze das hiermit mal.</p>
<p>Danke, dass Du meinen RSS-Feed abonniert hast!</p>]]>
                </description>
            </item>
                                                        <item>
                <title>Olli Schulz</title>
                <link>https://maurice-renck.de/de/notes/2024/olli-schulz</link>
                <guid>https://maurice-renck.de/de/notes/2024/olli-schulz</guid>
                <pubDate>Fri, 09 Feb 2024 11:12:00 +0100</pubDate>
                <dc:creator>Maurice Renck</dc:creator>

                                    <category>Blog</category>
                
                <description>
                    <![CDATA[<img alt="" src="https://maurice-renck.de/media/pages/notes/2024/olli-schulz/04f33b885c-1707473526/pxl_20240209_100359448-1000x.jpg">

<p>Danke, dass Du meinen RSS-Feed abonniert hast!</p>]]>
                </description>
            </item>
                                                        <item>
                <title>Kirby Podcaster transcripts</title>
                <link>https://maurice-renck.de/de/blog/2024/kirby-podcaster-transcripts</link>
                <guid>https://maurice-renck.de/de/blog/2024/kirby-podcaster-transcripts</guid>
                <pubDate>Thu, 01 Feb 2024 09:30:00 +0100</pubDate>
                <dc:creator>Maurice Renck</dc:creator>

                                    <category>Kirby CMS</category>
                
                <description>
                    <![CDATA[<img alt="" src="https://maurice-renck.de/media/pages/blog/2024/kirby-podcaster-transcripts/4eb0a22d1f-1706721402/podcaster-transcripts-1000x.png">
<p>Deine Podcasts sind jetzt ein wenig inklusiver. Apple führt automatische Transkripte ein, und das Podcaster-Plugin enthält jetzt auch eine neue Transkriptfunktion. Diese ermöglicht es dir, Transkriptdateien im VTT- oder SRT-Format hochzuladen und jedem Transkript eine Sprache zuzuweisen.</p>
<p>Auf diese Weise kannst du Apple (und anderen) Transkripte bereitstellen, ohne auf deren automatische Generierung angewiesen zu sein. Öffne einfach den Episoden-Tab, und du findest ein neues Feld unter dem Audio-Upload. Sobald du ein Transkript hochlädst, werden sie zu deinem RSS-Feed hinzugefügt und stehen allen Playern zur Verfügung, die diese Funktionalität unterstützen.</p>
<p>Das neueste Update ist über Composer oder die <a href="https://podcaster-plugin.com/">Podcaster-Website</a> verfügbar.</p>
<p>Viel Spaß beim Podcasten!</p>
<p>Danke, dass Du meinen RSS-Feed abonniert hast!</p>]]>
                </description>
            </item>
                                                        <item>
                <title>File over App</title>
                <link>https://maurice-renck.de/de/notes/2024/file-over-app</link>
                <guid>https://maurice-renck.de/de/notes/2024/file-over-app</guid>
                <pubDate>Wed, 31 Jan 2024 16:04:00 +0100</pubDate>
                <dc:creator>Maurice Renck</dc:creator>

                                    <category>Blog</category>
                
                <description>
                    <![CDATA[
<blockquote>
<p>File over app is a philosophy: if you want to create digital artifacts that last, they must be files you can control, in formats that are easy to retrieve and read. Use tools that give you this freedom.</p>
</blockquote>
<p>Love that!</p>
<p>Danke, dass Du meinen RSS-Feed abonniert hast!</p>]]>
                </description>
            </item>
                                                        <item>
                <title>Blog&#039;n&#039;Roll</title>
                <link>https://maurice-renck.de/de/blog/2024/blog-n-roll</link>
                <guid>https://maurice-renck.de/de/blog/2024/blog-n-roll</guid>
                <pubDate>Wed, 31 Jan 2024 08:56:00 +0100</pubDate>
                <dc:creator>Maurice Renck</dc:creator>

                                    <category>Open Web</category>
                
                <description>
                    <![CDATA[<img alt="" src="https://maurice-renck.de/media/pages/blog/2024/blog-n-roll/06b483c302-1706633307/mauricerenck_an_illustration_showing_the_connection_of_hundrets_ce938414-cb59-491f-98d4-bfc924464634-1000x.png">
<p>Matthias betreibt den ganz wunderbaren Newsletter <a href="https://buttondown.email/ownyourweb">Own Your Web</a>, der sich alle zwei Wochen mit Themen rund um die eigene Webseite befasst. In der achten Ausgabe beschäftigt Matthias sich mit <a href="https://buttondown.email/ownyourweb/archive/issue-08/">Blogrolls</a>.</p>
<p>Beim Lesen des Newsletters werde ich immer ein wenig nostalgisch, weil oft Themen aufgebracht werden, die es schon so lange gibt, die aber etwas verloren gegangen sind. Blogrolls gehören sicherlich dazu.</p>
<!--cut-->
<p>Sie stammen noch aus den Anfangszeiten der Blogs und dienen der Vernetzung. Im Grunde sind Blogrolls nichts anderes als einfache Listen von Blogs, die man selbst gerne liest und anderen ans Herz legen möchte. Das hat Matthias aber <a href="https://buttondown.email/ownyourweb/archive/issue-08/">hier schon wunderbar und viel detaillierter erklärt</a>.</p>
<p>Ich habe auch eine Blogroll. Die fand man bis zum letzten Redesign ganz klassisch in der Seitenleiste meines Blogs. Inzwischen gibt es diese Seitenleiste aber nicht mehr, sondern der ganze Spaß ist runter in Richtung Footer gewandert. </p>
<p>In den <a href="https://mastodon.social/@matthiasott/111807359833097188?utm_source=mauricerenck&amp;utm_medium=blog">Replies auf Matthias Artikel</a> haben viele Blogger:innen einen direkten Link zur eigenen Blogroll gepostet, sie haben also nicht nur irgendwo in der Seitenleiste eine Liste mit Blogs (wie ich), sondern zusätzlich eine dedizierte Seite.</p>
<p>Die Idee finde ich ziemlich gut. Gerade, wenn alle Blogger:innen sich an das gleiche Muster halten und man in jedem Blog einfach <code>/blogroll</code> an die Url hängen könnte, um eben jene zu sehen.</p>
<p>Ich hatte auch mein eine eigene Seite für die Blogroll. Die lag damals als "echte" Seite im CMS und das hat mich gestört, weil sie neben den Blogposts so ein wenig aus der Reihe fiel. Ich habe sie dann eingestampft und pflege meine Blogroll jetzt einfach direkt in den Blog-Settings.</p>
<p>Inzwischen kann ich mich etwas besser mit Kirby aus und würde Dinge anders umsetzen als damals – außerdem hat Kirby inzwischen zahlreiche weitere Features verabreicht bekommen, weshalb ich das Thema Blogroll jetzt noch einmal neu angegangen bin. Ich habe nun auch <a href="/blog/blogroll">eine dedizierte Seite</a>, die meine Blogroll ausliefert, ohne dass diese Seite im CMS existiert. <a href="https://maurice-renck.de/hub/so-funktioniert-meine-webseite/blogroll">Ich erkläre drüben im Hub, wie ich das gemacht habe</a>.</p>
<p>Schaut euch auf jeden Fall Matthias Newsletter an und vielleicht baut ihr euch ja dann auch noch eine Blogroll ins eigene Blog ein!</p>
<p>Danke, dass Du meinen RSS-Feed abonniert hast!</p>]]>
                </description>
            </item>
                                                        <item>
                <title>Blogroll</title>
                <link>https://maurice-renck.de/de/hub/so-funktioniert-meine-webseite/blogroll</link>
                <guid>https://maurice-renck.de/de/hub/so-funktioniert-meine-webseite/blogroll</guid>
                <pubDate>Tue, 30 Jan 2024 20:00:00 +0100</pubDate>
                <dc:creator>Maurice Renck</dc:creator>

                                    <category>Kirby CMS</category>
                
                <description>
                    <![CDATA[<img alt="" src="https://maurice-renck.de/media/pages/hub/so-funktioniert-meine-webseite/blogroll/9e1261bd76-1706640575/panel-blogroll-1000x.png">
<p>In meinem Blog gibt es seit geraumer Zeit eine Blogroll. Was genau es damit auf sich hat, habe ich <a href="/blog/2024/blog-n-roll">hier beschrieben</a>. An dieser Stelle m&ouml;chte ich erkl&auml;ren, wie ich meine Blogroll mit Kirby umgesetzt habe.</p>
<p>Meine Blogroll ist ziemlich simpel aufgebaut: In meinem Blog-<a href="https://getkirby.com/docs/guide/blueprints/introduction">Blueprint</a> habe ich ein Structure-Feld, welches lediglich aus zwei Feldern besteht:</p>
<ol>
<li>Der URL des Blogs</li>
<li>Dem Titel des Blogs</li>
</ol>
<p>Bisher wurde unter meinem Bloglisting unter anderem die Blogroll in Icon-Form angezeigt. Das m&ouml;chte ich erweitern, sodass die Blogroll auch unter <a href="/blog/blogroll">/blog/blogroll</a> erreichbar ist und ich somit einen Deeplink habe.</p>
<p>Da ich die Blogroll nicht als eigenst&auml;ndige Seite im CMS liegen haben m&ouml;chte, werde ich eine Route f&uuml;r die Blogroll anlegen und dort eine <a href="https://getkirby.com/docs/guide/virtual-pages/simple-virtual-page">virtuelle Seite</a> ausspielen.</p>
<h2>Die Route</h2>
<p>In der Datei <code>site/config/config.php</code> lege ich eine neue Route. Ich m&ouml;chte, dass die Blogroll unter allen Sprachen unter <code>blog/blogroll</code> erreichbar ist:</p>
<pre class="hljs"><code data-language="php">[
    <span class="hljs-string">'pattern'</span> =&gt; <span class="hljs-string">'blog/blogroll'</span>,
    <span class="hljs-string">'language'</span> =&gt; <span class="hljs-string">'*'</span>,
    <span class="hljs-string">'action'</span> =&gt; <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">($language)</span> </span>{
      <span class="hljs-comment">// [&hellip;]</span>
    }
]</code></pre>
<p>Damit kann die Seite unter der entsprechenden URL ausgeliefert werden. W&uuml;rden wir diese URL jetzt aufrufen, bek&auml;men wir allerdings einen 404-Fehler an den Kopf geworfen. Eine Route ben&ouml;tigt immer entweder eine Ausgabe oder muss auf eine andere Route weiterleiten.</p>
<p>Deshalb lege ich im n&auml;chsten Schritt eine virtuelle Seite an, die wir dann zur&uuml;ckgeben k&ouml;nnen:</p>
<pre class="hljs"><code data-language="php">    $data = [
        <span class="hljs-string">'slug'</span> =&gt; <span class="hljs-string">'blogroll'</span>,
        <span class="hljs-string">'parent'</span> =&gt; page(<span class="hljs-string">'blog'</span>),
        <span class="hljs-string">'template'</span> =&gt; <span class="hljs-string">'listing-blogroll'</span>,
        <span class="hljs-string">'translations'</span> =&gt; []
    ];

    $page = Page::factory($data);
    site()-&gt;visit($page, $language);

    <span class="hljs-keyword">return</span> $page;</code></pre>
<p>Zun&auml;chst lege ich den Slug fest, also <code>blogroll</code>, dann gebe ich die &uuml;bergeordnete Seite an, in meinem Fall das Blog. Schlie&szlig;lich ben&ouml;tigt die Seite noch ein Template, damit sie gerendert werden kann. Ich verwende hier ein spezielles Blogroll-Template. Auf die &Uuml;bersetzungen werde ich sp&auml;ter noch zu sprechen kommen.</p>
<h2>Template</h2>
<p>Ich werde hier nicht das komplette Template beschreiben, sondern nur die relevanten Teile. Das sind im Wesentlichen der Seitenkopf mit einer kurzen Beschreibung und das eigentliche Listing.</p>
<p>Der Seitenkopf besteht aus dem Titel und einer kurzen Beschreibung:</p>
<pre class="hljs"><code data-language="php">&lt;div <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">page</span>-<span class="hljs-title">intro</span>"&gt;
    &lt;<span class="hljs-title">h1</span>&gt;&lt;?= $<span class="hljs-title">page</span>-&gt;<span class="hljs-title">title</span>(); ?&gt;&lt;/<span class="hljs-title">h1</span>&gt;
    &lt;?= $<span class="hljs-title">page</span>-&gt;<span class="hljs-title">intro</span>()-&gt;<span class="hljs-title">kt</span>(); ?&gt;
&lt;/<span class="hljs-title">div</span>&gt;</span></code></pre>
<p>Das Listing verwendet ein Snippet, welches ich in den Notizen schon im Gebrauch habe:</p>
<pre class="hljs"><code data-language="php">&lt;ul <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">note</span>-<span class="hljs-title">list</span>"&gt;
    &lt;?<span class="hljs-title">php</span> <span class="hljs-title">foreach</span> ($<span class="hljs-title">blogroll</span> <span class="hljs-title">as</span> $<span class="hljs-title">blog</span>) : ?&gt;
        &lt;?<span class="hljs-title">php</span> $<span class="hljs-title">site</span>-&gt;<span class="hljs-title">organism</span>('<span class="hljs-title">list</span>-<span class="hljs-title">entry</span>-<span class="hljs-title">note</span>', ['<span class="hljs-title">note</span>' =&gt; $<span class="hljs-title">blog</span>]); ?&gt;
    &lt;?<span class="hljs-title">php</span> <span class="hljs-title">endforeach</span>; ?&gt;
&lt;/<span class="hljs-title">ul</span>&gt;</span></code></pre>
<p>Hier sei kurz angemerkt, dass ich f&uuml;r Snippets zwei Site-Methods nutzen, die ich mir selbst geschrieben haben. In diesem Fall <code>organism()</code>. Im Grunde funktionieren sie genauso wie Kirbys <code>snippet()</code> Funktion. Ich nutze meine Methoden, um etwas mehr Ordnung in meine Snippets zu bekommen. Darauf komme ich bestimmt in einem anderen Beitrag noch zu sprechen.</p>
<p>Mein Snippet <code>list-entry-note</code> bekommt also ein Blog aus meiner Blogroll hereingereicht und stellt es dann dar. Es braucht drei Informationen:</p>
<ol>
<li>Einen Titel</li>
<li>Eine Url</li>
<li>Ein Icon</li>
</ol>
<p>Die Daten kommen aus dem entsprechenden Controller</p>
<h2>Der Controller</h2>
<p>Controller dienen in Kirby dazu, die Templates von Datenlogik zu befreien. Ich mag den Ansatz und versuche deshalb meine Templates m&ouml;glichst dumm zu halten. Alles, was mit Daten zu tun hat, sollte m&ouml;glichst im Controller passieren.</p>
<p>So sieht der Controller f&uuml;r die Blogroll aus:</p>
<pre class="hljs"><code data-language="php"><span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">($page)</span> </span>{
    $blogroll = page(<span class="hljs-string">'blog'</span>)-&gt;blogroll()-&gt;toStructure();
    $blogEntries = [];

  <span class="hljs-keyword">foreach</span> ($blogroll <span class="hljs-keyword">as</span> $blog) {

    $url = str_replace([<span class="hljs-string">'https://'</span>, <span class="hljs-string">'http://'</span>, <span class="hljs-string">'/'</span>], [<span class="hljs-string">''</span>, <span class="hljs-string">''</span>, <span class="hljs-string">''</span>], Url::stripPath($blog-&gt;url()-&gt;value()));
    $icon = $page-&gt;getSiteIcon($url);

    $blogEntries[] = <span class="hljs-keyword">new</span> StructureObject([
        <span class="hljs-string">'content'</span> =&gt; [
            <span class="hljs-string">'title'</span> =&gt; $blog-&gt;title(),
            <span class="hljs-string">'url'</span> =&gt; $blog-&gt;url(),
            <span class="hljs-string">'icon'</span> =&gt; $icon,
            <span class="hljs-string">'intendedTemplate'</span> =&gt; <span class="hljs-string">'blogroll'</span>
        ]
    ]);
    }

    $blogrollStructure = <span class="hljs-keyword">new</span> Structure($blogEntries);

  <span class="hljs-keyword">return</span> [
    <span class="hljs-string">'blogroll'</span> =&gt; $blogrollStructure,
  ];
};</code></pre>
<p>Zun&auml;chst hole ich mir das Blog, denn dort sind die Daten hinterlegt. Ich greife direkt auf das Blogroll-Feld zu und lasse es mir als Struktur zur&uuml;ckgeben. Dann f&uuml;lle ich das <code>$blogEntries</code> Array mit Daten. Es bekommt jeweils den Titel, die Url und ein Icon. </p>
<p>F&uuml;r das Icon verwende ich eine eigene Methode, die versucht, das Favicon einer Seite zu holen und ggf. einen Fallback liefert. Das Ergebnis ist ein <code>data:image/png;base64</code>String. Also keine Bild-URL. Das erm&ouml;glicht mir, die Favicons eine Weile zu cachen. So muss ich nicht bei jedem Seitenaufruf etliche andere Seiten anfragen (dazu in einem anderen Post mal mehr).</p>
<p>Schlie&szlig;lich erzeuge ich mir aus dem Array eine <a href="https://getkirby.com/docs/reference/objects/cms/structure/factory">neue Struktur</a>, denn mein Note-Entry-Snippet erwartet das so (es bekommt ja normalerweise eine Notiz rein und das ist eine Kirby-Page).</p>
<h2>Abrunden der virtuellen Seite</h2>
<p>Jetzt bin ich fast so weit, es fehlen noch ein paar Informationen in der virtuellen Seite, denn diese ben&ouml;tigt u.&nbsp;a. einen Titel und eine Beschreibung. Diese verstecken sich in den &Uuml;bersetzungen. Ich habe mich dazu entschiede diese Daten zun&auml;chst nicht im Panel zu pflegen, weil ich sie vermutlich nicht besonders h&auml;ufig anpassen werde:</p>
<pre class="hljs"><code data-language="php"><span class="hljs-string">'translations'</span> =&gt; [
    <span class="hljs-string">'en'</span> =&gt; [
        <span class="hljs-string">'code'</span> =&gt; <span class="hljs-string">'en'</span>,
        <span class="hljs-string">'content'</span> =&gt; [
            <span class="hljs-string">'title'</span> =&gt; <span class="hljs-string">'Blogroll'</span>,
            <span class="hljs-string">'date'</span> =&gt; <span class="hljs-string">'2024-01-30'</span>,
            <span class="hljs-string">'intro'</span> =&gt; <span class="hljs-string">'Blog I read regularly and can recommend.'</span>,
            ]
        ],
    <span class="hljs-string">'de'</span> =&gt; [
        <span class="hljs-string">'code'</span> =&gt; <span class="hljs-string">'de'</span>,
        <span class="hljs-string">'content'</span> =&gt; [
            <span class="hljs-string">'title'</span> =&gt; <span class="hljs-string">'Blogroll'</span>,
            <span class="hljs-string">'date'</span> =&gt; <span class="hljs-string">'2024-01-30'</span>,
            <span class="hljs-string">'intro'</span> =&gt; <span class="hljs-string">'Blogs, die ich regelm&auml;&szlig;ig lese und die ich empfehlen kann.'</span>,
            <span class="hljs-string">'uuid'</span> =&gt; Uuid::generate(),
        ]
    ]
],</code></pre>
<p>Wie man sieht, stehen die n&ouml;tigen Daten nun in der virtuellen Seite. Die deutsche Sprachvariante bekommt zus&auml;tzlich noch eine uuid, sie ist bei mir die Hauptsprache.</p>
<h2>Die fertige Blogroll</h2>
<p>Damit ist die Seite nun unter <code>blog/blogroll</code> abrufbar. Das Template bekommt die Daten vom Controller und rendert sie mithilfe des Snippets. <a href="/blog/blogroll">Das Resultat k&ouml;nnt ihr hier sehen</a>.</p>
<p>Neue Eintr&auml;ge kann ich somit weiterhin einfach im Blueprint vom Blog vornehmen und die virtuelle Seite k&uuml;mmert sich um den Rest, ohne, dass ich im Panel noch extra eine Seite daf&uuml;r anlegen muss.</p>
<p>Danke, dass Du meinen RSS-Feed abonniert hast!</p>]]>
                </description>
            </item>
                                                        <item>
                <title>40 Jahre Mac</title>
                <link>https://maurice-renck.de/de/blog/2024/40-jahre-mac</link>
                <guid>https://maurice-renck.de/de/blog/2024/40-jahre-mac</guid>
                <pubDate>Thu, 25 Jan 2024 11:04:00 +0100</pubDate>
                <dc:creator>Maurice Renck</dc:creator>

                                    <category>Journal</category>
                
                <description>
                    <![CDATA[<img alt="" src="https://maurice-renck.de/media/pages/blog/2024/40-jahre-mac/5eab0a79dc-1706293602/dsc00850-1000x.jpg">
<p>Vor 40 Jahren wurde der erste Apple Mac veröffentlicht. Damit ist er jünger als ich, was ich zunächst anprangern muss. Spätestens beim Durchsehen alter Bilder, auf der Suche nach benutzten Computern, muss ich dann aber wohl eingestehen, dass das alles schon verdammt lange her ist.</p>
<p>Apple-Rechner benutze ich produktiv seit etwa 2006. Vorher war ich lange mit Linux unterwegs, hatte ja sogar dieses Linux-Blog und war bei <a href="https://radiotux.de">RadioTux</a> dabei. Dass ich bei Linux fast alles so konfigurieren konnte, wie ich wollte, fand ich toll. Das Terminal war ein stetiger Begleiter.</p>
<!--cut-->
<p>Irgendwann fing ich aber immer mehr an so ein bisschen neidisch auf die hübsche Oberfläche von Apple zu schauen. Ich versuchte das in GNOME nachzubauen, das ging zumindest so ein bisschen.</p>
<p>Irgendwann verdiente ich dann genug Geld, um mir einen aktuellen Mac leisten zu können, das erste weiße Intel Macbook wurde es. Endlich eine tolle Benutzeroberfläche und trotzdem das Terminal und ein UNIX zur Hand. Das war die perfekte Mischung. Außerdem musste ich dann auch nicht immer irgendwo ein Windows laufen haben, wenn ich mal mit einer etwas umfangreicheren DAW arbeiten wollte.</p>
<p>Seither bin ich beim Mac geblieben, zumindest als Desktop-Rechner. Linux läuft hier immer noch auf Servern oder dem OrangePi.</p>
<p>Meinen allerersten Mac kaufte ich übrigens gebraucht bei einer Redaktionsauflösung. Da lieft noch Mac OS 9 drauf und er war zum Kaufzeitpunkt eigentlich schon veraltet. So wirklich genutzt hatte ich ihn dann auch nicht, aber hey! Ich hatte einen Mac.</p>
<p>Weiter ging es dann mit besagtem Intel Macbook, einem iMac und seitdem immer mal wieder einen neuen Mac Mini, die für mich das beste Preis-Leistungsverhältnis haben.</p>
<p>Auf alte Bilder schauend, muss ich sagen, dass mein Setup zwar größer und moderner geworden ist, aber grundlegend immer gleich blieb. Ein Mac, Monitor, Mikrofon und irgendwo steht noch ein Midi-Keyboard rum. Manche Dinge ändern sich wohl nie…</p>
<figure><img alt="" src="https://maurice-renck.de/media/pages/blog/2024/40-jahre-mac/b2df05018d-1706294292/schreibtisch-heute-800x.webp" title="" width="800"></figure>
<p>Danke, dass Du meinen RSS-Feed abonniert hast!</p>]]>
                </description>
            </item>
            </channel>
</rss>