<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Posts on LNA-DEV ~ Lukas Nagel</title>
    <link>https://lna-dev.net/en/posts/</link>
    <description>Recent content in Posts on LNA-DEV ~ Lukas Nagel</description>
    <image>
      <title>LNA-DEV ~ Lukas Nagel</title>
      <url>https://lna-dev.net/assets/Ping%C3%BCino/Ping%C3%BCino.png</url>
      <link>https://lna-dev.net/assets/Ping%C3%BCino/Ping%C3%BCino.png</link>
    </image>
    <generator>Hugo -- 0.160.1</generator>
    <language>en-US</language>
    <managingEditor>me@lna-dev.net (Lukas Nagel)</managingEditor>
    <webMaster>me@lna-dev.net (Lukas Nagel)</webMaster>
    <copyright>🄯 various licenses</copyright>
    <lastBuildDate>Mon, 02 Mar 2026 21:45:00 +0100</lastBuildDate>
    <atom:link href="https://lna-dev.net/en/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>I won in the Wiki Loves Earth 2025 Special Category</title>
      <link>https://lna-dev.net/en/posts/photography/wiki-loves-earth-2025/</link>
      <pubDate>Mon, 02 Mar 2026 21:45:00 +0100</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/photography/wiki-loves-earth-2025/</guid>
      <description>I took part in the Wiki Loves Earth Competition in 2025 and won in the special category human rights and environment. Here I talk about my thoughts.</description>
      <content:encoded><![CDATA[<p>In my progress of getting better at photography, I wanted to take part in a photo competition as some kind of motivation. I therefore took a look at several competitions out there in the last year. One caught my attention: The Wiki Loves Earth competition. I love Wikipedia and share a good proportion of its values. Information should be open and accessible to anyone. Therefore, it sounded quite nice to me.</p>
<p>I later decided to take part in it. The requirements were that the photos are licensed under a Creative Commons (CC) license. As I publish most (currently all) of my photography under such licenses, this was not an issue for me. More notably, it was some kind of motivation knowing that now my images can be found and used by anyone on Wikipedia with the appropriate credit.</p>
<p>There were several categories: One <a href="https://wikilovesearth.org/wiki-loves-earth-international-presents-the-best-images-in-2025/">main category</a> with a landscape and macro image, which was hosted first on a national level, after which the winners would compete internationally. The second category in which I later won was the <a href="https://wikilovesearth.org/winning-images-of-the-special-category-human-rights-and-environment-from-wiki-loves-earth-2025/">special category</a> for environment and human rights.</p>
<p>My image, as you can see below, is a photo of a baby bird which tried to swallow a piece of plastic. I took this image in my garden where my father found the bird. The parents probably found the piece of plastic somewhere in the neighborhood and used it for nest building. It is a strong reminder to take care of your trash and what can happen to the environment around us if we don&rsquo;t.</p>
<p><img alt="Wiki Loves Earth winner bird swallowing plastic" loading="lazy" src="/en/posts/photography/wiki-loves-earth-2025/wiki-loves-earth-winner-bird-swallowing-plastic.png"></p>
<p>I uploaded a couple of images from protected areas and the image I talked about above. The normal images were all from Germany, so I competed there locally. In this local competition, there were many good photos, so my images did not rank especially high. So I had my fun and didn’t think much about the competition. A few months (or so) later, I got a Wikipedia message and mail that my image won in the International Human Rights and Environment competition. I must admit that I was fairly surprised. After the okayish result in the local competition I did not suspect that it would go so well for me in the special category. After seeing the message for the first time I spent some time with validating that it is real and not some crazy spearphishing attempt. But everything seemed perfectly fine. I was really happy that day.</p>
<p>The special category does not have any super crazy prizes, but I got a nice diploma with some Wikipedia goodies. I especially like the pencil holder. Also my laptop now has a new sticker which is always a great plus.</p>
<p><img alt="The goodies I received from Wikipedia" loading="lazy" src="/en/posts/photography/wiki-loves-earth-2025/wikipedia-goodies.jpg"></p>
<p>Anyway it was a great competition and I had a lot of fun. I really look forward to participating again in 2026. Maybe I could motivate you to join me and many others.</p>
]]></content:encoded>
    </item>
    <item>
      <title>My photography workflow</title>
      <link>https://lna-dev.net/en/posts/photography/my-workflow/</link>
      <pubDate>Wed, 24 Sep 2025 21:19:00 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/photography/my-workflow/</guid>
      <description>An overview of my photography workflow, including the tools I use and how I manage my photos.</description>
      <content:encoded><![CDATA[<p>I spent the last couple of weeks going through all my photographs I took in the last two years (and more) and edited my best shots. In this post I want to share what I learned, which tools I use and describe my workflow from taking the photos, over editing them, to publishing them on my webpage and social media. I am in no way an expert photographer, but I think it might be useful to those who are hobby photographers like me, interested in open source photo editing, or just curious.</p>
<h2 id="the-camera-itself">The camera itself</h2>
<p>When I started photographing with my <code>Panasonic G91</code> I mostly used the full automatic mode. That is pretty easy but not very optimal in my opinion. After reviewing a couple of images I took during that time, I saw that a couple of images had settings that did not make very much sense. The aperture priority mode I tried with this camera made a lot of a difference. In this mode, you set the ISO and aperture and the camera automatically determines the appropriate shutter speed. This mode gives you a lot more control over how the image looks because you set the aperture consciously. I also got a pretty good feeling for which light conditions required which settings, because I chose two of the three myself and had a good look at what shutter speed the camera chose.</p>
<p>Sometime this year I got a new <code>Nikon Z8</code>, which changed a lot for me. I bought three prime lenses instead of a zoom lens, which I had with the Panasonic. Those prime lenses force me into being a bit more creative when taking the shot. Those lenses are a 20mm, a 50mm and a 100mm macro. The 50mm was quite unspectacular for me because it looks like a normal image. The tele lens felt quite natural to me, which was really nice. But I realized that macro was a lot harder than I thought. Even with image stabilizers it&rsquo;s hard to get your subject into focus and have a nice bokeh without having everything, or mostly everything, blurred out. My wide-angle lens first felt odd, but after using it for a bit, I fell in love with it. I think this is the hardest lens, but I have produced some really cool looking images with it. Overall, I am quite happy with those lenses.</p>
<p>Having no zoom lens is quite another way of photographing. It means you cannot always take the shots you want to because you cannot change your focal length quickly in the field. I often had my other lenses with me, but changing them always takes a bit. But I think maybe that&rsquo;s a good thing from time to time. So you can take the same route several times and force yourself into thinking a bit out of the box and experiment with different focal lengths.</p>
<p>On my recent trip to Sweden, I used filters for the first time. They are a game changer when photographing landscapes with a tripod. Those smooth images when using an ND filter to photograph water are just awesome. I think I created one or two good waterfall photographs. I also tried a CPL filter and the combination of ND + CPL, but do not really know when to use what. I am still trying to figure that out, so I take most of the time a shot with and without CPL.</p>
<h2 id="photomanagement">Photomanagement</h2>
<p>I am a Linux user because it is the only available option which offers a good open source experience with a lot of software. And being open source and privacy-friendly are requirements for me to protect my freedom. Therefore classical image editors like Lightroom are out of the question for me. Firstly, because they are neither open source nor do they work on my Fedora system (or any other Linux system without dirty workarounds). And secondly, because I really do not like Adobe and its very ugly subscription model.</p>
<p>But that&rsquo;s no problem at all because there is a similar and in some cases even better, alternative to Lightroom called Darktable. It is free and open source software (FOSS) and available for all of the big operating systems. It offers the capability to manage big libraries of photos and edit them in a non-destructive way.</p>
<p>I have all my images in a library folder which is synchronized to all my devices using Nextcloud (also FOSS), but other tools like Syncthing could be used. Ensuring all my photos are only in one place and synchronized assures that I do not have duplicates or lose something because it was not backed up correctly.</p>
<p>If I come back from a shoot and want to save my images, I load them into a new folder under the library folder. I give every shoot a name to know what I can expect. Those shoot folders are saved in the following format: <code>library/&lt;year&gt;/&lt;shootName&gt;</code></p>
<p>After saving all my files, which are now already safely backed up, I go through them and pick what is going to be deleted and give the rest of the images a rating to prioritize them while editing. I work with stars for the rating.</p>
<table>
  <thead>
      <tr>
          <th>Rating</th>
          <th>What it means</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rejected</td>
          <td>To be deleted</td>
      </tr>
      <tr>
          <td>0 / 1</td>
          <td>Unrated / ToDo</td>
      </tr>
      <tr>
          <td>2</td>
          <td>Bad photo but wanna keep</td>
      </tr>
      <tr>
          <td>3</td>
          <td>Maybe worth editing</td>
      </tr>
      <tr>
          <td>4</td>
          <td>Will be edited</td>
      </tr>
      <tr>
          <td>5</td>
          <td>One of the better shots I have taken</td>
      </tr>
  </tbody>
</table>
<p>I then edit first all five-star images and then the four-star ones. I previously did only the sorting of the images right after the shoot, but this had the disadvantage that I needed to do a <strong>lot</strong> of work in the last few weeks to edit all of the photos I wanted to. For the future, my plan is to edit them right away after the shoot to have only small bits of work and not so much at once. A nice bonus is that if you edit images not long after the shoot, you still have in mind the intention you had while shooting.</p>
<p>Besides the stars, I also use colors to manage my photos. I, for example, use red for the images I have already published/exported and purple for private images I do not want to export for now — if, for example, a person I know is on them, but they do not want to be online.</p>
<h2 id="editing">Editing</h2>
<p>After editing all my photos, I seem to have built a habit about which modules I use most on my images. And what I am most happy about is that I can see that my edited photographs look much better now than in the beginning, where I struggled to be better than the JPG produced by the camera. I am sure I can learn <strong>a lot more</strong> in this area, but think that needs time and maybe some guidance. But for now, I want to share with you how I am editing my images.</p>
<h3 id="base-style">Base <em>style</em></h3>
<p>I have a style in Darktable which I apply to every image I start editing and go from there. In this style I activate color balance RGB and increase the global brilliance and global saturation. I activate denoise (profiled) and local contrast at the default settings and set the color calibration to &ldquo;as shot in camera.&rdquo;</p>
<h3 id="manual-edits">Manual edits</h3>
<p>I then edit the photo manually to fit my liking. I first try haze removal and take a look if it is improving the image. Most of the time I then increase the local contrast a bit and sometimes add a bit of contrast over the filmic RGB module. But that&rsquo;s not that often happening. After that, I check if the shadows and highlights module helps the image and play around with the settings of it a bit. In color balance RGB I try to set everything so the image feels right. Most of the time only brilliance and saturation, but sometimes also chroma. I use crop and rotate the image to make it as pleasing to my eye as possible. If there are any smudges on the camera lens, I remove them with retouch. Sometimes I also use sharpen, but I do it less and less.</p>
<h2 id="exporting">Exporting</h2>
<p>I publish all my images to my website and syndicate them from there to all social media platforms. I have changed quite a bit in the process since my last automation posts, but that is a topic for another time. For now what is important is that I publish for the web. Therefore, I do a few things before publishing, like setting some notes on the image which describe what can be seen. That information is later used to set an alt tag which is used for people with screen readers, like for example some blind people use. Besides that, I also add tags which are used later as hashtags for social media. I seldom set a description, which in my case gets displayed as a text next to the image. But in some cases, I want the viewer to have some information and then I can provide it with that. I also set publisher and rights so that all the important metadata is set.</p>
<hr>
<p>I hope you could learn something from my reflections about my photography journey or at least have found it interesting to read. I will continue to learn both getting better photographing skills and editing skills and maybe write about it in the future. I hope I can see some progress then 😄</p>
]]></content:encoded>
    </item>
    <item>
      <title>This website is now on the darknet</title>
      <link>https://lna-dev.net/en/posts/privacy/tor/</link>
      <pubDate>Sat, 10 May 2025 23:10:00 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/privacy/tor/</guid>
      <description>Why I decided to publish my website on the darknet and a bit of background information about it and the Tor network.</description>
      <content:encoded><![CDATA[<h2 id="tor-as-the-gateway-to-information">Tor as the gateway to information</h2>
<p>The Tor network empowers people to access the uncensored internet from many places around the world. It provides a way for all of humanity to access information in a free and open manner. It includes many tools and options to circumvent censorship and even active measures taken against it. Tor bridges or the Snowflake browser add-on, for example, help people in censored countries connect to Tor even when the known entry node IPs are blocked by the state.</p>
<h2 id="how-tor-gives-you-freedom-of-speech">How Tor gives you freedom of speech</h2>
<p>But that’s only half of it. The freedom to acquire information is arguably one of the most fundamental qualities of a good life. Freedom of expression is also essential for a free and fulfilling life. Tor also enables people who have to fear the consequences of an authoritarian state to publish their opinions as anonymously as possible and therefore protect those freedoms.</p>
<h2 id="how-you-can-use-tor">How you can use Tor</h2>
<p>For those of you not very tech-savvy, here’s a little summary of how Tor works and how you can access the &ldquo;darknet&rdquo;.</p>
<p>If you want to start browsing the Tor network and the &ldquo;normal&rdquo; World Wide Web, you need a special browser. This browser is called the Tor Browser and you can download it <a href="https://www.torproject.org/download/">here</a>. With this browser installed, you can use it like any other browser.</p>
<p>You might notice that it is a bit slow, but this is because of how the Tor network works. Basically, your whole traffic is encrypted multiple times like an onion. That’s also where the name Tor comes from — <strong>T</strong>he <strong>O</strong>nion <strong>R</strong>outer. This traffic is then sent over multiple computers, so your initial location can only be traced back with difficulty and under very specific conditions. This enables the end user to stay private.</p>
<p>There is also a new type of top-level domain available if you are using the Tor browser. (A top-level domain (TLD) is something like <code>.com</code>, <code>.org</code>, <code>.net</code>, <code>.de</code>, <code>.au</code>, <code>.nz</code>, &hellip;) This TLD is <code>.onion</code> and can only be accessed over the Tor network. You can get a domain like that for free and they look kind of weird. Here is mine, for example: <a href="http://lnadevwj2vzomixiunv7i4lahwpoxh6zw56cxbce3uui5ijmwt4czpyd.onion">lnadevwj2vzomixiunv7i4lahwpoxh6zw56cxbce3uui5ijmwt4czpyd.onion</a>. That is because there need to be many different ones if you want to give them out for free.</p>
<p>These <code>.onion</code> domains allow the creator of the website to hide their IP address and therefore stay (sort of) &ldquo;anonymous&rdquo; — similar to the user, but the other way around. This is, like I said before, needed if you want to express yourself from within a repressive country. But even today it is needed for activists, journalists, whistleblowers and many more groups of people needed for a well-functioning and free democracy.</p>
<h2 id="why-does-that-concern-me">Why does that concern me?</h2>
<p>Our world gets more autocratic and dangerous day by day. Looking at you, USA&hellip; But even here in Europe, things look bad. Far-right-wing parties with high vote counts and corrupt politicians, for a start. More worrying is that even the more democratic parties are applying more and more autocratic tools like mass surveillance and strengthening the police&hellip;
Still, the situation in America is way worse — not even speaking of all the other parts of the world like China or Russia, which are the authoritarian powerhouses of this world.</p>
<p>With all this repression, there is a big need in the world for tools like Tor and they need to be supported as much as possible. I wanted to check out how difficult it is to host a website — in my case, this one — on Tor, or as the mainstream calls it, &ldquo;the darknet&rdquo;. As it turns out, it is extremely easy, as you’ll see in one of my upcoming blog posts about how my setup works. But for now, let’s just say you can skip a couple of steps compared to a traditional website. You don’t need to buy a domain — you just get one for free. You also don’t need to worry about any firewalls or anything similar. Tor goes straight through it. Remember, Tor was designed to thrive in a censored environment.</p>
<p>With the knowledge to host a website over the Tor network and the information on how easy it is broadly available, we are better prepared for a reality that isn’t as fine as our current situation (in which many people currently live and suffer under).</p>
<p>So for me, it was an experiment to see how this technology works and to gain the knowledge — and hope I never need it. My website is the same here and on the darknet, so I have no anonymity boost because of it. But after all, it’s also just very cool and interesting to me. Censorship-prevention technologies have fascinated me since like forever.</p>
<h2 id="how-you-can-learn-more-about-this-topic">How you can learn more about this topic</h2>
<p>There are a couple of good sources about the threat of authoritarianism, Tor and how it can help. The most obvious source is the <a href="https://www.torproject.org/">website of Tor</a> itself. There you can find a lot of information about how Tor works and how to use it. Also consider going through the blog posts on their page to see news and that sort of thing.</p>
<p>The Electronic Frontier Foundation (EFF) has a section about <a href="https://www.eff.org/issues/free-speech">free speech</a> and <a href="https://www.eff.org/search/site/tor?f%5B0%5D=type%3Apage">a bit about Tor</a>.</p>
<p>There are many other resources out there — explore, read and stay curious. The more people understand and support tools like these, the more resilient we become.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to automate Bluesky posts</title>
      <link>https://lna-dev.net/en/posts/projects/bluesky-automation/</link>
      <pubDate>Wed, 29 Jan 2025 18:50:00 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/projects/bluesky-automation/</guid>
      <description>I recently created my Bluesky account and in this post, I’ll show you how I automate my posts to it.</description>
      <content:encoded><![CDATA[<p>I had Bluesky on my radar for quite a while but never liked the way the platform was built. <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> But recently, I decided to give it a try. With this post, I want to share my experience automating my posts to Bluesky with you. (You can find <a href="https://bsky.app/profile/lna-dev.net">my profile</a> here.)</p>
<p>I also wrote <a href="../pixelfed-automation/">a post</a> in which I explained how to automate posts to <a href="../../media/fediverse/#pixelfed">Pixelfed</a> (a <a href="../../media/fediverse/">Fediverse</a> platform), which is quite similar to this one. I might refer to that post from time to time because the process is quite similar.</p>
<blockquote>
<p>This post might be a bit technical if you have no previous experience with coding / tech.</p>
</blockquote>
<h2 id="posse">POSSE</h2>
<p>So, first of all, why do all this? For me, the answer is simple: I have been getting into the <a href="https://indieweb.org/">IndieWeb</a> bubble for a while. As part of it, you want to build your own website, which you control and have full power over. Your website is your little garden where you put all your stuff. But because most people won’t just visit your website directly, you need a way to get your content to them. For this, automation is key.</p>
<p>You host all your content on your page and then build scripts and little bots that take the content from your webpage and <strong>syndicate</strong> it wherever you want. Whether it&rsquo;s the walled gardens of big tech, the <a href="../../media/fediverse/">Fediverse</a> or Bluesky doesn’t matter. You just post to your website and your bots do the rest. This principle is called <em>Post On your Own Site Syndicate Elsewhere</em> or <strong>POSSE</strong> for short.</p>
<p>I might write a detailed post about the IndieWeb and <a href="https://www.citationneeded.news/posse/">POSSE</a> in the future, so stay tuned!</p>
<h2 id="my-data-source---rss-feed">My data source - RSS Feed</h2>
<p>As I already explained in my <a href="../pixelfed-automation/#rss-feed">Pixelfed Automation</a> post, I have an RSS feed that contains all the data I need for publishing my images. Specifically, this includes the image URL, description, alt text and hashtags.</p>
<p>Your data source may vary if you have an API or something similar to access the required data. Personally, I like the idea of having an RSS feed because users of my webpage can follow the same feed and use an RSS reader to consume my content.</p>
<h2 id="the-python-script">The Python script</h2>
<p>Now, onto the actual automation. I used Python because I already had my Pixelfed script, which I could reuse a lot from.</p>
<p>There are a couple of code blocks that I didn’t really touch. For example, the parts responsible for retrieving the image from my website, selecting which image to publish and notifying my API about the images that are now online.</p>
<p>Since I’ve covered these steps before, I’ll jump straight to the interesting part: how to publish to Bluesky. (If you want to know the details about the other steps, you can read them <a href="../pixelfed-automation/#the-pixelfed-script">here</a>.)</p>
<h3 id="publish-to-bluesky">Publish to Bluesky</h3>
<p>This is where it gets interesting because we’re doing the actual publishing. First of all, there is a good Python library for Bluesky / ATproto called <code>atproto</code>. You can install it with the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>pip install atproto
</span></span></code></pre></div><p>After installing the right package, we can look at the code itself. First, we need a client where we pass our email and PAT (I explain how to generate one in the next section). This is enough to authenticate with Bluesky.</p>
<p>Next, we prepare our post. For this, I use the <code>TextBuilder</code> provided by the package to format links and hashtags correctly. After preparing our text/caption, we can use the <code>send_image</code> or <code>send_post</code> method, respectively. And that’s pretty much it! Quite easy when using the library.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> atproto <span style="color:#f92672">import</span> Client
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> atproto <span style="color:#f92672">import</span> client_utils
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>BLUESKY_PAT <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;BLUESKY_PAT&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>bsClient <span style="color:#f92672">=</span> Client()
</span></span><span style="display:flex;"><span>bsClient<span style="color:#f92672">.</span>login(
</span></span><span style="display:flex;"><span>    login<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;bluesky@lna-dev.net&#34;</span>,
</span></span><span style="display:flex;"><span>    password<span style="color:#f92672">=</span>BLUESKY_PAT,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># [...]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_entry</span>(entry):
</span></span><span style="display:flex;"><span>    caption <span style="color:#f92672">=</span> client_utils<span style="color:#f92672">.</span>TextBuilder()
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34;More at &#34;</span>)
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>link(text<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://lna-dev.net/en/gallery&#34;</span>, url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://lna-dev.net/en/gallery&#34;</span>)
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> entry<span style="color:#f92672">.</span>tags:
</span></span><span style="display:flex;"><span>        caption<span style="color:#f92672">.</span>tag(<span style="color:#e6db74">&#34;#&#34;</span> <span style="color:#f92672">+</span> element<span style="color:#f92672">.</span>term, element<span style="color:#f92672">.</span>term)
</span></span><span style="display:flex;"><span>        caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34; &#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    media_url <span style="color:#f92672">=</span> entry<span style="color:#f92672">.</span>media_content[<span style="color:#ae81ff">0</span>][<span style="color:#e6db74">&#34;url&#34;</span>]
</span></span><span style="display:flex;"><span>    alt_text <span style="color:#f92672">=</span> re<span style="color:#f92672">.</span>search(<span style="color:#e6db74">&#39;alt=&#34;(.*?)&#34;&#39;</span>, entry<span style="color:#f92672">.</span>summary)
</span></span><span style="display:flex;"><span>    alt_text <span style="color:#f92672">=</span> alt_text<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">1</span>) <span style="color:#66d9ef">if</span> alt_text <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;Alt not found&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    bsClient<span style="color:#f92672">.</span>send_image(
</span></span><span style="display:flex;"><span>        text<span style="color:#f92672">=</span>caption,
</span></span><span style="display:flex;"><span>        image<span style="color:#f92672">=</span>download_image(media_url),
</span></span><span style="display:flex;"><span>        image_alt<span style="color:#f92672">=</span>alt_text,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    published_entry(entry<span style="color:#f92672">.</span>title)
</span></span></code></pre></div><h3 id="how-to-get-your-bluesky-pat">How to get your Bluesky PAT</h3>
<p>To generate a <em>Personal Access Token</em> (<strong>PAT</strong>), also called an <em>App password</em>, go to <code>Settings =&gt; Privacy and Security =&gt; App passwords</code> or click <a href="https://bsky.app/settings/privacy-and-security">here</a>.</p>
<p>Now you can just click <code>Add App Password</code> and copy the text. That’s it! You can now add this to your script or environment variables.</p>
<h3 id="full-code">Full code</h3>


<p><details >
  <summary markdown="span">The full code</summary>
  <p>Alternatively to this code block, there is an up-to-date <a href="https://github.com/LNA-DEV/Autouploader">Repo</a> on GitHub.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> io <span style="color:#f92672">import</span> BytesIO
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> re
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> feedparser
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> datetime
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> random
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> requests
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> atproto <span style="color:#f92672">import</span> Client
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> atproto <span style="color:#f92672">import</span> client_utils
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>BLUESKY_PAT <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;BLUESKY_PAT&#39;</span>)
</span></span><span style="display:flex;"><span>API_KEY <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;API_KEY&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>bsClient <span style="color:#f92672">=</span> Client()
</span></span><span style="display:flex;"><span>bsClient<span style="color:#f92672">.</span>login(
</span></span><span style="display:flex;"><span>    login<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;bluesky@lna-dev.net&#34;</span>,
</span></span><span style="display:flex;"><span>    password<span style="color:#f92672">=</span>BLUESKY_PAT,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Function to filter entries based on the name list</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">filter_entries</span>(entries, name_list):
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Temp skips (for example if this image does not fit currently)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># name_list.append(&#34;P1002496.JPG&#34;)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> [entry <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> entries <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>title <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> name_list]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_already_uploaded_items</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;https://api.lna-dev.net/autouploader/bluesky&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>            string_list <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>json()
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> string_list
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Failed to fetch data from API. Status code: </span><span style="color:#e6db74">{</span>response<span style="color:#f92672">.</span>status_code<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;An error occurred: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">published_entry</span>(entry_name):
</span></span><span style="display:flex;"><span>    requests<span style="color:#f92672">.</span>post(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://api.lna-dev.net/autouploader/bluesky?item=</span><span style="color:#e6db74">{</span>entry_name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>, headers<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;Authorization&#34;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;ApiKey </span><span style="color:#e6db74">{</span>API_KEY<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">download_image</span>(image_url):
</span></span><span style="display:flex;"><span>    response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(image_url)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> BytesIO(response<span style="color:#f92672">.</span>content)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Failed to download image!&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_entry</span>(entry):
</span></span><span style="display:flex;"><span>    caption <span style="color:#f92672">=</span> client_utils<span style="color:#f92672">.</span>TextBuilder()
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34;More at &#34;</span>)
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>link(text<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://lna-dev.net/en/gallery&#34;</span>, url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://lna-dev.net/en/gallery&#34;</span>)
</span></span><span style="display:flex;"><span>    caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> entry<span style="color:#f92672">.</span>tags:
</span></span><span style="display:flex;"><span>        caption<span style="color:#f92672">.</span>tag(<span style="color:#e6db74">&#34;#&#34;</span> <span style="color:#f92672">+</span> element<span style="color:#f92672">.</span>term, element<span style="color:#f92672">.</span>term)
</span></span><span style="display:flex;"><span>        caption<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#34; &#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    media_url <span style="color:#f92672">=</span> entry<span style="color:#f92672">.</span>media_content[<span style="color:#ae81ff">0</span>][<span style="color:#e6db74">&#34;url&#34;</span>]
</span></span><span style="display:flex;"><span>    alt_text <span style="color:#f92672">=</span> re<span style="color:#f92672">.</span>search(<span style="color:#e6db74">&#39;alt=&#34;(.*?)&#34;&#39;</span>, entry<span style="color:#f92672">.</span>summary)
</span></span><span style="display:flex;"><span>    alt_text <span style="color:#f92672">=</span> alt_text<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">1</span>) <span style="color:#66d9ef">if</span> alt_text <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;Alt not found&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    bsClient<span style="color:#f92672">.</span>send_image(
</span></span><span style="display:flex;"><span>        text<span style="color:#f92672">=</span>caption,
</span></span><span style="display:flex;"><span>        image<span style="color:#f92672">=</span>download_image(media_url),
</span></span><span style="display:flex;"><span>        image_alt<span style="color:#f92672">=</span>alt_text,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    published_entry(entry<span style="color:#f92672">.</span>title)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Parse the RSS feed</span>
</span></span><span style="display:flex;"><span>feed_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https://lna-dev.net/en/gallery/index.xml&#39;</span>
</span></span><span style="display:flex;"><span>feed <span style="color:#f92672">=</span> feedparser<span style="color:#f92672">.</span>parse(feed_url)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Filter out entries with specific names</span>
</span></span><span style="display:flex;"><span>specific_names <span style="color:#f92672">=</span> get_already_uploaded_items()
</span></span><span style="display:flex;"><span>filtered_entries <span style="color:#f92672">=</span> filter_entries(feed<span style="color:#f92672">.</span>entries, specific_names)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> filtered_entries:
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;No entries available after filtering.&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Calculate time differences considering only month, day, hour, minute, and second</span>
</span></span><span style="display:flex;"><span>    current_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>now()
</span></span><span style="display:flex;"><span>    closest_entry <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    skipped_entries <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    min_difference <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> filtered_entries:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">or</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>            skipped_entries<span style="color:#f92672">.</span>append(entry)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>  <span style="color:#75715e"># Skip entries with invalid year</span>
</span></span><span style="display:flex;"><span>        temp <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>mktime(entry<span style="color:#f92672">.</span>published_parsed)
</span></span><span style="display:flex;"><span>        published_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>fromtimestamp(temp)
</span></span><span style="display:flex;"><span>        difference <span style="color:#f92672">=</span> abs(current_time<span style="color:#f92672">.</span>replace(year<span style="color:#f92672">=</span>published_time<span style="color:#f92672">.</span>year, tzinfo<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>) <span style="color:#f92672">-</span> published_time)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> min_difference <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span> <span style="color:#f92672">or</span> difference <span style="color:#f92672">&lt;</span> min_difference:
</span></span><span style="display:flex;"><span>            min_difference <span style="color:#f92672">=</span> difference
</span></span><span style="display:flex;"><span>            closest_entry <span style="color:#f92672">=</span> entry
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> closest_entry <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;No valid entries available after filtering.&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Get all entries published at the same time as the closest entry</span>
</span></span><span style="display:flex;"><span>        closest_entries <span style="color:#f92672">=</span> [entry <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> filtered_entries <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>published <span style="color:#f92672">==</span> closest_entry<span style="color:#f92672">.</span>published]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> skipped_entries:
</span></span><span style="display:flex;"><span>            closest_entries<span style="color:#f92672">.</span>append(element)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Select a random entry from the closest entries</span>
</span></span><span style="display:flex;"><span>        random_entry <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>choice(closest_entries)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Print the selected entry</span>
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Random entry closest to the current date/time (ignoring year):&#34;</span>)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Title:&#34;</span>, random_entry<span style="color:#f92672">.</span>title)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;URL:&#34;</span>, random_entry<span style="color:#f92672">.</span>link)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Published Date:&#34;</span>, random_entry<span style="color:#f92672">.</span>published)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        publish_entry(random_entry)
</span></span></code></pre></div>
</details></p>

<h2 id="schedule-the-script">Schedule the script</h2>
<p>After creating such a script, you need to trigger it somehow. If you are using <a href="../../../tags/kubernetes/">Kubernetes</a>, you may want to check out the <a href="../pixelfed-automation/#the-schedule">Pixelfed post</a>, which explains how to set up a Kubernetes CronJob.</p>
<p>For those who just have a simple server, I recommend setting up a Linux CronJob to trigger the script periodically.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I mainly have two big problems with Bluesky. The first is that there is already a good protocol for decentralized social networks. So why reinvent the wheel? And not only did they reinvent it, but they also made it worse. The AT protocol relies much more on centralized services controlled by Bluesky, which is completely different from <a href="../../../tags/activitypub/">ActivityPub</a>. The second issue is that Bluesky is a for-profit company instead of a nonprofit, which is the standard in the <a href="../../media/fediverse/">Fediverse</a>. What’s really alarming is that this company doesn’t yet know how it wants to make money. So, it’s realistic that the platform will get worse in the future to generate revenue.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>Nginx proxy manager on TrueNas Scale without True Charts</title>
      <link>https://lna-dev.net/en/posts/home-server/nginx-proxy-manager/</link>
      <pubDate>Sat, 05 Oct 2024 14:00:05 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/home-server/nginx-proxy-manager/</guid>
      <description>How to setup a reverse proxy for TrueNas Scale without True Charts using the Nginx proxy manager.</description>
      <content:encoded><![CDATA[<p>This is the third post in a <a href="../../../tags/home-server-project/">larger series</a> about self-hosting with TrueNAS Scale. In this post, I want to show you how to set up the Nginx reverse proxy without relying on TrueCharts. I explained in <a href="../truenas-scale/">the last post</a> why I am moving away from them. In short: Support will be dropped.</p>
<h2 id="nginx-proxy-manager">Nginx Proxy Manager</h2>
<p>Using the default charts in TrueNAS Scale, your application can only be exposed on a high port. Accessing your apps via such a weird port and without a proper domain isn&rsquo;t cutting it. Is it? Therefore you can run a reverse proxy that manages TLS encryption and directs traffic for each subdomain to its respective app, without needing a special port in the URL.</p>
<p>Until now, I had always used a Traefik proxy when working with Kubernetes, but I found this cool project that seemed to fit perfectly. It&rsquo;s called Nginx Proxy Manager, or NPM for short. With NPM, you can set up a reverse proxy and also manage your TLS certificates using Let&rsquo;s Encrypt.</p>
<h2 id="installation">Installation</h2>
<p>First of all, we can&rsquo;t install this app the way we would normally install an app. It is in the default app catalog, but this version can only be hosted on port 9000. Instead, we’ll use the Docker image to deploy it. This won’t make any difference, but we can use our own IP address for it, which also means we can use all ports.</p>
<p>To start, go to the app catalog and click <code>Custom APP</code> in the top right corner. There, you can configure everything. I’ll provide the required settings below.</p>
<table>
  <thead>
      <tr>
          <th>Configuration Name</th>
          <th>Value</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Image Repository</td>
          <td><code>jc21/nginx-proxy-manager</code></td>
      </tr>
      <tr>
          <td>Image Tag</td>
          <td><code>latest</code></td>
      </tr>
      <tr>
          <td>External Interface</td>
          <td>Choose your interface and use an IP address that is not used by any device, including your server <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</td>
      </tr>
      <tr>
          <td>DNS Policy</td>
          <td>Prioritize Kubernetes DNS <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>Set up two storage paths one for <code>/etc/letsencrypt</code> and one for <code>/data</code>. I recommend using Host Path Volumes.</td>
      </tr>
  </tbody>
</table>
<p>After installing the app, you can access it via its IP address on port <code>81</code>. The default username is <code>admin@example.com</code>and the password is <code>changeme</code>. Change them immediately!</p>
<h2 id="setting-up-tls">Setting Up TLS</h2>
<p><img alt="Screenshot of the Nginx Proxy Manager home screen" loading="lazy" src="/en/posts/home-server/nginx-proxy-manager/npmHomeScreen.png"></p>
<p>Once everything is set up and you’ve changed your password, you’ll see something like this. First of all, I recommend setting up your TLS certificate if you have a domain. If you don’t have a domain, you can still upload a custom certificate.</p>
<p><img alt="Overview of all TLS certificates in Nginx Proxy Manager" loading="lazy" src="/en/posts/home-server/nginx-proxy-manager/npmSslOverview.png"></p>
<p>Click on the <code>SSL Certificates</code> tab to see a list of all the certificates you’ve set up. If you click the button in the top right, you can create a new certificate. Here, you can choose between the Let&rsquo;s Encrypt certificate authority or upload a custom certificate if you&rsquo;re using a local domain. But if you can get a domain, I recommend doing so. I will proceed with the setup instructions for Let&rsquo;s Encrypt.</p>
<p><img alt="The Let&rsquo;s Encrypt setup dialog in NPM" loading="lazy" src="/en/posts/home-server/nginx-proxy-manager/letsEncrypt.png"></p>
<p>If your server is reachable from the public internet, you just need to enter your domain name and agree to the Terms of Service. This will spin up a local webpage to verify that you actually own the domain you enter.</p>
<blockquote>
<p>You can also set up a TLS certificate for all subdomains by entering <code>*.example.com</code>.</p>
</blockquote>
<p>If your server is not accessible from the public or if you want to use another method, you can choose the DNS challenge. NPM has a couple of DNS providers which you can choose from. Take a look at the list and search for your DNS provider.</p>
<p><img alt="The Let&rsquo;s Encrypt setup dialog with the DNS challenge in Nginx Proxy Manager" loading="lazy" src="/en/posts/home-server/nginx-proxy-manager/letsEncryptDNS.png"></p>
<p>You just have to enter the API token or log in another way specific to your provider. NPM will then modify the DNS records to prove that you own the domain you have provided.</p>
<p>If you complete any of these methods successfully, you&rsquo;ll get your SSL certificate.</p>
<h2 id="setting-up-your-apps">Setting Up Your Apps</h2>
<p>Now we want to configure the Nginx Proxy Manager to relay traffic from specific domains to a specific TrueNAS Scale app. To do this, go to <code>Dashboard =&gt; Proxy Hosts</code>.</p>
<p><img alt="Screenshot of the proxy hosts overview page of Nginx Proxy Manager" loading="lazy" src="/en/posts/home-server/nginx-proxy-manager/npmProxyOverview.png"></p>
<p>Here, you’ll see an overview of all the hosts / apps you’ve configured. On the top right, you can add a new host.</p>
<p><img alt="New proxy hosts dialog in NPM" loading="lazy" src="/en/posts/home-server/nginx-proxy-manager/newProxyHost.png"></p>
<p>In this window, you can finally configure access to your app. You need to enter the domain where you want your app to be available, choose the scheme (http/https) your app runs on (probably http)and enter the port your application uses.</p>
<p>The hostname of your app is its internal DNS entry in the Kubernetes cluster. It usually follows this pattern: <code>&lt;app-name&gt;.ix-&lt;app-name&gt;.svc.cluster.local</code>. So, for Jellyfin, it would be: <code>jellyfin.ix-jellyfin.svc.cluster.local</code>.</p>
<p>You can find the correct port in the app&rsquo;s configuration or use the default port.</p>
<blockquote>
<p><strong>These are the values for your app running in TrueNAS Scale, not the values for accessing the app publicly!</strong></p>
</blockquote>
<p>I also recommend enabling <code>Websocket Support</code> and <code>Block Common Exploits</code>.</p>
<p>At this point, your app will be accessible via your domain, but one final step is needed to allow requests over HTTPS. You just have to go to the SSL tab and select the certificate we set up earlier.</p>
<p><img alt="SSL config for new NPM proxy host" loading="lazy" src="/en/posts/home-server/nginx-proxy-manager/npmProxySSL.png"></p>
<p>After selecting your certificate, I recommend enabling all the available options (<code>Force SSL</code>, <code>HTTP/2 Support</code>, <code>HSTS Enabled</code>, <code>HSTS Subdomains</code>).</p>
<p>Once you click save, the host is set upand your app should be accessible via your domain and the HTTPS protocol.</p>
<blockquote>
<p>If you want to point one domain to your Nginx Proxy Manager UI, you can just point it to <code>http://localhost:81</code>.</p>
</blockquote>
<h2 id="footnotes">Footnotes</h2>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>It took me a while to figure this out because my app only worked sometimes on certain devices. I had registered the IP under the server&rsquo;s networking, which caused some devices to work intermittently. Why it worked at all, I can’t say, but just don’t do it.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>This is needed if you want to use the internal Kubernetes DNS entries to refer to your apps.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>How I built my home server using TrueNAS Scale</title>
      <link>https://lna-dev.net/en/posts/home-server/truenas-scale/</link>
      <pubDate>Tue, 13 Aug 2024 22:02:10 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/home-server/truenas-scale/</guid>
      <description>Here you learn how to setup a home server using TrueNAS Scale and I show you how I did it.</description>
      <content:encoded><![CDATA[<p>This post is part two of a bigger series of blog posts about my home server and how you can make your own. In <a href="../advantages-of-a-home-server/">the first post</a> I describe why I wanted to have a home server and the next one will be about how to set up the Nginx Proxy Manager (NPM) without TrueCharts. You can find all posts under the <a href="../../../tags/home-server-project/">Home-Server Project</a> tag.</p>
<h2 id="hardware">Hardware</h2>
<p><img alt="Picture of my home server from the inside." loading="lazy" src="/en/posts/home-server/truenas-scale/server.jpg"></p>
<p>(Looks a bit messy, I know 😅 But I somehow like it 😜)</p>


<p><details >
  <summary markdown="span">Full specs</summary>
  <table>
  <thead>
      <tr>
          <th>Type</th>
          <th>Hardware</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CPU</td>
          <td>Intel i7</td>
      </tr>
      <tr>
          <td>RAM</td>
          <td>32 GB DDR3</td>
      </tr>
      <tr>
          <td>HDD</td>
          <td>5 x 6TB Seagate Ironwolf (For storage)</td>
      </tr>
      <tr>
          <td>SSD</td>
          <td>128 GB Samsung (For the OS)</td>
      </tr>
      <tr>
          <td>GPU</td>
          <td>NVIDIA GTX 970 (Can be used in some apps)</td>
      </tr>
  </tbody>
</table>

</details></p>

<p>As you can see, I am using an old PC as my server. This has the advantage that I only needed to buy the hard drives to support my storage needs, but the downside is that electricity consumption is rather high. Last time I measured with all my apps running, it consumed about 60W. A fresh build could be optimized for electricity consumption, which can save you a lot of money if you want the server to run 24/7. <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>I chose not to do a hardware RAID because I am using the ZFS file system. I will explain this in detail in <a href="#openzfs">a dedicated part</a>.</p>
<h3 id="ecc-memory">ECC memory?</h3>
<p>I unfortunately do not have ECC RAM, but if you can get it, I would recommend it. Error Correction Code (ECC) RAM is able to correct wrong bits, which makes the system more secure against some hardware attacks and especially more reliable. It is not a complete dealbreaker not having it, (especially because it&rsquo;s kind of rare in the consumer world at the moment) but having it is a nice addon.</p>
<h3 id="uninterrupted-power-supply-ups">Uninterrupted Power Supply (UPS)</h3>
<p>I bought a UPS to support my server in case of power outages. The UPS has a battery that can keep my server alive for about 15 minutes, then it gives a signal to the server, which is configured to shut down before the UPS battery runs out. This ensures that most power outages never cause a problem, as they are shorter than the battery time and if the power is out for a while the server shuts down gracefully.</p>
<h2 id="why-i-chose-truenas-scale">Why I chose TrueNAS Scale</h2>
<p>Now to, in my opinion, the most important part: The operating system. I chose TrueNAS Scale for a couple of reasons. First of all I wanted something that is <strong>Open Source</strong>. I also wanted to run apps on the system and definitely needed some sort of RAID / software RAID. TrueNAS Scale has it all. It is based on Linux and led by the company <a href="https://www.ixsystems.com/">iXsystems</a>. <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> For applications, it supports Docker and Kubernetes, as well as its own VMs. I am actually not really using the VMs yet, but I really like Kubernetes and also Docker. So this was great news for me. These technologies allow you to run basically any app you like on it because most apps are or can be containerized. In addition to this, the system uses OpenZFS as the file system, which is pretty cool in my opinion. We will take a look at it in the next part.</p>
<h3 id="raid">RAID</h3>
<p>As I previously mentioned, I am not using a classical RAID system. Instead the ZFS file system which is used by TrueNAS Scale, has something similar to a software RAID built into it.</p>
<p>But first of all, to bring everyone on the same level, I want to explain what RAID actually is. RAID stands for Redundant Array of Independent Disks and as the name suggests brings redundancy into data storage. There are several versions of RAID (0, 1, 5, 6 are the most important ones). RAID 0 just stripes the data across multiple disks and does not provide any redundancy. RAID 1 mirrors every disk on another one so half of the disks could fail without losing any data. The last two versions are the important ones for this blog post. They both work similarly: First the data gets striped like in RAID 0, but a parity part is also calculated, which can be used to recover the data if a drive fails. With RAID 5 you can lose one disk without losing data and with RAID 6 you can lose two disks without an issue. The disadvantage of RAID is that you need more disks because of the redundancy.</p>
<h3 id="openzfs">OpenZFS</h3>
<p>Now let&rsquo;s talk about ZFS, which is an awesome file system in my opinion. It provides a set of features that make it especially interesting for server operation and other systems where correct data storage is important. A few of its features are RAID-Z (which I&rsquo;ll discuss later), data compression, data deduplication, snapshots and self-healing. All of these features are very interesting for building a storage-focused server like the one I am building.</p>
<p>RAID-Z is the most important feature of ZFS in my opinion. It allows me to run the server without a physical RAID controller. It works a bit differently from RAID, which brings better performance and it also has the self-healing ability ZFS is known for, all while not needing a physical RAID controller anymore. This is actually only possible because there is no RAID controller and the file system can manage the disks directly. It is not recommended to use ZFS in addition to a hardware RAID because of that.</p>
<h3 id="setup-of-the-os">Setup of the OS</h3>
<p>The installation of the OS is pretty straightforward. You can follow the <a href="https://www.truenas.com/docs/scale/24.04/gettingstarted/install/installingscale/">installation guide</a> on the developer&rsquo;s website.</p>
<p><img alt="Dashboard of TrueNAS Scale" loading="lazy" src="/en/posts/home-server/truenas-scale/trueNasDashboard.png"></p>
<p>If you set everything up, this is how the web UI looks. Here you can control pretty much everything about the system graphically. In addition to the UI, I would recommend setting up an SSH connection so you can use the command line on your system. You can do that under <code>credentials =&gt; users =&gt; upload SSH key</code>.</p>
<p><img alt="Screenshot of the dataset tab" loading="lazy" src="/en/posts/home-server/truenas-scale/datasets.png"></p>
<p>Under datasets you can set up different folders with different permissions, purposes, shares and much more. You can decide if you want to encrypt your data (you want to 😉) and how you want to do it. There are two options. With the simplest option, a key gets generated and you just have to save it somewhere safe as a backup, while the server stores it securely. But if you are like me and a bit paranoid when it comes to disk encryption, you can also use a really strong password. The difference is that you have to enter this password after every restart. This provides the security that if your system is shut down, it is safe against a lot of hardware attacks.</p>
<h3 id="how-i-move-files">How I move files</h3>
<p>TrueNAS Scale provides you with a few kinds of file shares. I tried them all but did not really like any of them, which actually makes it a lot simpler for me. I am using SSH in my Gnome file explorer if I want to move files or I just do it over the command line. This works perfectly fine for me.</p>
<p>If you want to use shares, you are able to use SMB, NFS and iSCSI to do so. Just play around with it and see what you like most. For me, it&rsquo;s the good old SSH.</p>
<p><img alt="Screenshot of the shares tab of TrueNAS Scale" loading="lazy" src="/en/posts/home-server/truenas-scale/shares.png"></p>
<h2 id="what-apps-i-run">What apps I run</h2>
<p>Now to the most interesting part, in my opinion. I want to run apps that support me in my day-to-day life and provide value. This is why I do all of this.</p>
<h3 id="truecharts-and-why-i-do-not-use-them-anymore">TrueCharts and why I do not use them anymore</h3>
<p>But first of all, we need to talk about TrueCharts. They are a pretty big third-party app catalog or at least they were for a very long time. They now said that they are going to drop support for TrueNAS Scale because they claim iXsystems will remove Kubernetes from the OS in a future update. I am not really sure about that because I haven&rsquo;t read anything significant. But I think sticking with the original apps is not that bad either way. It’s a bit different to set up, but I will guide you through it here.</p>
<p>I also want to mention that I had a few problems using charts from TrueCharts. I do not want to blame them for it, but I think they are less stable than the original ones. But they have advantages like more settings and less performance overhead, so I think the guys from TrueCharts still did a really good job.</p>
<h3 id="install-an-app">Install an app</h3>
<p>I do not want to use file shares for everything. That is not very convenient. I want to use an app that works like all the other file cloud providers. This is why I host Nextcloud on my NAS. This is a really cool private cloud software that manages everything like files, contacts, calendars and the list goes on and on. I will talk a bit more about it in a future post. <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> But for now, let&rsquo;s take a look at how to install an app like that.</p>
<p><img alt="Screenshot of the TrueNAS Scale apps window" loading="lazy" src="/en/posts/home-server/truenas-scale/apps.png"></p>
<p>The process is actually pretty simple. You can just choose your app from the catalog, which you can open with the discover button.</p>
<p><img alt="Screenshot of the TrueNAS Scale apps catalog" loading="lazy" src="/en/posts/home-server/truenas-scale/app-catalog.png"></p>
<p>Here you can choose from many different apps or provide your own if you want to create something not in the default apps. If you have found an app you want to install, you can click on it and configure the app with everything the app allows you to change. Here the TrueCharts apps are a lot better because you can configure way more. But the default apps have a few settings as well and are much simpler. An important thing to keep in mind is that there is a limitation with the default apps, so you cannot use ports below 9000.</p>
<p><img alt="Screenshot of the TrueNAS Scale install dialog" loading="lazy" src="/en/posts/home-server/truenas-scale/install.png"></p>
<p>If you are finished with configuring, it is just as easy as pressing the install button. After a few minutes, the app is running and you can access it over the port you chose.</p>
<h2 id="the-end">The End</h2>
<p>You now know the basics of TrueNAS Scale. I hope you took a little bit of information from this post and I hope to see you in the next one of this series😄</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I thought about a couple of solutions that do not require the system to run all the time, but those are mostly hacks and not real solutions for me. For example, you could shut down the server overnight or wake it up using wake on LAN, but this is not a great fit for me.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>For those who are interested, iXsystems is a company based in the US and therefore a bit suboptimal because they are liable to US law, which is not ideal. But as I see it, they are not affected negatively at the moment.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>There are many apps I want to talk about. Examples are: Nextcloud, Jellyfin and Home Assistant. But I will do this another time in another post.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>Why I need a home server and you might too</title>
      <link>https://lna-dev.net/en/posts/home-server/advantages-of-a-home-server/</link>
      <pubDate>Mon, 29 Jul 2024 21:34:00 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/home-server/advantages-of-a-home-server/</guid>
      <description>In this post, I discuss the reasons why I decided to build a home server and talk about the advantages of having one.</description>
      <content:encoded><![CDATA[<p>First of all, this is the first post of a larger series of blog posts I will dedicate to building my home server. You can find all of them under the tag <a href="../../../tags/home-server-project/">Home-Server Project</a>.</p>
<p>There are many reasons someone would want to have a home server. In this post, I want to explain why I chose to build a home server and in the following ones, I will show you how I did it and how you can do it too.</p>
<h2 id="privacy-and-independence">Privacy and Independence</h2>
<p>For me, there are a few main reasons why I wanted to get started with self-hosting. I think my journey began about two years ago in 2022 when I first played around with a Synology NAS. This helped me a bit with some of the goals I had. Having a Synology NAS improved my independence, especially from the big tech nightmares and therefore also improved my privacy. But after a while, I felt that something wasn&rsquo;t quite right with this setup. First, the Synology software is not <strong>Open Source</strong> and therefore there are still <strong>privacy</strong> concerns. (I will cover why this is so important to me in a future post.) Also, Synology is a Taiwanese company and the situation with China was worrying me. (There would be a lot of bigger problems in tech, but it was still concerning.)</p>
<p>So the next step was building my own NAS / home server with custom <strong>Open Source</strong> software. I explain this in another post of this series. For now, let&rsquo;s just say I built my own server about a year ago in 2023 and now I have all my data at home in an environment I trust and am able to control fully. So I achieved a lot of independence from the shady big tech companies.</p>
<p><img alt="A meme about open source and closed source. Obviously always choosing open source." loading="lazy" src="/en/posts/home-server/advantages-of-a-home-server/openSourceMeme.jpg"></p>
<h2 id="cost">Cost</h2>
<p>Cost can be an advantage or disadvantage if you start to self-host. On one side, you have to pay for your hardware and the electricity your server consumes, but on the other side, you can cancel a couple of subscriptions which are also pretty expensive. For example, you are probably using some sort of cloud storage. (Or at least you should be using something like that in my opinion. <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> )</p>
<p>Having a bit of data in such storage can have high costs. For example, buying OneDrive, which is included in the Microsoft Personal subscription, costs about $6.99 a month.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> This price only includes 1TB. But if you have large amounts of data, this can get very expensive. To look at another example, Google&rsquo;s cloud storage costs 9.99€ a month for 2TB.<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> Other cloud storages, like a hosted Nextcloud, might even be more expensive.</p>
<p><img alt="Screenshot of the Google Drive prices" loading="lazy" src="/en/posts/home-server/advantages-of-a-home-server/google-costs.png"></p>
<p>You also have to keep in mind these are only costs for one service. On your server, you can host a couple of services you need and even share those and also the costs.</p>
<p>Because of all this, you save a bug in my opinion if you start self-hosting and replacing the services you pay for with your own server. But this can vary from person to person.</p>
<blockquote>
<p>Just to mention it: OneDrive and a lot of other big tech tools scan your files for illegal content and use filters that might report you to law enforcement. These tools are known to make mistakes and so an image of your child on the beach can lead to your Microsoft account being suspended in the best case and a meeting with the local police in the worst case. Something like that will never happen if you are self-hosting. Be aware!</p>
</blockquote>
<h2 id="knowledge">Knowledge</h2>
<p>Another good point for having a home lab is all the knowledge you gain from it. You have to think about the hardware, set everything up physically, learn about RAID, about the operating systems and the software you are running, not to mention security and setting up a safe environment. There is a lot to learn on this journey!</p>
<p>And the good thing here is this is not a one-time thing. You can learn a lot as time goes on. Every now and then, if you feel motivated, you can play around with some settings or try something new.</p>
<p>This knowledge can also pay off in your job as you gain experience you maybe wouldn&rsquo;t have otherwise. Personally speaking, I learned most of the things I know about IT, especially programming and DevOps, while building my own things. It is just way more motivating building the things you want than just learning by doing exercises you do not care about.</p>
<h2 id="helping-others">Helping others</h2>
<p>Last but not least, I want to mention a small point I think is notable. If you are building all of this for yourself, it is really easy to give a few other people access to the services you are running. So you can help out your friends and family to have better privacy and generally speaking all of the benefits I was talking about. <sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Of course you should not use services that are not end-to-end encrypted or run by one of the big tech companies. But a service like that provides great value, especially if you self-host it!&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Data from <a href="https://www.microsoft.com/en-us/microsoft-365/onedrive/compare-onedrive-plans">https://www.microsoft.com/en-us/microsoft-365/onedrive/compare-onedrive-plans</a> retrieved on July 24.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Data from <a href="https://one.google.com/about/plans?g1_landing_page=0&amp;hl=en">https://one.google.com/about/plans?g1_landing_page=0&amp;hl=en</a> retrieved on July 24.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>But be careful with that because you gain a lot of power if you do that, as you may get access to sensitive information. So keep in mind: With great power comes great responsibility^^&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>How to automate Pixelfed posts</title>
      <link>https://lna-dev.net/en/posts/projects/pixelfed-automation/</link>
      <pubDate>Sun, 05 May 2024 22:30:45 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/projects/pixelfed-automation/</guid>
      <description>I needed to automate my posts to Pixelfed. In this post, I describe how I achieved that.</description>
      <content:encoded><![CDATA[<h2 id="my-problem-with-posting-to-pixelfed">My problem with posting to Pixelfed</h2>
<p>I started with Mastodon as my first account in the <a href="posts/media/fediverse/">Fediverse</a>. After a while, I found out about Pixelfed and created my account there. I kept using it for a while and even posted from time to time. My problem with posting to Pixelfed was the lack of a scheduler. I want to post regularly, but I do not want to login every day and make a post manually. This feature was even announced but never finished. Even today, this is not implemented. There is also a <a href="https://github.com/pixelfed/pixelfed/issues/2872">GitHub issue</a> discussing this if your interest goes deeper. In the following post by Pixelfed, you can see the announcement.


<iframe
  src="https://mastodon.social/@pixelfed/107574719894032457/embed"
  class="mastodon-embed"
  style="max-width: 100%; border: 0; margin-top: 15px; margin-bottom: 10px"
  width="100%"
  allowfullscreen="allowfullscreen"
></iframe>
<script src="https://mastodon.social/embed.js" async="async"></script>
</p>
<p>There are some post schedulers for Mastodon out there. I tried using them with Pixelfed, but nothing worked properly. I even tried the client-side automatic upload feature of <a href="https://fedilab.app/">Fedilab</a>, which posted immediately instead of at the given time. This surely was a bug I could have reported or fixed myself (because Fedilab is <a href="tags/open-source/">FOSS</a>). But I wanted to make things more automatic anyway.</p>
<p>I do not want to complain about this. It is totally fine. There are a lot of other things that need to be done in Pixelfed and the Fediverse as a whole. But for me, it meant I needed to implement something on my own.</p>
<h2 id="requirements">Requirements</h2>
<p>So to solve this, I needed something which can somehow talk with my Pixelfed server and make a post there. I also needed a way to know which posts I want to make. This information must include some metadata like the date taken and definitely an alt text and tags to display in Pixelfed. Another thing to keep in mind is that I needed something which &ldquo;remembers&rdquo; the state of my postings. Basically, a list in which each post gets added if it is uploaded successfully. And finally, I needed some sort of mechanism which can schedule the execution of the code.</p>
<h2 id="the-automation">The automation</h2>
<h3 id="rss-feed">RSS feed</h3>
<p>So first of all, I have a <a href="https://lna-dev.net/en/gallery">photography page</a> which has an RSS feed. I modified the feed of the page to contain an entry per image I have uploaded there. I am also using Hugo there. Therefore I wrote this little Hugo <code>rss.xml</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span>{{- $authorEmail := site.Params.author.email -}}
</span></span><span style="display:flex;"><span>{{- $authorName := site.Params.author.name }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>{{- $pctx := . -}}
</span></span><span style="display:flex;"><span>{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
</span></span><span style="display:flex;"><span>{{- $pages := slice -}}
</span></span><span style="display:flex;"><span>{{- if or $.IsHome $.IsSection -}}
</span></span><span style="display:flex;"><span>{{- $pages = where $pctx.RegularPages &#34;Params.private&#34; &#34;ne&#34; true }}
</span></span><span style="display:flex;"><span>{{- else -}}
</span></span><span style="display:flex;"><span>{{- $pages = where $pctx.Pages &#34;Params.private&#34; &#34;ne&#34; true }}
</span></span><span style="display:flex;"><span>{{- end -}}
</span></span><span style="display:flex;"><span>{{- $pages = where $pages &#34;Params.rss_ignore&#34; &#34;ne&#34; true -}}
</span></span><span style="display:flex;"><span>{{- $limit := .Site.Config.Services.RSS.Limit -}}
</span></span><span style="display:flex;"><span>{{- if ge $limit 1 -}}
</span></span><span style="display:flex;"><span>{{- $pages = $pages | first $limit -}}
</span></span><span style="display:flex;"><span>{{- end -}}
</span></span><span style="display:flex;"><span>{{- printf &#34;<span style="color:#75715e">&lt;?xml version=\&#34;1.0\&#34; encoding=\&#34;utf-8\&#34; standalone=\&#34;yes\&#34;?&gt;</span>&#34; | safeHTML }}
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;rss</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">&#34;2.0&#34;</span> <span style="color:#a6e22e">xmlns:media=</span><span style="color:#e6db74">&#34;http://search.yahoo.com/mrss/&#34;</span> <span style="color:#a6e22e">xmlns:atom=</span><span style="color:#e6db74">&#34;http://www.w3.org/2005/Atom&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;channel&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;title&gt;</span>{{ if eq  .Title  .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}<span style="color:#f92672">&lt;/title&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;link&gt;</span>{{ .Permalink }}<span style="color:#f92672">&lt;/link&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;description&gt;</span>Recent content {{ if ne  .Title  .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}<span style="color:#f92672">&lt;/description&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;generator&gt;</span>Hugo -- gohugo.io<span style="color:#f92672">&lt;/generator&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;language&gt;</span>{{ site.Language.LanguageCode }}<span style="color:#f92672">&lt;/language&gt;</span>{{ with site.Params.author.email }}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;managingEditor&gt;</span>{{.}}{{ with $authorName }} ({{ . }}){{ end }}<span style="color:#f92672">&lt;/managingEditor&gt;</span>{{ end }}{{ with $authorEmail }}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;webMaster&gt;</span>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}<span style="color:#f92672">&lt;/webMaster&gt;</span>{{ end }}{{ with .Site.Copyright }}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;copyright&gt;</span>{{.}}<span style="color:#f92672">&lt;/copyright&gt;</span>{{end}}{{ if not .Date.IsZero }}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;lastBuildDate&gt;</span>{{ (index $pages.ByLastmod.Reverse 0).Lastmod.Format &#34;Mon, 02 Jan 2006 15:04:05 -0700&#34; | safeHTML }}<span style="color:#f92672">&lt;/lastBuildDate&gt;</span>{{ end }}
</span></span><span style="display:flex;"><span>    {{- with .OutputFormats.Get &#34;RSS&#34; -}}
</span></span><span style="display:flex;"><span>    {{ printf &#34;<span style="color:#f92672">&lt;atom:link</span> <span style="color:#a6e22e">href=</span><span style="color:#e6db74">%q</span> <span style="color:#a6e22e">rel=</span><span style="color:#e6db74">\&#34;self\&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">%q</span> <span style="color:#f92672">/&gt;</span>&#34; .Permalink .MediaType | safeHTML }}
</span></span><span style="display:flex;"><span>    {{- end -}}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    {{- range $pages }}
</span></span><span style="display:flex;"><span>    {{- if not (in .Path &#34;/archive/&#34;) -}}
</span></span><span style="display:flex;"><span>    {{ range .Resources.ByType &#34;image&#34; }}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;item&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;title&gt;</span>{{ .Name }}<span style="color:#f92672">&lt;/title&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;link&gt;</span>{{ .Permalink }}<span style="color:#f92672">&lt;/link&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;pubDate&gt;</span>{{ .Exif.Date.Format &#34;Mon, 02 Jan 2006 15:04:05 -0700&#34; | safeHTML }}<span style="color:#f92672">&lt;/pubDate&gt;</span>
</span></span><span style="display:flex;"><span>      {{- with $authorEmail }}<span style="color:#f92672">&lt;author&gt;</span>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}<span style="color:#f92672">&lt;/author&gt;</span>{{ end }}
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;guid&gt;</span>{{ .Permalink }}<span style="color:#f92672">&lt;/guid&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;media:content</span> <span style="color:#a6e22e">url=</span><span style="color:#e6db74">&#34;{{ .Permalink }}&#34;</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;image/jpeg&#34;</span><span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;description&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;img</span> <span style="color:#a6e22e">src=</span><span style="color:#e6db74">&#34;{{ .Permalink }}&#34;</span> <span style="color:#a6e22e">alt=</span><span style="color:#e6db74">&#34;{{ .Params.Alt }}&#34;</span><span style="color:#f92672">/&gt;</span>          
</span></span><span style="display:flex;"><span>        {{- if ne .Title .Name -}}
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;p&gt;</span> {{ .Title }} <span style="color:#f92672">&lt;/p&gt;</span>
</span></span><span style="display:flex;"><span>        {{ end }}
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;/description&gt;</span>
</span></span><span style="display:flex;"><span>      {{ range .Params.Tags }}
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;category&gt;</span>{{ . }}<span style="color:#f92672">&lt;/category&gt;</span>        
</span></span><span style="display:flex;"><span>      {{- end }}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/item&gt;</span>
</span></span><span style="display:flex;"><span>    {{- end -}}
</span></span><span style="display:flex;"><span>    {{- end -}}
</span></span><span style="display:flex;"><span>    {{ end }}
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/channel&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/rss&gt;</span>
</span></span></code></pre></div><p>That done, I managed the metadata of all my images in the Hugo config file and wrote an alt text and tags for all my images. (Must admit&hellip; an LLM helped me a bit with that. This made the process a lot faster, but the quality is also just okay.)</p>
<h3 id="my-personal-api">My personal API</h3>
<p>I even wanted to create an API for this page before this little project. So I created a new API written in <strong>go</strong>. There I implemented a few endpoints which can save and read a list of filenames from and to MongoDB. I am using MongoDB because of the easy use and their free cloud storage. I will probably change that to something self-hosted in the future, but for now, it does its job. (I am running a K8s cluster with broken local storage right now. Need to fix this first.)<br>
Oh, and I also made the <code>POST</code> endpoint protected by an API key so no one unauthorized can alter my data😉</p>
<p>Now having an API which knows which images have already been posted, I could continue to the script interacting with Pixelfed.</p>
<h3 id="the-pixelfed-script">The Pixelfed Script</h3>
<p>For writing the script, I went with Python. I am having mixed feelings about Python. On the one hand, I really dislike the way it is handling types (it doesn&rsquo;t lol) but on the other hand, you can quickly make basic things work. So I went the (for me) experimental way and chose Python as my way to go.</p>
<p>I created this basic script which is handling all the stuff needed to create the post and make all the API and RSS handling. I go into detail on some of the methods and bits of code I find most important but I have the full code provided below if you are interested.</p>


<p><details >
  <summary markdown="span">The full code</summary>
  <p>Alternatively to this code block, there is an up-to-date <a href="https://github.com/LNA-DEV/Autouploader">Repo</a> on GitHub.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> io <span style="color:#f92672">import</span> BytesIO
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> re
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> feedparser
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> datetime
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> random
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> requests
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>PIXELFED_INSTANCE_URL <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https://pixelfed.de&#39;</span>
</span></span><span style="display:flex;"><span>PAT <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;PIXELFED_PAT&#39;</span>)
</span></span><span style="display:flex;"><span>API_KEY <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;API_KEY&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Function to filter entries based on the name list</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">filter_entries</span>(entries, name_list):
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Temp skips (for example if this image does not fit currently)</span>
</span></span><span style="display:flex;"><span>    name_list<span style="color:#f92672">.</span>append(<span style="color:#e6db74">&#34;P1002496.JPG&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> [entry <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> entries <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>title <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> name_list]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_already_uploaded_items</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;https://api.lna-dev.net/autouploader/pixelfed&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>            string_list <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>json()
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> string_list
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Failed to fetch data from API. Status code: </span><span style="color:#e6db74">{</span>response<span style="color:#f92672">.</span>status_code<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;An error occurred: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">published_entry</span>(entry_name):
</span></span><span style="display:flex;"><span>    requests<span style="color:#f92672">.</span>post(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://api.lna-dev.net/autouploader/pixelfed?item=</span><span style="color:#e6db74">{</span>entry_name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>, headers<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;Authorization&#34;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;ApiKey </span><span style="color:#e6db74">{</span>API_KEY<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">download_image</span>(image_url):
</span></span><span style="display:flex;"><span>    response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(image_url)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> BytesIO(response<span style="color:#f92672">.</span>content)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Failed to download image!&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_entry</span>(entry):
</span></span><span style="display:flex;"><span>    caption <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;More at https://lna-dev.net/en/gallery</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> entry<span style="color:#f92672">.</span>tags:
</span></span><span style="display:flex;"><span>        caption <span style="color:#f92672">+=</span> <span style="color:#e6db74">&#39;#&#39;</span> <span style="color:#f92672">+</span> element<span style="color:#f92672">.</span>term <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34; &#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    mediaResponse <span style="color:#f92672">=</span> upload_media(entry)
</span></span><span style="display:flex;"><span>    publish_post(caption, mediaResponse)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    published_entry(entry<span style="color:#f92672">.</span>title)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">upload_media</span>(entry):
</span></span><span style="display:flex;"><span>    media_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>PIXELFED_INSTANCE_URL<span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/media&#39;</span>
</span></span><span style="display:flex;"><span>    headers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Authorization&#39;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Bearer </span><span style="color:#e6db74">{</span>PAT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Accept&#39;</span>: <span style="color:#e6db74">&#39;application/json&#39;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    files <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;file&#39;</span>: download_image(entry<span style="color:#f92672">.</span>link)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    data <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;description&#39;</span>: re<span style="color:#f92672">.</span>search(<span style="color:#e6db74">&#39;alt=&#34;(.*?)&#34;&#39;</span>, entry<span style="color:#f92672">.</span>summary)<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>post(media_url, headers<span style="color:#f92672">=</span>headers, files<span style="color:#f92672">=</span>files, data<span style="color:#f92672">=</span>data)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> response<span style="color:#f92672">.</span>json()[<span style="color:#e6db74">&#39;id&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Failed to upload media.&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_post</span>(caption, media_id):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> caption<span style="color:#f92672">.</span>strip():
</span></span><span style="display:flex;"><span>        post_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>PIXELFED_INSTANCE_URL<span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/statuses&#39;</span>
</span></span><span style="display:flex;"><span>        headers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Authorization&#39;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Bearer </span><span style="color:#e6db74">{</span>PAT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Accept&#39;</span>: <span style="color:#e6db74">&#39;application/json&#39;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        data <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;status&#39;</span>: caption,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;media_ids[]&#39;</span>: media_id
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>post(post_url, headers<span style="color:#f92672">=</span>headers, data<span style="color:#f92672">=</span>data)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">&#34;Post published successfully!&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">&#34;Failed to publish post!&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Caption cannot be empty.&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Parse the RSS feed</span>
</span></span><span style="display:flex;"><span>feed_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https://lna-dev.net/en/gallery/index.xml&#39;</span>
</span></span><span style="display:flex;"><span>feed <span style="color:#f92672">=</span> feedparser<span style="color:#f92672">.</span>parse(feed_url)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Filter out entries with specific names</span>
</span></span><span style="display:flex;"><span>specific_names <span style="color:#f92672">=</span> get_already_uploaded_items()
</span></span><span style="display:flex;"><span>filtered_entries <span style="color:#f92672">=</span> filter_entries(feed<span style="color:#f92672">.</span>entries, specific_names)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> filtered_entries:
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;No entries available after filtering.&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Calculate time differences considering only month, day, hour, minute, and second</span>
</span></span><span style="display:flex;"><span>    current_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>now()
</span></span><span style="display:flex;"><span>    closest_entry <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    skipped_entries <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    min_difference <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> filtered_entries:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">or</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>            skipped_entries<span style="color:#f92672">.</span>append(entry)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>  <span style="color:#75715e"># Skip entries with invalid year</span>
</span></span><span style="display:flex;"><span>        temp <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>mktime(entry<span style="color:#f92672">.</span>published_parsed)
</span></span><span style="display:flex;"><span>        published_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>fromtimestamp(temp)
</span></span><span style="display:flex;"><span>        difference <span style="color:#f92672">=</span> abs(current_time<span style="color:#f92672">.</span>replace(year<span style="color:#f92672">=</span>published_time<span style="color:#f92672">.</span>year, tzinfo<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>) <span style="color:#f92672">-</span> published_time)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> min_difference <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span> <span style="color:#f92672">or</span> difference <span style="color:#f92672">&lt;</span> min_difference:
</span></span><span style="display:flex;"><span>            min_difference <span style="color:#f92672">=</span> difference
</span></span><span style="display:flex;"><span>            closest_entry <span style="color:#f92672">=</span> entry
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> closest_entry <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;No valid entries available after filtering.&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Get all entries published at the same time as the closest entry</span>
</span></span><span style="display:flex;"><span>        closest_entries <span style="color:#f92672">=</span> [entry <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> filtered_entries <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>published <span style="color:#f92672">==</span> closest_entry<span style="color:#f92672">.</span>published]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> skipped_entries:
</span></span><span style="display:flex;"><span>            closest_entries<span style="color:#f92672">.</span>append(element)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Select a random entry from the closest entries</span>
</span></span><span style="display:flex;"><span>        random_entry <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>choice(closest_entries)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Print the selected entry</span>
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Random entry closest to the current date/time (ignoring year):&#34;</span>)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Title:&#34;</span>, random_entry<span style="color:#f92672">.</span>title)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;URL:&#34;</span>, random_entry<span style="color:#f92672">.</span>link)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Published Date:&#34;</span>, random_entry<span style="color:#f92672">.</span>published)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        publish_entry(random_entry)
</span></span></code></pre></div>
</details></p>

<p>So let&rsquo;s start with the retrieval of the RSS data. To simplify this for me, I used the <code>feedparser</code> library here. With that, I have access to all the data of the RSS feed in a better-structured way.</p>
<p>With this simple API call, I retrieve the already uploaded posts from my personal API I mentioned above. With this data, I can filter out any entries in the RSS feed which I don&rsquo;t need to upload anymore.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_already_uploaded_items</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;https://api.lna-dev.net/autouploader/pixelfed&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>            string_list <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>json()
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> string_list
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Failed to fetch data from API. Status code: </span><span style="color:#e6db74">{</span>response<span style="color:#f92672">.</span>status_code<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;An error occurred: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span></code></pre></div><p>After having a list of possible posts, I need the script to decide which post to post next. For that, I am calculating which posts are nearest to the current day and month without taking the year into the calculation.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> filtered_entries:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">or</span> entry<span style="color:#f92672">.</span>published_parsed<span style="color:#f92672">.</span>tm_year <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>        skipped_entries<span style="color:#f92672">.</span>append(entry)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">continue</span>  <span style="color:#75715e"># Skip entries with invalid year</span>
</span></span><span style="display:flex;"><span>    temp <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>mktime(entry<span style="color:#f92672">.</span>published_parsed)
</span></span><span style="display:flex;"><span>    published_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>fromtimestamp(temp)
</span></span><span style="display:flex;"><span>    difference <span style="color:#f92672">=</span> abs(current_time<span style="color:#f92672">.</span>replace(year<span style="color:#f92672">=</span>published_time<span style="color:#f92672">.</span>year, tzinfo<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>) <span style="color:#f92672">-</span> published_time)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> min_difference <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span> <span style="color:#f92672">or</span> difference <span style="color:#f92672">&lt;</span> min_difference:
</span></span><span style="display:flex;"><span>        min_difference <span style="color:#f92672">=</span> difference
</span></span><span style="display:flex;"><span>        closest_entry <span style="color:#f92672">=</span> entry
</span></span></code></pre></div><p>Then I select one of the closest entries randomly. I need to do this because if I would have taken many photos on one day there could be multiple items close to the current date.</p>
<p>Now we are ready for creating the post. Therefore, I am now preparing the caption which is included in the post.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_entry</span>(entry):
</span></span><span style="display:flex;"><span>    caption <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;More at https://lna-dev.net/en/gallery</span><span style="color:#ae81ff">\n\n</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> element <span style="color:#f92672">in</span> entry<span style="color:#f92672">.</span>tags:
</span></span><span style="display:flex;"><span>        caption <span style="color:#f92672">+=</span> <span style="color:#e6db74">&#39;#&#39;</span> <span style="color:#f92672">+</span> element<span style="color:#f92672">.</span>term <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34; &#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    mediaResponse <span style="color:#f92672">=</span> upload_media(entry)
</span></span><span style="display:flex;"><span>    publish_post(caption, mediaResponse)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    published_entry(entry<span style="color:#f92672">.</span>title)
</span></span></code></pre></div><p>Now to the trickiest bit: The actual posting. This was especially interesting because at the time I created this script there was a lack of documentation and I needed to somehow figure this out on my own. I also didn&rsquo;t find many examples searching the internet. The best I could do was look into the Mastodon documentation (because Pixelfeds API is similar) and figure it out on my own.</p>
<p>There are two important things here. First, you have to upload the media first before you can make the post which then simply contains the id of the media you uploaded beforehand. And second, you need to create a PAT (personal access token) and provide it via the bearer syntax to the API.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">upload_media</span>(entry):
</span></span><span style="display:flex;"><span>    media_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>PIXELFED_INSTANCE_URL<span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/media&#39;</span>
</span></span><span style="display:flex;"><span>    headers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Authorization&#39;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Bearer </span><span style="color:#e6db74">{</span>PAT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;Accept&#39;</span>: <span style="color:#e6db74">&#39;application/json&#39;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    files <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;file&#39;</span>: download_image(entry<span style="color:#f92672">.</span>link)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    data <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;description&#39;</span>: re<span style="color:#f92672">.</span>search(<span style="color:#e6db74">&#39;alt=&#34;(.*?)&#34;&#39;</span>, entry<span style="color:#f92672">.</span>summary)<span style="color:#f92672">.</span>group(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>post(media_url, headers<span style="color:#f92672">=</span>headers, files<span style="color:#f92672">=</span>files, data<span style="color:#f92672">=</span>data)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> response<span style="color:#f92672">.</span>json()[<span style="color:#e6db74">&#39;id&#39;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Failed to upload media.&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">publish_post</span>(caption, media_id):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> caption<span style="color:#f92672">.</span>strip():
</span></span><span style="display:flex;"><span>        post_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;</span><span style="color:#e6db74">{</span>PIXELFED_INSTANCE_URL<span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/statuses&#39;</span>
</span></span><span style="display:flex;"><span>        headers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Authorization&#39;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Bearer </span><span style="color:#e6db74">{</span>PAT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Accept&#39;</span>: <span style="color:#e6db74">&#39;application/json&#39;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        data <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;status&#39;</span>: caption,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;media_ids[]&#39;</span>: media_id
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>post(post_url, headers<span style="color:#f92672">=</span>headers, data<span style="color:#f92672">=</span>data)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">&#34;Post published successfully!&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">&#34;Failed to publish post!&#34;</span>)
</span></span><span style="display:flex;"><span>            sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;Caption cannot be empty.&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span></code></pre></div><p>That done, I made a final API request to my personal API noting that I uploaded the image.</p>
<h3 id="the-schedule">The schedule</h3>
<p>This part was fairly easy because I was already using a <a href="/tags/Kubernetes">Kubernetes</a> cluster. I just needed to create a cronjob running each day and that&rsquo;s it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">batch/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CronJob</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pixelfed-autoupload</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">schedule</span>: <span style="color:#e6db74">&#34;0 16 * * *&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">jobTemplate</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pixelfed-autoupload</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">image</span>: <span style="color:#ae81ff">lnadev/pixelfed-autoupload:{{ .Values.autoupload.pixelfed.version }}</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">imagePullPolicy</span>: <span style="color:#ae81ff">Always</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;128Mi&#34;</span> 
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;500m&#34;</span> <span style="color:#75715e"># Don&#39;t use CPU limits in K8s but that is another topic...</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>                - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">PIXELFED_PAT</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">pixelfed</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#f92672">key</span>: <span style="color:#ae81ff">pat</span>
</span></span><span style="display:flex;"><span>                - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">API_KEY</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">personal-api-secret</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#f92672">key</span>: <span style="color:#ae81ff">apikey</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">restartPolicy</span>: <span style="color:#ae81ff">Never</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">backoffLimit</span>: <span style="color:#ae81ff">0</span>
</span></span></code></pre></div><p>One thing I had to learn here is the following: Setting the <code>restartPolicy</code> to <code>never</code> will not prevent the cronjob from retrying. You need to also set the <code>backoffLimit</code> to <code>0</code> to achieve the desired behavior. (This is because those two settings change different retry mechanisms you both want to disable.)</p>
]]></content:encoded>
    </item>
    <item>
      <title>What is the Fediverse?</title>
      <link>https://lna-dev.net/en/posts/media/fediverse/</link>
      <pubDate>Sat, 27 Jan 2024 20:45:02 +0100</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/media/fediverse/</guid>
      <description>Deep dive into the really social network.</description>
      <content:encoded><![CDATA[<figure class="img-right">
    <img loading="lazy" src="./fediverse.svg"
         alt="The Fediverse-Logo"/> 
</figure>

<p>In this post, I want to explain what the Fediverse is and why you should join it. I talk very little about the technical stuff, but this post is intended to be understood by anyone.</p>
<p>Another post for the really technical topics is planned for the future.</p>
<h2 id="where-does-the-name-come-from">Where does the name come from?</h2>
<p>The name is a portmanteau of federation and universe. So the Fediverse is a federated universe.</p>
<p>This name does represent it pretty well. The Fediverse is decentralized web of social media platforms all connected to each other. Anyone can connect with anybody else, even if they are using completely different services. You can imagine it like email. Every user has an email address by a specific service, but they can write with anyone they want, not just with users of the same service.</p>
<p>For those who are familiar with the classic social networks: Imagine using Twitter and seeing Instagram or Facebook posts in your timeline, just like they would all be using Twitter. But that&rsquo;s not all. There also could be YouTube videos or anything else. You decide. That is the beauty of the Fediverse.</p>
<h2 id="why-is-the-fediverse-so-important">Why is the Fediverse so important?</h2>
<h3 id="decentralization">Decentralization</h3>
<p>The old social media platforms are owned by companies. Not so is the Fediverse. First of all, there are many services in the Fediverse, so the whole thing cannot be owned. Just like the internet. Therefore, nobody can force users into something or exploit their market dominance, as the current big players do.</p>
<p>The creators of the Fediverse platform Peertube explained it in a straightforward way. Here is their explanation. (This video is by the way hosted on Peertube)</p>
<div style="position: relative; padding-bottom: 56.25%; margin-bottom: 1rem; height: 0; overflow: hidden;">
<iframe sandbox="allow-same-origin allow-scripts allow-popups" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"
    src="https://framatube.org/videos/embed/9dRFC6Ya11NCVeYKn8ZhiD?title=0&warningTitle=0&peertubeLink=0" title="Peertube Video" allowfullscreen>
</iframe>
</div>
<h3 id="free-and-open-source">Free and Open Source</h3>
<p>But even the producers of Fediverse software are very different. It mainly consists of FOSS (Free and Open Source Software), which means the software is genuinely free and independent developers can modify it and help to improve it. FOSS is incredibly important and I will cover its numerous advantages in a future post.</p>
<p>Because of this freedom, making money is more challenging. However, the user doesn&rsquo;t get treated like trash, as the user contributes to the Fediverse through donations. Also, there are no ads because of that.</p>
<p>So, the software producers of the Fediverse are primarily volunteers or non-profit organizations. Both aim to make the internet a better place and not just want to make a bug.</p>
<p>Moreover, it&rsquo;s not just the software producers who act fairly, the people who run the servers are mainly volunteers, organizations, or even states!</p>
<h3 id="the-force-of-the-masses">The force of the masses</h3>
<p>Because you can freely choose your service in the Fediverse, no one gets excluded from society anymore. I find this to be a significant issue with classical social media platforms. I am not using any of these because of all the bad things they do. As a result, I sometimes feel a bit excluded in specific contexts.</p>
<p>This is extremely important when it comes to messengers. Not using the specific service that is popular in your region can have a significant impact. Personally, I stopped using WhatsApp for around a year. In Germany, where I&rsquo;m from, it is used nearly without exception. A few people have <a href="https://www.signal.org/">Signal</a> or <a href="https://threema.ch/en">Threema</a>, but that&rsquo;s about it. Not having WhatsApp did affect my day-to-day life significantly. While I thought that using other services might encourage a few people to switch, it did only to some extent. Not everyone made the transition and some friends or family members remained exclusively on WhatsApp. Beyond the challenge of communicating with those you know, like and love, there are difficulties in meeting new people. I&rsquo;ve had experiences where some form negative opinions if you don&rsquo;t use the service that is common. Moreover, the barrier of contacting someone is much higher if they need to install a new app and you cannot contact them because you don&rsquo;t have the right messenger.</p>
<p>While a social media platform is not a messenger, the effects are quite similar: <strong>The people using the big service essentially force you to use it too!</strong></p>
<h2 id="current-culture-in-the-fediverse">Current culture in the Fediverse</h2>
<p>The current culture in the Fediverse is a significant positive aspect. People in this community are exceptionally friendly compared to traditional social networks. Moreover, the Fediverse is highly inclusive. For instance, if you post an image without an alt text (allowing blind people to understand what the image is about), someone will certainly remind you to add it. This inclusivity is crucial to the community, emphasizing that everyone should have the opportunity to participate.</p>
<p>There&rsquo;s also a good amount of instances for LGBTQ+ to create safe spaces for those people. I consistently see <a href="https://www.fediverse.to/search/?category=lgbt">different instances</a> with this objective.</p>
<p>Of course, this depends on which edge of the Fediverse you are at. But overall, I think there are many minorities in the Fediverse who are not as represented elsewhere. I recall a poll someone conducted with around 11,000 votes, asking how many queer people there are in the Fediverse. The result was 60% not queer and 40% queer. These are solid numbers. Of course, this result is biased in some way because of who the poll maker is and who reposts it. But with this number of votes, it definitely gives a good hint.</p>

    <iframe title="Hypothesis: queers make up roughly or more than 50% of the active people on Fedi." src="https://fosstodon.org/@writeblankspace/111744426795021145/embed" class="mastodon-embed" style="max-width: 100%; border: 0;" width="100%" allowfullscreen="allowfullscreen"></iframe><script src="https://fosstodon.org/embed.js" async="async"></script>

    <br>
    <br>

<p>If someone wants to create artistic content that goes a bit beyond the guidelines of platforms like Instagram, it is acceptable in the Fediverse. You can choose an instance where the rules align with your preferences and create your content there. While most instances may require a content warning for specific posts, you are free to express yourself as you like.</p>
<p>Also, there is no need to upload regularly or avoid using specific words to please the algorithm. There is nothing like that here. There are no ads and therefore no advertisers which do not want you to say something &ldquo;bad&rdquo;. You can handle things how you and your instance want them to be.</p>
<p>Another immensely beneficial aspect here is that the moderator count per user is relatively high. This is because each instance is responsible for its moderation. As a result, every instance manages its part, leading to an overall high count of moderators. Moreover, if an instance becomes unmoderated, other servers will block them, ensuring a clean network.</p>
<p>But, after all, the Fediverse is currently a big home for people who are interested in technology. It is relatively new tech and because of that, there are a lot of nerds. Which is great!</p>
<p>Of course, these are not all groups of people, but I hope I could provide you with a bit of an overview of the kinds of people currently in the Fediverse. This may change as new people join us, but at the moment, this is how I perceive the Fediverse. Let&rsquo;s hope we can maintain this diversity for the future.</p>
<h3 id="who-is-in-the-fediverse">Who is in the Fediverse?</h3>
<p>For example, nearly all German institutions have an account on their own <a href="https://social.bund.de/about">server of the state</a>. Additionally, the public broadcasting providers <a href="https://ard.social/about">ARD</a> and <a href="https://zdf.social/about">ZDF</a> have their own instances.</p>
<p>I just saw that the <a href="https://social.overheid.nl/about">Netherlands</a> also have a state owned Mastodon instance. I really like the idea of nations backing the network and hopefully the values, this network comes with.</p>
<p>Mozilla also created its own instance available to the public: <a href="https://mozilla.social/about">Mozilla Social</a>. However, there are another 26,000 servers waiting for you to explore them.</p>
<p>Currently there are around 10,000,000 users in the Fediverse with a total of about 1,200,000 of them being active in the last month. So you hopefully you find someone to connect with.</p>
<p>A good overview of instances and the biggest users can be found at <a href="https://fedidb.org">FediDB</a>.</p>
<h2 id="platforms">Platforms</h2>
<p>In this section, I want to showcase a few of the most important services of the Fediverse. <strong>But there are many more to be explored!</strong></p>
<p>This image provides a quick overview of the different categories and services.</p>
<figure>
    <img loading="lazy" src="./fediverse-branches.webp"
         alt="This image shows a tree with the different branches of the Fediverse. It shows a few services like Mastodon, Pleroma, Pixelfed, Peertube, Castopod, Mobilizon and even more."/> <figcaption>
            <p><a href="https://axbom.com/">🄯 CC-BY-SA 3.0 by Per Axbom</a></p>
        </figcaption>
</figure>

<h3 id="mastodon">Mastodon</h3>
<p>Mastodon is the biggest and also the most developed platform of the Fediverse. It is a microblogging service. The company behind it is a German non-profit company (gGmbH). It is comparable with Twitter and gained a lot of attraction during <a href="https://www.theguardian.com/technology/2023/oct/27/elon-musk-x-twitter-takeover-revenue-users-advertising">the catastrophic takeover of Elon Musk</a>. At that time, there was a huge mass of people moving to Mastodon from Twitter.</p>
<p>After these events, the &ldquo;freed&rdquo; platform attracted <a href="https://www.theguardian.com/world/2023/jun/03/twitter-conservative-media-elon-musk-ron-desantis">even more extreme right-wing individuals</a>. Additionally, many <a href="https://www.theguardian.com/technology/2022/dec/16/twitter-elon-musk-suspension-journalists-sets-dangerous-precedent-un-warns">journalists face occasional blocks</a>. If you truly seek a free platform, consider joining Mastodon: <a href="https://joinmastodon.org">https://joinmastodon.org</a>.
(They also have a really fancy website: Check it out!)</p>
<figure>
    <img loading="lazy" src="./twitter-x.jpg"
         alt="This image shows a dead bird which should represent the Twitter bird. This bird is crossed with a iron beam. No they represent the new logo X."/> <figcaption>
            <p><a href="https://www.davidrevoy.com/">🄯 CC-BY 4.0 by David Revoy</a></p>
        </figcaption>
</figure>

<h3 id="pixelfed">Pixelfed</h3>
<p>Instagram is a really big platform run by Meta, which is known for its <a href="https://tuta.com/blog/posts/google-facebook-free">extremely bad privacy</a> and they also had a few scandals. For example, they helped manipulate elections. See <a href="https://www.theguardian.com/news/2018/mar/17/cambridge-analytica-facebook-influence-us-election">Cambridge Analytica</a>.</p>
<p>Pixelfed is the pardon from the Fediverse for Instagram. It is developed by a Canadian software engineer and the community.</p>
<p>In my opinion, it is not as widely developed as Mastodon. But it&rsquo;s almost on a perfect level. It has many features, even some that classical Instagram does not have. But most importantly, it is part of the Fediverse and therefore, you could also consume the great photography available on Pixelfed from your Mastodon feed or whichever service you like the most.</p>
<p>And of course, Pixelfed is developed as <strong>Free and Open Source Software / FOSS!</strong></p>
<p>You can join it at: <a href="https://pixelfed.org/">https://pixelfed.org/</a></p>
<h3 id="lemmy">Lemmy</h3>
<p>Lemmy is Reddit for the Fediverse and it is also <strong>FOSS</strong>. It seems to have a really active community. I personally did not try it out that much yet but it seems pretty good.</p>
<p>After all, the decision to use Lemmy is not that difficult if Reddit is the current choice. Reddit did screw up hard this year. Same as Twitter. They <a href="https://www.pbs.org/newshour/economy/despite-widespread-user-protest-reddit-ceo-says-company-is-not-negotiating-on-3rd-party-app-charges">banned every third-party app</a> and also made a couple of other bad decisions. On the image below, you see a Reddit event where the collective of Reddit users has written &ldquo;Fuck spez!&rdquo; which is the CEO of Reddit.</p>
<figure>
    <img loading="lazy" src="./fuck-spez.png"
         alt="There is written Fuck spez! on the r/place canvas."/> 
</figure>

<p>This is a great example that the community cannot do anything against the will of a platform like Reddit, Twitter, Instagram, TikTok&hellip; They are extremely powerful companies, maybe monopolies, exploiting their power of having all the users. Let&rsquo;s break that! Let&rsquo;s make a real change and go to the Fediverse!</p>
<p>If you are interested in Lemmy: <a href="https://join-lemmy.org/">https://join-lemmy.org/</a></p>
<h3 id="peertube">Peertube</h3>
<p>Peertube is an alternative to YouTube. Because video streaming is quite expensive and in a decentralized network, there are many smaller communities with less money. Peertube decided to use <em>peer</em> to <em>peer</em>. This means if you watch a video, you help someone else watching because your computer sends bits of the video to other persons. So the server does need less bandwidth and therefore, small communities are able to operate a video streaming platform. This approach comes with downsides, but after all, it makes something possible, which would be really hard to achieve in another way. <strong>A really free video platform.</strong></p>
<p>In terms of content, this platform is still a bit niche. There are really good channels, especially in the privacy, tech and Linux community. But for more mainstream content, there is not that much to offer. This, of course, is not a problem of the platform but a problem of the creators who are not feeding the platform. I hope this will change in the future!</p>
<p>Because of the Fediverse, you can, of course, watch and subscribe to Peertube channels from Mastodon and other platforms.</p>
<p>Peertube is developed by the French non-profit association <a href="https://framasoft.org/en/">Framasoft</a> as <strong>FOSS</strong>.</p>
<p>Here you can find more information: <a href="https://joinpeertube.org/">https://joinpeertube.org/</a></p>
<h3 id="what-about-threads-meta">What about Threads (Meta)?</h3>
<figure>
    <img loading="lazy" src="./meta-red-carpet.jpg"
         alt="A little Mastodon is rolling out a red carpet for Meta represented as death / a reaper."/> <figcaption>
            <p><a href="https://www.davidrevoy.com/">🄯 CC-BY 4.0 by David Revoy</a></p>
        </figcaption>
</figure>

<p>Now, bigger companies are starting to join the Fediverse. The biggest is Threads made by Meta.</p>
<p>Currently, Threads is implementing the required  <a href="../../../tags/activitypub">ActivityPub</a> logic to join the network. You can already follow the CEO of Instagram and a few of the team. But it seems like the full connection will take a while to implement.</p>
<p>This topic is a controversy in the Fediverse. On one side stands that Meta is bringing a lot of users to the network, but on the other side, Meta is also well-known for incredibly bad privacy and being part of surveillance capitalism, hurting millions of users.</p>
<p>A few instances have even made the <a href="https://fedidb.org/current-events/anti-meta-fedi-pact">&ldquo;Fedi-Pact&rdquo;</a> and therefore will block Meta on the server side when they are joining. This has an impact of about 7% of active users on the open side of the Fediverse. (Of course, only Meta will not see them. The rest of the network can still interact with them normally.)</p>
<p>If a server does not block a specific other server, the user still has the power to do that. Here, the founder of Mastodon explains his audience how it is done.</p>

    <iframe title="Block Threads" src="https://mastodon.social/@Gargron/111587088958531028/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="100%" allowfullscreen="allowfullscreen"></iframe><script src="https://mastodon.social/embed.js" async="async"></script>

<p>And, of course, Meta will not provide a platform that is nearly as free and open as the other ones I mentioned above. They are only trying to make money and hopefully are not destroying the Fediverse while doing that. The only good they do is that they bring a lot of users with them. That&rsquo;s it. But this also may open the Fediverse to a lot of people, which would be great.</p>
<p>Meta joining is like a dance with the devil. But we may benefit from it.</p>
<h2 id="also-worth-mentioning">Also worth mentioning</h2>
<p>You are not stuck with your account on a server. Of course, you have the freedom to choose another one and take all your followers with you. No problem here. Free software doesn&rsquo;t lock you in place.</p>
<p>Another thing getting weird in the old networks is the account verification. You now need to pay in both <a href="https://www.pcmag.com/news/paid-verification-for-facebook-instagram-starts-rolling-out-in-us">Instagram</a> and <a href="https://www.businessinsider.com/twitter-verification-abuse-trolls-parody-george-bush-oj-simpson-confession-2022-11?IR=T">Twitter</a> to be verified, even if you are not a big creator or another person of interest. This is rather stupid and confusing. In the Fediverse, you do not need to pay anyone. You can just link yourself from another platform/website you own and you get a sign that this account/page is verified to be owned by you. A much better way of verification.</p>
<h2 id="the-technical-stuff">The technical stuff</h2>
<p>I decided not to put the really technical stuff in this post. This would have made this blog post even longer than it is now. I have a bit of experience in creating servers for the Fediverse because of <a href="../../projects/whathappenedtofedodo">Fedodo</a>. I want to share this experience in a future post and talk about the details of  <a href="../../../tags/activitypub">ActivityPub</a>. This is the protocol powering most of the Fediverse.</p>
<h2 id="appreciations">Appreciations</h2>
<p>Thanks to the amazing artists providing their work under Creative Commons licenses. They help me so much in illustrating this post. Links are available under each image. Go and check them out!</p>
<p>Also, thanks to you for reading this until the end. I hope I could give you a bit of an overview of the topic. And maybe I even convinced you to join us fighting for a better web. But don&rsquo;t pressure yourself. Have fun ❤️</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why did I stop working on Fedodo?</title>
      <link>https://lna-dev.net/en/posts/projects/whathappenedtofedodo/</link>
      <pubDate>Sat, 25 Nov 2023 09:12:00 +0100</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/projects/whathappenedtofedodo/</guid>
      <description>I developed a social media platform called Fedodo for half a year. But why did I pause / stop the development after this time?</description>
      <content:encoded><![CDATA[<h2 id="what-is-fedodo">What is Fedodo?</h2>
<p>First of all, to get everyone on the same page, I need to explain what Fedodo is and why I started developing it.</p>
<p>With the rise of <a href="https://joinmastodon.org/">Mastodon</a>, I became more and more interested in the <a href="https://www.w3.org/TR/activitypub/">ActivityPub</a> protocol. I really like the idea of having a big decentralized platform that can compete with the walled gardens of big tech. The <a href="https://www.w3.org/TR/activitypub/">ActivityPub</a> protocol seemed to be just right to achieve that.</p>
<p>Until now, everyone developed a platform for a specific use case:</p>
<ul>
<li><a href="https://joinmastodon.org/">Mastodon</a> as an alternative to Twitter.</li>
<li><a href="https://pixelfed.org/">Pixelfed</a> as an alternative to Instagram.</li>
<li><a href="https://join-lemmy.org/">Lemmy</a> as an alternative to Reddit.</li>
<li>And the list goes on&hellip;</li>
</ul>
<p>I wanted to do this differently. I thought it would not be optimal to develop an ActivityPub server for each of these platforms. So my idea was to create an ActivityPub server which does not only implement the <a href="https://www.w3.org/TR/activitypub/#server-to-server-interactions">ActivityPub server to server</a> protocol but also the <a href="https://www.w3.org/TR/activitypub/#client-to-server-interactions">ActivityPub client to server</a> protocol. This client to server protocol was not really wide spread at this time. But it would allow anyone to use a frontend of their liking with the same server. Also every user now just needs one account. This is possible because Fedodo has a feature which allows multiple ActivityPub actors per user account.</p>
<p>As part of Fedodo, I wanted to develop not only the server but also a set of frontends. In detail, I developed a <a href="https://joinmastodon.org/">Mastodon</a>-like frontend an was starting to develop a <a href="https://pixelfed.org/">Pixelfed</a>-like UI.</p>
<h2 id="the-development">The development</h2>
<p>I started developing Fedodo in December 2022 and worked on it extensively until August 2023. Dedicating around eight months to the project.</p>
<p>During this time, I successfully developed a functional server. While it might be beneficial to give it a bit more attention, write a few tests here and there, and fix a few things, overall, it is working great. In addition to that I had a basic <a href="https://joinmastodon.org/">Mastodon</a>-like app kind of finished and made significant progress developing the <a href="https://pixelfed.org/">Pixelfed</a> alternative.</p>
<p>For me, this is significant achievement and I am also a bit pride. I even had the opportunity to participate in a podcast hosted by a significant German tech YouTuber and chat about ActivityPub. This was quite an honor for me. (<a href="https://www.youtube.com/watch?v=yP4yN1vyn5s">To the episode</a>)</p>
<h2 id="what-is-the-problem">What is the Problem?</h2>
<p>During the development period, I dedicated approximately 380 hours solely to coding. In addition to this comes the time I thought about the project and did not have an editor open (which I did not track). This makes about one and a half hours per day just coding. <strong>Every day</strong> in this eight months. Only paid with the German minimum wage, this would make up a 4715€. Clearly, a significant amount of time and effort went into the Fedodo project.</p>
<p>The problem is I did this in addition to a 40 hours work week. So my total working hours per week were about 50 hours.</p>
<p>I really enjoy coding in my free time. It is a awesome hobby for me. The problem with big projects like Fedodo is that at some point you aren&rsquo;t as hyped on it as in the beginning. And after some time, even a hobby project turns into work. And I think this is the significant problem.</p>
<p>If I am working on small projects I am hyped for it doesn&rsquo;t feel like work. But Fedodo did after four to five months. And than the 50 hours per week effect you.</p>
<p>I would not recommend to anyone working on a project of this size. I think everything which you can completely finish in under half a year is fine. But if you have the feeling you do work in your freetime it is not worth it. So keep on doing smaller projects and enjoy the process of creating.</p>
<p>Another option would be joining a comparable large community, developing an open source project. There you could do small things and have a proportionally big impact with little effort.</p>
<h2 id="how-does-the-future-of-fedodo-look-like">How does the future of Fedodo look like?</h2>
<p>This feeling of work and personal issues during this time led me to pausing the development of Fedodo and taking a break from coding in my freetime altogether.</p>
<p>Now, a few months later, I&rsquo;m slowly beginning to have this urge again to create something. So, I think it was the right decision to take a step back.</p>
<p>However, I will not work on Fedodo this hard again. Given the project&rsquo;s size, this likely means I won&rsquo;t be able to bring it to completion. Sadly.</p>
<blockquote>
<p><strong>Fedodo is just to big for me</strong></p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Brave</title>
      <link>https://lna-dev.net/en/posts/privacytools/brave/</link>
      <pubDate>Sun, 09 Oct 2022 13:12:12 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/privacytools/brave/</guid>
      <description>Brave is one of the most private browsers and has many additional features. It is also called Brave-Browser.</description>
      <content:encoded><![CDATA[<p>I love using the browser and want to share my experience with you. In this post I will showcase Brave and its features.</p>
<h2 id="privacy-">Privacy 🕵️</h2>
<p>In my opinion privacy is the most <strong>important</strong> feature of a browser. A modern browser should protect you while you surfe
the internet. Brave does have a lot of privacy features build in. And most important a lot more than Google Chrome which
lacks a lot of privacy features.</p>
<h3 id="open-source">Open Source</h3>
<p>Open Source makes sure that what a company promises is what a company gives to its customers. Brave is Open Source on
GitHub. This makes sure that everybody can control the software deployments. You can check it out right
<a href="https://github.com/brave/">here</a>.</p>
<h3 id="shields">Shields</h3>
<p>The idea of <strong>Shields</strong> is to stop Online-Tracking. With this in mind Brave blocks following contents by default. The
user <strong>can</strong> optionally disable the blocking for specific websites.</p>
<h4 id="shield-features">Shield Features</h4>
<ul>
<li>Block third party ads</li>
<li>Block third party trackers</li>
<li>Resource Replacement</li>
<li>CNAME uncloaking</li>
<li>Block cross site cookies</li>
<li>Ephemeral Storage</li>
<li>Randomizing browser APIs</li>
<li>Block Browser-language and font fingerprinting</li>
<li>Block Phishing Sites</li>
</ul>
<h3 id="build-in-tor">Build-in Tor</h3>
<p>With Brave it is possible to open a new private window with <strong>Tor</strong>. It makes it really easy to use the Tor network
without installing a new tool. It is also possible to open links with the <code>.onion</code> TLD.</p>
<h2 id="brave-search-">Brave Search 🔍</h2>
<p>Of course you can choose every Search-Engine available but <strong>Brave Search</strong> offers an alternative to Google Search. It
is really new and of course at the moment not as good as Google Search but I think it is usable.</p>
<h2 id="cross-platform-">Cross-Platform 📲</h2>
<p>Brave Browser is available for all major platforms:</p>
<ol>
<li><a href="https://brave.com/download/">Linux</a></li>
<li><a href="https://brave.com/download/">Windows</a></li>
<li><a href="https://brave.com/download/">Mac</a></li>
<li><a href="https://play.google.com/store/apps/details?id=com.brave.browser">Android</a></li>
<li><a href="https://apps.apple.com/us/app/brave-private-web-browser-vpn/id1052879175">IOS</a></li>
</ol>
<h3 id="sync">Sync</h3>
<p>Brave offers the feature &ldquo;Sync&rdquo;. This feature allows the user to sync whatever the user wants with other devices. So you
can have all your Bookmarks and co. on every device. It does not matter if you are on Mobile, Linux, Windows&hellip; it just
works. In addition to this there is no account required for this feature. To Sync your stuff you just need to scan a
QR-Code and select what you want to sync. (It is also possible to display and type in a view words)</p>
<h2 id="speed-">Speed 🥇</h2>
<p>Brave says it is three times faster than Chrome and uses less memory. In my experience that is true. Just try it out 😉</p>
<h2 id="a-new-way-of-advertising-">A new way of advertising 💲</h2>
<p><strong>Brave Rewards</strong> is a new way of provisioning ads. Brave blocks every ad by default. From a user perspective this is
awesome of course. But as a creator point of view this sucks. They just don&rsquo;t make money anymore. A lot of content is
free and the creators get paid with the add revenue. This does not exist if all ads are blocked. So Brave developed a
new way of provisioning ads. Brave can optionally serve ads to the user. <strong>BUT!</strong> this is completely privacy focused and
does not track you. Also the user gets 70% of the money in form of the <strong>BAT</strong> Crypto-Currency, 15% gets the Website-Creator
and the last 15% gets Brave itself. I think this is a pretty good deal for everybody. In my opinion the own data should
be owned by yourself and this way of advertising supports that. In addition Brave offers the ability to give <strong>BAT</strong> back
to the Creators. This can be done automatically or manually. So you can support the websites you use and like directly
and specifically.</p>
<h2 id="crypto-">Crypto 🪙</h2>
<p>Brave offers the ability to store your Cryptocurrency&rsquo;s directly in your browser. They can be secured with a backup and
it offers a lot of different Blockchains out of the box. But it is possible to add any additional networks.</p>
<h2 id="links-">Links 🔗</h2>
<p><a href="https://brave.com/">Brave</a><br>
<a href="https://en.wikipedia.org/wiki/Brave_Search">Wikipedia Brave Search</a><br>
<a href="https://youtu.be/VHwIyR6ca4o">YouTube Techlore</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Don&#39;t Hex the Water</title>
      <link>https://lna-dev.net/en/posts/privacy/donthexthewater/</link>
      <pubDate>Mon, 05 Sep 2022 22:09:34 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/privacy/donthexthewater/</guid>
      <description>Joining exurb1a on a topic of privacy.</description>
      <content:encoded><![CDATA[<h2 id="general">General</h2>
<p>In this post I want to join <a href="https://www.youtube.com/c/Exurb1a">exurb1a</a> on his statement about privacy and the importance
of it in our day-to-day life. And much more important than this I want to showcase how to do it better. For this I will
create posts about <a href="/en/categories/privacy-tools">Privacy Tools</a> and <a href="/en/categories/privacy">Privacy</a> in general from time to time.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>This post is based on <a href="https://www.youtube.com/watch?v=Fzhkwyoe5vI">this</a> video. I <strong>highly</strong> recommend watching it. It
is a 10 out of 10 video about modern privacy. You can imagine the video to be based more on philosophical standpoint than on a technical one. He uses analogies but also explains a bit of the technical details. But the focus definitely lies on a philosophical point of view.</p>
<h2 id="why-should-i-care-about-privacy">Why should I care about privacy?</h2>
<p>If you say: &ldquo;I like the amount of privacy I have right now&rdquo; than you lay the first stones of a more privacy invading
future. <strong>The way we do the things today shapes&hellip; no&hellip; determines the future!</strong></p>
<p>If it is ok today than tomorrow it will be not. So in my opinion we have to change how we think about privacy.</p>
<h3 id="the-erosion-of-privacy-over-time">The Erosion of Privacy Over Time</h3>
<p>The digital landscape is ever-evolving, with technology penetrating deeper into our lives. What may seem like a tolerable invasion of privacy today could escalate into a more significant concern as new technologies and practices emerge. By proactively acknowledging this reality, we can actively participate in shaping the boundaries of our privacy. Especially empowering states or companies is a big issue. Even if your country currently is a well build democracy there can always be a change. As I am from Germany I am well aware of what countries with horrible ideas could do with those technologies at their fingertips.</p>
<h3 id="empowering-people-and-shaping-how-we-live-online">Empowering People and Shaping How We Live Online</h3>
<p>Privacy isn&rsquo;t just about you or me it ripples through society. The choices we make today set the tone for tomorrow&rsquo;s norms. The digital on social norms. When we stand up for privacy, we&rsquo;re giving individuals the power to control their data and build a digital world that respects everyone&rsquo;s rights in the online world and offline.</p>
<h3 id="lets-be-mindful-about-privacy">Let&rsquo;s Be Mindful About Privacy</h3>
<p>Changing our view on privacy is about making conscious choices. It&rsquo;s looking at the way things are now, thinking about what could happen and actively finding ways to boost our online privacy. This isn&rsquo;t just about shielding our own stuff, it&rsquo;s about joining forces to make a digital world that&rsquo;s more aware of privacy. Don&rsquo;t let the powerful decide for us.</p>
<p>In simple terms, caring about privacy isn&rsquo;t just a personal preference, it&rsquo;s a shared responsibility that goes beyond just one person. It shapes where our digital journey is headed. Let&rsquo;s get that, accept the responsibility, and team up for a future where privacy isn&rsquo;t just talked about but truly looked after.</p>
<h2 id="what-should-i-do-now">What should I do now?</h2>
<p>I think the best you can do is staying well-informed to make good decisions. And this is not a thing you do once. It is
a journey. There are many good sites which showcase good privacy respecting tools. If you are interested I always have a few interesting links at my <a href="/en/links">Link-Page</a>.</p>
<p>I also will showcase a couple of nice software I use and recommend. Stay tuned!</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to create and host a Blog with Hugo for free</title>
      <link>https://lna-dev.net/en/posts/tutorials/blogwithhugo/</link>
      <pubDate>Fri, 19 Aug 2022 15:59:48 +0200</pubDate><author>me@lna-dev.net (Lukas Nagel)</author>
      <guid>https://lna-dev.net/en/posts/tutorials/blogwithhugo/</guid>
      <description>In this blog post we will discuss how to build and deploy your own blog with Hugo.</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>In this post I will teach you how to set up a blog like this with hugo and GitHub-Pages.</p>
<h2 id="what-you-need">What you need</h2>
<ul>
<li>GitHub-Account</li>
<li>Markdown Knowledge</li>
<li>Basic understanding of Pipelines (This time GitHub-Actions)</li>
<li>Basic understanding of Git</li>
</ul>
<h2 id="repository">Repository</h2>
<p>All the actions below must be executed in a repository connected to GitHub.</p>
<h2 id="hugo">Hugo</h2>
<h3 id="install-hugo">Install Hugo</h3>
<h4 id="snap-linux">Snap (Linux)</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>snap install hugo --channel<span style="color:#f92672">=</span>extended
</span></span></code></pre></div><h4 id="chocolatey-windows">Chocolatey (Windows)</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ps" data-lang="ps"><span style="display:flex;"><span><span style="color:#a6e22e">choco</span> <span style="color:#a6e22e">install</span> <span style="color:#a6e22e">hugo-extended</span> <span style="color:#a6e22e">-confirm</span>
</span></span></code></pre></div><h4 id="homebrew-mac--linux">Homebrew (Mac / Linux)</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>brew install hugo
</span></span></code></pre></div><p>If you need more information or installation options:
<a href="https://gohugo.io/getting-started/installing/">Hugo - Docs - Installation</a></p>
<h3 id="create-a-new-project">Create a new project</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>hugo new site &lt;name of site&gt; -f yml
</span></span></code></pre></div><h3 id="theme">Theme</h3>
<h4 id="install-a-theme">Install a theme</h4>
<p>In this tutorial we will use the <a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod-Theme</a> for Hugo.</p>
<ol>
<li>Go to <a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod-Theme</a> and download the repo</li>
<li>Create a folder themes/PaperMod in your hugo project</li>
<li>Paste the files in the themes/PaperMod folder in your hugo project</li>
</ol>
<h4 id="configure-the-theme">Configure the theme</h4>
<p>To configure the theme you must change the content of the config.yaml. Change it until it fits your needs. Further
documentation about specific parameters can be found on the
<a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod-GitHub-Page</a>.</p>
<h5 id="example-yaml">Example YAML</h5>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">baseURL</span>: <span style="color:#e6db74">&#34;https://examplesite.com/&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">title</span>: <span style="color:#ae81ff">ExampleSite</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">paginate</span>: <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">theme</span>: <span style="color:#ae81ff">PaperMod</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">enableRobotsTXT</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">buildDrafts</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">buildFuture</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">buildExpired</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">googleAnalytics</span>: <span style="color:#ae81ff">UA-123-45</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">minify</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">disableXML</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">minifyOutput</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">params</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">env</span>: <span style="color:#ae81ff">production</span> <span style="color:#75715e"># to enable Google Analytics, opengraph, twitter-cards and schema.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">title</span>: <span style="color:#ae81ff">ExampleSite</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">description</span>: <span style="color:#e6db74">&#34;ExampleSite description&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">keywords</span>: [<span style="color:#ae81ff">Blog, Portfolio, PaperMod]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">author</span>: <span style="color:#ae81ff">Me</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># author: [&#34;Me&#34;, &#34;You&#34;] # multiple authors</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">images</span>: [<span style="color:#e6db74">&#34;&lt;link or path of image for opengraph, twitter-cards&gt;&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">DateFormat</span>: <span style="color:#e6db74">&#34;January 2, 2006&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">defaultTheme</span>: <span style="color:#ae81ff">auto</span> <span style="color:#75715e"># dark, light</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">disableThemeToggle</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ShowReadingTime</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ShowShareButtons</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ShowPostNavLinks</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ShowBreadCrumbs</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ShowCodeCopyButtons</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ShowWordCount</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ShowRssButtonInSectionTermList</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">UseHugoToc</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">disableSpecial1stPost</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">disableScrollToTop</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">comments</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hidemeta</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hideSummary</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">showtoc</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tocopen</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">assets</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># disableHLJS: true # to disable highlight.js</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># disableFingerprinting: true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">favicon</span>: <span style="color:#e6db74">&#34;&lt;link / abs url&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">favicon16x16</span>: <span style="color:#e6db74">&#34;&lt;link / abs url&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">favicon32x32</span>: <span style="color:#e6db74">&#34;&lt;link / abs url&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">apple_touch_icon</span>: <span style="color:#e6db74">&#34;&lt;link / abs url&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">safari_pinned_tab</span>: <span style="color:#e6db74">&#34;&lt;link / abs url&gt;&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">label</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">text</span>: <span style="color:#e6db74">&#34;Home&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">icon</span>: <span style="color:#ae81ff">/apple-touch-icon.png</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">iconHeight</span>: <span style="color:#ae81ff">35</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># profile-mode</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">profileMode</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span> <span style="color:#75715e"># needs to be explicitly set</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">title</span>: <span style="color:#ae81ff">ExampleSite</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">subtitle</span>: <span style="color:#e6db74">&#34;This is subtitle&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">imageUrl</span>: <span style="color:#e6db74">&#34;&lt;img location&gt;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">imageWidth</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">imageHeight</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">imageTitle</span>: <span style="color:#ae81ff">my image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">buttons</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Posts</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">url</span>: <span style="color:#ae81ff">posts</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Tags</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">url</span>: <span style="color:#ae81ff">tags</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># home-info mode</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">homeInfoParams</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">Title</span>: <span style="color:#e6db74">&#34;Hi there \U0001F44B&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">Content</span>: <span style="color:#ae81ff">Welcome to my blog</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">socialIcons</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">twitter</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#e6db74">&#34;https://twitter.com/&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">stackoverflow</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#e6db74">&#34;https://stackoverflow.com&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">github</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#e6db74">&#34;https://github.com/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">analytics</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">google</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">SiteVerificationTag</span>: <span style="color:#e6db74">&#34;XYZabc&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">bing</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">SiteVerificationTag</span>: <span style="color:#e6db74">&#34;XYZabc&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">yandex</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">SiteVerificationTag</span>: <span style="color:#e6db74">&#34;XYZabc&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cover</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">hidden</span>: <span style="color:#66d9ef">true</span> <span style="color:#75715e"># hide everywhere but not in structured data</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">hiddenInList</span>: <span style="color:#66d9ef">true</span> <span style="color:#75715e"># hide on list pages and home</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">hiddenInSingle</span>: <span style="color:#66d9ef">true</span> <span style="color:#75715e"># hide on single page</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">editPost</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">URL</span>: <span style="color:#e6db74">&#34;https://github.com/&lt;path_to_repo&gt;/content&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">Text</span>: <span style="color:#e6db74">&#34;Suggest Changes&#34;</span> <span style="color:#75715e"># edit text</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">appendFilePath</span>: <span style="color:#66d9ef">true</span> <span style="color:#75715e"># to append file path to Edit link</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># for search</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># https://fusejs.io/api/options.html</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">fuseOpts</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">isCaseSensitive</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">shouldSort</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span>: <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">distance</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">threshold</span>: <span style="color:#ae81ff">0.4</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">minMatchCharLength</span>: <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">keys</span>: [<span style="color:#e6db74">&#34;title&#34;</span>, <span style="color:#e6db74">&#34;permalink&#34;</span>, <span style="color:#e6db74">&#34;summary&#34;</span>, <span style="color:#e6db74">&#34;content&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#f92672">menu</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">main</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">identifier</span>: <span style="color:#ae81ff">categories</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">categories</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">/categories/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">weight</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">identifier</span>: <span style="color:#ae81ff">tags</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tags</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">/tags/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">weight</span>: <span style="color:#ae81ff">20</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">identifier</span>: <span style="color:#ae81ff">example</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">example.org</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://example.org</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">weight</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Read: https://github.com/adityatelange/hugo-PaperMod/wiki/FAQs#using-hugos-syntax-highlighter-chroma</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">pygmentsUseClasses</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">markup</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">highlight</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">noClasses</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># anchorLineNos: true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># codeFences: true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># guessSyntax: true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># lineNos: true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># style: monokai</span>
</span></span></code></pre></div><h3 id="using-a-post-template">Using a post template</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-path" data-lang="path"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">/archetypes/</span>
</span></span></code></pre></div><p>All files in the directory above can be used as templates for posts. The default.md file is the default file for
generating new posts. All other files must be specified while creating a post.</p>
<h3 id="create-a-post">Create a Post</h3>
<p>I would always use the hugo commands to generate the files because it copies the standard configuration in the new file.
I would generate the .md files always in the posts sub-folder (If you want to create a post 😉). Only files from this
folder will be shown as posts.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>hugo new &lt;filePath&gt;
</span></span></code></pre></div><h4 id="example">Example</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>hugo new posts/BlogWithHugo.md
</span></span></code></pre></div><h3 id="build-the-page">Build the page</h3>
<p>The hugo command builds the project into a website. The files created are stored in the /public/ folder.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>hugo
</span></span></code></pre></div><h3 id="debug-the-page">Debug the page</h3>
<p>To view the page in the browser run the command below. There will be a link displayed which references the local hosted
webpage.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>hugo serve
</span></span></code></pre></div><h2 id="build-and-deployment--pipeline">Build and Deployment / Pipeline</h2>
<h3 id="setup-github-pages">Setup GitHub-Pages</h3>
<ol>
<li>Go into the settings of your repo</li>
<li>Select the GitHubPages tab</li>
<li>Set the source to GitHubActions</li>
</ol>
<p><img alt="GitHubPages Settings" loading="lazy" src="/BlogWithHugo/GitHubPages.png"></p>
<h3 id="add-pipeline-yaml">Add Pipeline YAML</h3>
<p>Create following file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-path" data-lang="path"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">.github/workflows/HugoBuildAndDeploy.yaml</span>
</span></span></code></pre></div><p>Insert the following code into the file and adjust it to your needs. I described what I was doing per comments.</p>
<p>In this pipeline we will checkout the repo build it and after that deploy it to GitHub-Pages.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">HugoBuildAndDeploy</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_dispatch</span>: <span style="color:#75715e"># To have the ability to run the workflow manually</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#ae81ff">main]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">NAME</span>: <span style="color:#ae81ff">LNA-DEV-Blog</span> <span style="color:#75715e">#TODO Change to your project name</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">HugoBuild</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Checkout the repository</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v3</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Install Hugo</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">sudo snap install hugo</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Build the hugo repository</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">hugo</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">working-directory</span>: <span style="color:#ae81ff">./${{ env.NAME }}/</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Zip the Artifact for GitHubPages deployment</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Archive artifact</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">shell</span>: <span style="color:#ae81ff">bash</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">if</span>: <span style="color:#ae81ff">runner.os != &#39;Windows&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">tar -cvf ${{ runner.temp }}/artifact.tar -C ./${{ env.NAME }}/public .</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Create a build artifact</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload a Build Artifact</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/upload-artifact@v3.1.0</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">github-pages</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">path</span>: <span style="color:#ae81ff">${{ runner.temp }}/artifact.tar</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">DeployToGithubPages</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">needs</span>: <span style="color:#ae81ff">HugoBuild</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Setup GitHubPages</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">permissions</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">pages</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">id-token</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">github-pages</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">${{ steps.deployment.outputs.page_url }}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Deploy the artifact</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy to GitHub Pages</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">id</span>: <span style="color:#ae81ff">deployment</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/deploy-pages@v1</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
  </channel>
</rss>