diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..be8762fa --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +tiim.ch diff --git a/__data.json b/__data.json new file mode 100644 index 00000000..fdbe4ed2 --- /dev/null +++ b/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"indexMeta":1,"mf2":13,"recent":26},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Ch1>Hey, I'm \u003Cspan class=\"p-given-name\">Tim\u003C/span> 😊\u003C/h1>\n\u003Cp>\u003Cstrong>I'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at the \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swim coach and swimmer in \u003Cspan class=\"p-country-name\">Switzerland\u003C/span>.\u003C/strong>\u003C/p>\n\u003Cp>🏊‍♂️💻🏋️‍♂️\u003C/p>","index","43aca391-125a-4fd3-a238-8916f41920d2",["Date","2022-11-03T00:00:00.000Z"],["Date","2022-11-03T17:06:03.000Z"],true,"\u003Cp>\u003Cstrong>I'm a {{role}} at the {{org}}, swim coach and swimmer in {{country_name}}.\u003C/strong>\u003C/p>",[],"article","","metadata",{"name":14,"given_name":15,"family_name":16,"nickname":17,"email":18,"photo":19,"url":20,"locality":21,"country_name":22,"org":23,"job_title":11,"role":24,"gender_identity":25},"Tim Bachmann","Tim","Bachmann","Tiim","hey@tiim.ch","https://media.tiim.ch/tiim.jpg","https://tiim.ch/","Basel","Switzerland","University of Basel","master graduate in computer science","male",[27,48,64,82,101,112],{"html":28,"slug":29,"uuid":30,"date":31,"created":32,"aliases":33,"title":34,"published":7,"modified":33,"description":35,"cover_image":36,"cover_image_txt":37,"content_tags":38,"abstract":43,"tags":44,"links":-1,"type":10,"folder":45,"comments":46,"latestComment":47},"\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>\n\u003Cp>Deleting with elevated permission on the command line is easy: The command \u003Ccode>sudo rm -rf ~/docker/myservice\u003C/code> performs the \u003Ccode>rm\u003C/code> operation as the root user. In bash, this will delete the \u003Ccode>docker/myservice\u003C/code> folder in the user's home directory, but when doing the equivalent in Ansible, this won't work!\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\"># This does not work!\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"~/docker/myservice\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>This code will try to delete the file \u003Ccode>/user/root/docker/myservice\u003C/code>, which is not what we wanted.\u003C/p>\n\u003Cp>The bash version works because the shell first resolves the tilde in the argument to the current users' directory before calling the sudo command. In Ansible, we first switch to the root user and only then the tilde is resolved: this time to the home directory of the root user.\u003C/p>\n\u003Cp>To circumvent this, we can manually resolve the path to an absolute path. Unfortunately, I have not found a straightforward way to do this in Ansible, however the bash command \u003Ccode>readlink -f <path>\u003C/code> does exactly this. To use it in Ansible, we can use the following configuration:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\">- name: Get absolute folder path\n ansible.builtin.command:\n cmd: \"readlink -f ~/docker/myservice\"\n register: folder_abs\n changed_when: False\n\n- name: Debug\n debug:\n msg: \"{{folder_abs.stdout}}\" # prints /user/tim/docker/myservice\n\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"{{folder_abs.stdout}}\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>With this Ansible script, we manually resolve the absolute path and use it to delete the folder using root permissions. If you know of an easier way to resolve to an absolute path, please let me know!\u003C/p>","blog/2023-09-20-ansible-absolute-path","ad58acaf-56b0-4bcf-9b72-d6c054fc48d4",["Date","2023-09-20T21:39:13.000Z"],["Date","2023-09-20T20:22:35.634Z"],null,"Getting the Absolute Path of a Remote Directory in Ansible","There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.","https://media.tiim.ch/3c1246e4-3201-4df6-af87-6aa4ab98800e.webp","(stable doodle) server room, neon, cables",[39,40,41,42],"dev","ansible","linux","bash","\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>",[40,42,39,41],"blog",[],"2023-09-02T19:26:59Z",{"html":49,"slug":50,"uuid":51,"title":52,"date":53,"modified":33,"section":54,"published":7,"content_tags":55,"links":58,"abstract":60,"tags":61,"type":10,"cover_image":-1,"description":11,"folder":62,"comments":63,"latestComment":47},"\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>\n\u003Cp>Pomo is a simple pomodoro timer. It allows you to either specify the number of repetitions (pomodori), the duration of the pomodori and the duration of the breaks, or\nyou can stecify an end time, and let pomo calculate the durations and repetitions.\u003C/p>\n\u003Cp>Pomo runs as a cli tool and stores the current state in a json file. All pomo executions excep \u003Ccode>pomo watch\u003C/code> just\nmodify this json file and terminate. The watch command displays the current pomodoro timer, optionally writes the timer to a text file,\nand watches for changes of the json file.\u003C/p>","projects/pomo","bfa1a7fa-b8a3-469f-b8e8-727ab705cb93","Pomo 🍅",["Date","2023-08-03T11:03:00.000Z"],"Projects",[56,57,39],"rust","cli",[59],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/pomo\" rel=\"nofollow noopener noreferrer\">pomo Github\u003C/a>\u003C/p>","\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>",[57,39,56],"projects",[],{"html":65,"slug":66,"uuid":67,"title":68,"date":69,"modified":33,"section":54,"published":7,"content_tags":70,"links":76,"abstract":79,"tags":80,"type":10,"cover_image":-1,"description":11,"folder":62,"comments":81,"latestComment":47},"\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>\n\u003Cp>Android widgets are handled by the operating system and only support a limited set of features for rendering.\nTo display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.\u003C/p>","projects/markdown-widget","c6a11779-cf98-4983-8744-9b1effae8d7a","Android Markdown Widget",["Date","2023-08-02T08:59:00.000Z"],[71,72,73,74,75,39],"android","java","kotlin","markdown","widget",[77,78],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Android-Markdown-Widget\" rel=\"nofollow noopener noreferrer\">Android Markdown Widget Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://f-droid.org/packages/ch.tiim.markdown_widget/\" rel=\"nofollow noopener noreferrer\">Download on F-Droid\u003C/a>\u003C/p>","\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>",[71,39,72,73,74,75],[],{"html":83,"slug":84,"uuid":85,"title":86,"date":87,"modified":33,"section":54,"published":7,"cover_image":88,"content_tags":89,"links":95,"abstract":98,"tags":99,"type":10,"description":11,"folder":62,"comments":100,"latestComment":47},"\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>\n\u003Cul>\n\u003Cli>The basic commenting system\u003C/li>\n\u003Cli>Sending and receiving webmentions\u003C/li>\n\u003Cli>Micropub server implementation\u003C/li>\n\u003Cli>IndieAuth (decentralized authentication standard based on OAuth)\u003C/li>\n\u003Cli>Admin dashboard\u003C/li>\n\u003Cli>Admin backup endpoint\u003C/li>\n\u003C/ul>\n\u003Cp>Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and\ncomments through the fediverse show up back on my website.\u003C/p>\n\u003Cp>The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core\nof the application is only concerned with initializing those plugins.\u003C/p>\n\u003Cp>If you have any questions, or want to run IndieGo yourself, don't hesitate to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>.\u003C/p>","projects/indiego","0cf125b3-a99a-4996-8f84-ec5105d64c57","IndieGo",["Date","2023-08-02T08:39:00.000Z"],"/assets/2022-07-first-go-project-commenting-api.png",[90,91,92,93,94,39],"go","golang","indieweb","docker","sqlite",[96,97],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">IndieGo Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://comments.tiim.ch\" rel=\"nofollow noopener noreferrer\">Admin Interface\u003C/a> - authentication required\u003C/p>","\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>",[39,93,90,91,92,94],[],{"html":102,"slug":103,"uuid":104,"title":105,"published":7,"description":106,"date":107,"modified":33,"cover_image":-1,"abstract":108,"tags":109,"links":-1,"type":10,"folder":110,"comments":111,"latestComment":47},"\u003Cp>I am planning to publish the list of blogs I subscribe to here in the future. But I still have not figured out how\nto automatically export the list of feeds from Mozilla Thunderbird and import it here. Until then, this page consists of\na manual list of pages that I like.\u003C/p>\n\u003Ch2>Links\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://blogroll.org/\" rel=\"nofollow noopener noreferrer\">Ye Olde Blogroll\u003C/a>, a human curated list of personal blogs.\u003C/li>\n\u003Cli>\u003Ca href=\"https://uses.tech/\" rel=\"nofollow noopener noreferrer\">uses.tech\u003C/a>, a list of personal websites that have a \u003Ca href=\"https://tiim.ch/uses\" rel=\"nofollow noopener noreferrer\">\u003Ccode>/uses\u003C/code>\u003C/a> page.\u003C/li>\n\u003Cli>\u003Ca href=\"https://changelog.com/news\" rel=\"nofollow noopener noreferrer\">changelog.com/news\u003C/a>, curated developer news as a podcast and as a newsletter. Unfortunately not available a RSS subscribable blog post.\u003C/li>\n\u003C/ul>\n\u003Cp>\u003Cem>You have some suggestions for other cool or useful websites or blogs? \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">Let me know\u003C/a>.\u003C/em>\u003C/p>","pages/links","5d86f95b-751c-416d-bd24-e35401317494","Blogroll and Links","List of cool blogs and other links I follow",["Date","2023-07-31T19:06:29.000Z"],"\u003Cp>I am planning to publish the list of blogs I subscribe to here in the future. But I still have not figured out how\nto automatically export the list of feeds from Mozilla Thunderbird and import it here. Until then, this page consists of\na manual list of pages that I like.\u003C/p>",[],"pages",[],{"html":113,"slug":114,"date":115,"content_tags":116,"in_reply_to":120,"raw_data":122,"abstract":139,"tags":140,"links":-1,"published":7,"type":141,"cover_image":-1,"description":142,"folder":143,"comments":144,"latestComment":47},"\u003Cdiv class=\"mf2\">\u003Cp>This post is in reply to \"\u003Ca class=\"u-in-reply-to\" href=\"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\">https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\u003C/a>\"\u003C/p>\u003C/div>\n\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>","mf2/2023/07/mteymz",["Date","2023-07-02T17:58:00.000Z"],[117,118,119],"rss","email","newsletter",{"url":121},"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/",{"items":123,"rels":137,"relurls":138},[124],{"id":11,"value":11,"html":11,"type":125,"properties":127,"shape":11,"coords":11,"children":136},[126],"h-entry",{"category":128,"content":129,"in-reply-to":131,"post-status":132,"published":134},[117,118,119],[130],"Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\n\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile. ",[121],[133],"published",[135],"2023-07-02T19:58:00+0200",[],{},{},"\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>",[118,119,117],"reply","💬 In reply to: https://kevincox.ca/2023/06/27/decade-of-rss-via-email/","mf2",[145],{"id":146,"type":147,"replyTo":11,"timestamp":148,"page":114,"url":149,"content":150,"name":151},"447820cb-5432-4f54-bd83-94e5674366e6","comment","2023-07-03T11:01:20Z","https://tiim.ch/mf2/2023/07/mteymz#447820cb-5432-4f54-bd83-94e5674366e6","I definitely get that email-based feed reading is not for everyone, but I don't think we are that different here. My inbox is indeed reserved for \"for things that require my attention\". That is why only a very small number of feeds go to my inbox (like WebMention replies to my posts). The vast majority of feeds go into dedicated folders with no notifications. So in a way these folders are my feed reader. The only relation that they have to my \"normal email workflow\" is that the exist in the same app. When I say I get my feeds via email lots of people immediately picture having feeds show up in their regular email workflow and are (rightfully) mortified. I think this approach would work well for almost no one. I think it is very important to have separation by priority much like Thunderbird has a separate news feed rather than dumping the feeds into your inbox. The difference here is that the separation is different folders in the same IMAP account rather than a distinct news account.","Kevin"],"uses":{}}]} diff --git a/_app/immutable/assets/0.8d503a92.css b/_app/immutable/assets/0.8d503a92.css new file mode 100644 index 00000000..eb7691ec --- /dev/null +++ b/_app/immutable/assets/0.8d503a92.css @@ -0,0 +1 @@ +.header-checkbox.svelte-1qkwc1i.svelte-1qkwc1i{display:none}.toggle.svelte-1qkwc1i.svelte-1qkwc1i{position:relative;cursor:pointer;display:block;width:36px;height:36px}.toggle.svelte-1qkwc1i.svelte-1qkwc1i:hover{background-color:var(--background-color-light-2)}.toggle.svelte-1qkwc1i span.svelte-1qkwc1i{position:absolute;display:block;width:26px;right:5px;height:2px;background-color:#fff;border-radius:2px;transition:background-color .2s ease-in-out}.toggle.svelte-1qkwc1i span.svelte-1qkwc1i:nth-child(1){top:10px}.toggle.svelte-1qkwc1i span.svelte-1qkwc1i:nth-child(2){top:calc(50% - 1px)}.toggle.svelte-1qkwc1i span.svelte-1qkwc1i:nth-child(3){bottom:10px}.mobile-dropdown.svelte-1qkwc1i.svelte-1qkwc1i{position:absolute;top:80px;right:10px;display:flex;flex-direction:column;background-color:var(--background-color-light-2);border-radius:.5rem}.navbar.svelte-1qkwc1i.svelte-1qkwc1i{height:60px;padding:1rem var(--padding-side);display:flex;align-items:stretch;gap:var(--header-gap);background-color:var(--background-color-light);position:relative}.navbar-brand.svelte-1qkwc1i.svelte-1qkwc1i{font-weight:bolder;display:none}.navbar-brand.svelte-1qkwc1i .navbar-item.svelte-1qkwc1i{color:var(--color-syntax-1);font-size:1.1rem;letter-spacing:2px;margin-right:2rem}.navbar-brand.svelte-1qkwc1i.svelte-1qkwc1i,.navbar-menu.svelte-1qkwc1i.svelte-1qkwc1i{display:flex;align-items:center;gap:var(--header-gap)}.navbar-menu.mobile.svelte-1qkwc1i.svelte-1qkwc1i{display:none}@media only screen and (max-width: 800px){.navbar-menu.desktop.svelte-1qkwc1i.svelte-1qkwc1i{display:none}.navbar-menu.mobile.svelte-1qkwc1i.svelte-1qkwc1i{display:flex}.navbar.svelte-1qkwc1i.svelte-1qkwc1i{justify-content:space-between}}.navbar-item.svelte-1qkwc1i.svelte-1qkwc1i{padding:1rem;display:inline;text-decoration:none;text-align:center;color:var(--font-color-light);border-radius:.5rem}.navbar-brand.svelte-1qkwc1i .navbar-item.svelte-1qkwc1i{padding:0;background-color:transparent!important}.navbar-item.svelte-1qkwc1i.svelte-1qkwc1i:hover{background-color:var(--background-color-light-2)}.mobile.svelte-1qkwc1i .navbar-item.svelte-1qkwc1i:hover{background-color:var(--background-color-light-3)}.copyright.svelte-18j7aqw{text-align:center;margin-top:3rem;font-size:.8rem}footer.svelte-18j7aqw{margin-top:2rem;border-top:dashed 2px var(--color-ui-12);background-color:var(--background-color-light);color:var(--font-color-muted);padding:2rem}/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{--color-syntax-1: #b877db;--color-syntax-2: #25b2bc;--color-syntax-3: #e95678;--color-syntax-4: #f09383;--color-syntax-5: #fab795;--color-syntax-6: #fac29a;--color-ui-1: #16161c;--color-ui-2: #1a1c23;--color-ui-3: #1c1e26;--color-ui-4: #232530;--color-ui-5: #2e303e;--color-ui-6: #6c6f93;--color-ui-7: #e9436f;--color-ui-8: #e95379;--color-ui-9: #f43e5c;--color-ui-10: #09f7a0;--color-ui-11: #27d796;--color-ui-12: #21bfc2;--color-ui-13: #fab28e;--background-color: var(--color-ui-1);--background-color-light: var(--color-ui-3);--background-color-light-2: var(--color-ui-5);--background-color-light-3: var(--color-ui-6);--font-color: white;--font-color-muted: #dcdcdc;--font-color-light: var(--color-syntax-6);--font-color-link: var(--color-syntax-2);--font-color-link-hover: var(--color-syntax-3);--line-color: var(--color-ui-6);--shadow-color: var(--color-ui-11);--header-gap: .5rem;--padding-side: 2rem;font-family:Fira Code,Consolas,Courier New,monospace;font-weight:400;font-size:1em;line-height:1.5;color:var(--font-color)}*{box-sizing:border-box}body{background-color:var(--background-color)}h1,h2{color:var(--color-syntax-1)}h3{color:var(--color-syntax-3)}h4{color:var(--color-syntax-4)}h5{color:var(--color-syntax-5)}pre{background-color:var(--background-color-light);overflow-x:auto;padding:1rem;border-radius:1rem}code{background-color:var(--background-color-light-2);padding:0rem .25rem;color:var(--color-syntax-4);border-radius:5px}a{text-decoration:none;color:var(--font-color-link)}a:hover{color:var(--font-color-link-hover)}article{margin:0}blockquote{border-left:4px solid var(--color-ui-11);padding:.5rem 1rem;background-color:var(--background-color-light);margin:1rem}input,textarea,button{color:var(--font-color);width:100%;max-width:100%;padding:.5rem;border-radius:5px;border:0px;outline:none;background-color:var(--color-ui-3);resize:none}button{cursor:pointer}button:hover{background-color:var(--color-ui-4)}.container img{max-width:100%;display:block;margin:2rem auto}.container{padding:0 var(--padding-side);max-width:1344px;margin:0 auto}.section{padding:3rem 0rem}.columns{display:flex;justify-content:center;row-gap:2rem}.columns .column{flex:1}.columns .column.column-shrink{flex:0}.columns.multiline{flex-wrap:wrap}.has-text-centered{text-align:center}.card{border-radius:1rem;background-color:var(--background-color-light);padding:.5rem 1.25rem}.links p{margin:0}.tags{display:flex;flex-wrap:wrap;justify-content:center;gap:.5rem}.tag{padding:.2rem .5rem;border-radius:.25rem;background-color:var(--background-color-light-2);color:var(--font-color-light)}body::-webkit-scrollbar{width:12px}html{-webkit-scrollbar-width:thin;-moz-scrollbar-width:thin;-ms-scrollbar-width:thin;scrollbar-width:thin;-webkit-scrollbar-color:var(--color-syntax-2) var(--color-ui-1);-moz-scrollbar-color:var(--color-syntax-2) var(--color-ui-1);-ms-scrollbar-color:var(--color-syntax-2) var(--color-ui-1);scrollbar-color:var(--color-syntax-2) var(--color-ui-1)}body::-webkit-scrollbar-track{background:var(--color-ui-1)}body::-webkit-scrollbar-thumb{background-color:var(--color-syntax-2);border-radius:6px;border:3px solid var(--color-ui-1)}.callout{background-color:var(--background-color-light-2);padding:0;padding-bottom:.5rem;border-left:4px solid rgba(var(--ccolor),1);--opacity: .2;--opacity-icon: .4;--ccolor: rgba(158, 158, 158, var(--opacity))}.callout>*{padding-left:1rem;padding-right:1rem}.callout-title{display:flex;align-items:center;width:100%;font-weight:700;background-color:rgba(var(--ccolor),var(--opacity));padding:.5rem 1rem;gap:1rem}.callout-icon{width:1.2rem;height:1.2rem;margin:0!important;padding:0;fill:currentColor;opacity:var(--opacity-icon)}.callout-note{--ccolor: 68, 138, 255}.callout-abstract,.callout-summary,.callout-tldr{--ccolor: 0, 176, 255}.callout-info,.callout-todo{--ccolor: 0, 184, 212}.callout-tip,.callout-hint,.callout-important{--ccolor: 0, 191, 165}.callout-success,.callout-check,.callout-done{--ccolor: 0, 200, 83}.callout-question,.callout-help,.callout-faq{--ccolor: 100, 221, 23}.callout-warning,.callout-caution,.callout-attention{--ccolor: 255, 145, 0}.callout-failure,.callout-fail,.callout-missing{--ccolor: 255, 82, 82}.callout-danger,.callout-error{--ccolor: 255, 23, 68}.callout-bug{--ccolor: 245, 0, 87}.callout-quote,.callout-cite{--ccolor: 158, 158, 158} diff --git a/_app/immutable/assets/12.3bfdc038.css b/_app/immutable/assets/12.3bfdc038.css new file mode 100644 index 00000000..08ed9615 --- /dev/null +++ b/_app/immutable/assets/12.3bfdc038.css @@ -0,0 +1 @@ +.tag-item.svelte-zwjyck p{margin:0;display:inline-block} diff --git a/_app/immutable/assets/13.a9b58653.css b/_app/immutable/assets/13.a9b58653.css new file mode 100644 index 00000000..55b5de09 --- /dev/null +++ b/_app/immutable/assets/13.a9b58653.css @@ -0,0 +1 @@ +.tag-content.svelte-2aohp6{text-align:left} diff --git a/_app/immutable/assets/2.9c80008b.css b/_app/immutable/assets/2.9c80008b.css new file mode 100644 index 00000000..e64a7188 --- /dev/null +++ b/_app/immutable/assets/2.9c80008b.css @@ -0,0 +1 @@ +.column.svelte-l4ofsj{flex:none;min-width:18%;margin:.5rem}.content.svelte-1t74pc strong{font-size:1.25rem;color:var(--font-color-light);font-weight:400}.profile-picture.svelte-1t74pc{border-radius:50%;width:128px;height:128px;display:block}.column.svelte-1t74pc{display:flex;justify-content:center;min-width:min(100%,370px)}.hidden.svelte-1t74pc{display:none} diff --git a/_app/immutable/assets/4.32b0d823.css b/_app/immutable/assets/4.32b0d823.css new file mode 100644 index 00000000..a327fd26 --- /dev/null +++ b/_app/immutable/assets/4.32b0d823.css @@ -0,0 +1 @@ +.outro.svelte-1ysgxol{margin-top:4em;font-size:.8em;color:var(--font-color-light)} diff --git a/_app/immutable/assets/5.9fa4e00e.css b/_app/immutable/assets/5.9fa4e00e.css new file mode 100644 index 00000000..a08eba2c --- /dev/null +++ b/_app/immutable/assets/5.9fa4e00e.css @@ -0,0 +1 @@ +h1.svelte-1s7r06i{text-align:center}.wrap.svelte-1s7r06i{display:flex;flex-direction:column;justify-content:center;align-items:center;margin:1rem 0;gap:6rem}.ico.svelte-1s7r06i{font-size:2rem}.item.svelte-1s7r06i{display:flex;flex-direction:row;align-items:center;padding:1rem;gap:1rem;border-radius:1rem}.text.svelte-1s7r06i{display:flex;flex-direction:column;gap:.25rem}.handle.svelte-1s7r06i{font-size:.8rem;color:var(--font-color-muted)}.grid.svelte-1s7r06i{display:grid;grid-template-columns:repeat(2,1fr);grid-gap:3rem;background-color:var(--color-ui-3);padding:1rem;border-radius:1rem} diff --git a/_app/immutable/assets/6.83f5b710.css b/_app/immutable/assets/6.83f5b710.css new file mode 100644 index 00000000..7a8f26c3 --- /dev/null +++ b/_app/immutable/assets/6.83f5b710.css @@ -0,0 +1 @@ +h1.svelte-1kpxhvr{text-align:center}.wrap.svelte-1kpxhvr{display:flex;justify-content:center;align-items:center;margin:2rem 0}.ico.svelte-1kpxhvr{font-size:2rem}.item.svelte-1kpxhvr{display:flex;flex-direction:row;align-items:center;padding:1rem;gap:1rem;border-radius:1rem}.text.svelte-1kpxhvr{display:flex;flex-direction:column;gap:.25rem}.handle.svelte-1kpxhvr{font-size:.8rem;color:var(--font-color-muted)}.grid.svelte-1kpxhvr{display:grid;grid-template-columns:repeat(2,1fr);grid-gap:3rem;background-color:var(--color-ui-3);padding:1rem;border-radius:1rem} diff --git a/_app/immutable/assets/MarkdownSite.52351ed7.css b/_app/immutable/assets/MarkdownSite.52351ed7.css new file mode 100644 index 00000000..c6d26339 --- /dev/null +++ b/_app/immutable/assets/MarkdownSite.52351ed7.css @@ -0,0 +1 @@ +.author-info.svelte-gqjfyc.svelte-gqjfyc{margin-top:5rem;padding:1rem;background-color:var(--background-color-light-2);display:flex;justify-content:center;align-items:center;gap:2rem;padding:2rem}.author-info.svelte-gqjfyc>:first-child{margin-top:0}.author-avatar.svelte-gqjfyc img.svelte-gqjfyc{width:7rem;height:auto;border-radius:50%;overflow:hidden}.author-detail.svelte-gqjfyc.svelte-gqjfyc{display:flex;flex-direction:row;align-items:baseline;gap:1rem;flex-wrap:wrap;font-size:.7rem}.hidden.svelte-gqjfyc.svelte-gqjfyc{display:none}.comment-input.svelte-1xby3i5.svelte-1xby3i5{display:flex;flex-direction:row;gap:2rem;margin-bottom:2rem;padding:1rem}.error.svelte-1xby3i5.svelte-1xby3i5{padding:1rem;margin:1rem;border-radius:.5rem;color:var(--color-ui-9);background-color:var(--background-color-light-2)}.comment-input.svelte-1xby3i5 .avatar.svelte-1xby3i5{background-color:var(--color-ui-6)}.comment-input-content.svelte-1xby3i5.svelte-1xby3i5{display:flex;flex-direction:column;gap:.5rem;align-items:stretch;flex-grow:1}textarea.svelte-1xby3i5.svelte-1xby3i5{resize:vertical;min-height:10rem}.avatar.svelte-1xby3i5.svelte-1xby3i5{font-size:1.5rem;display:inline-block;background-color:var(--background-color-light-2);padding:1rem;width:3.5rem;height:3.5rem;text-align:center;border-radius:50%}.horizontal.svelte-1xby3i5.svelte-1xby3i5{display:flex;flex-direction:row;gap:.5rem;align-items:center}.checkbox.svelte-1xby3i5.svelte-1xby3i5{display:flex;flex-direction:row;align-items:center;color:var(--font-color-light)}.checkbox.svelte-1xby3i5 input.svelte-1xby3i5{width:30px}.reply-to.svelte-1xby3i5.svelte-1xby3i5{color:var(--font-color-light);display:flex}blockquote.svelte-1xby3i5.svelte-1xby3i5{white-space:pre-wrap}.reply-to.svelte-1xby3i5 button.svelte-1xby3i5{width:100%;margin-left:1rem;padding:.25rem}h2.svelte-z0478z.svelte-z0478z{margin-top:3rem}.comment-list.svelte-z0478z.svelte-z0478z{list-style:none;padding:0}.comment.svelte-z0478z.svelte-z0478z{padding:1rem;display:flex;flex-direction:row;gap:2rem}.comment.svelte-z0478z.svelte-z0478z:not(:last-child){margin-bottom:2rem;border-bottom:1px solid var(--line-color)}.avatar.svelte-z0478z.svelte-z0478z{font-size:1.5rem;display:inline-block;background-color:var(--background-color-light-2);padding:1rem;width:3.5rem;height:3.5rem;text-align:center;border-radius:50%}.comment.svelte-z0478z h3.svelte-z0478z{display:inline;margin-bottom:0}.time.svelte-z0478z.svelte-z0478z{color:var(--color-ui-6);font-size:.9rem;text-decoration:underline}.comment-card.svelte-z0478z.svelte-z0478z{width:100%}.comment-content.svelte-z0478z.svelte-z0478z{white-space:pre-wrap}.comment-header.svelte-z0478z.svelte-z0478z{width:100%;display:flex;flex-direction:row;gap:.5rem;align-items:baseline;flex-grow:1;justify-content:baseline}button.svelte-z0478z.svelte-z0478z{margin-left:2rem;padding:.5rem 0rem;font-size:.8rem;color:var(--font-color-light)}.reply-notice.svelte-z0478z.svelte-z0478z{font-size:.8rem;color:var(--color-ui-6)}figure.svelte-1vgyhou.svelte-1vgyhou{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:.25rem}figcaption.svelte-1vgyhou.svelte-1vgyhou{font-size:.8rem;color:var(--font-color-muted)}img.svelte-1vgyhou.svelte-1vgyhou{max-width:100%;margin-bottom:0}.notification.svelte-1vgyhou.svelte-1vgyhou{background-color:var(--color-ui-12);padding:1rem;border-radius:10px;text-align:center}.by.svelte-1vgyhou.svelte-1vgyhou{font-size:.8rem;color:var(--font-color-light);margin-top:.5rem}.by.svelte-1vgyhou>.svelte-1vgyhou{display:inline-block;margin-right:1rem}.links.svelte-1vgyhou.svelte-1vgyhou{margin-bottom:2rem}.content.svelte-1vgyhou.svelte-1vgyhou{color:var(--font-color-muted);margin-top:4rem}.hidden.svelte-1vgyhou.svelte-1vgyhou{display:none} diff --git a/_app/immutable/assets/PostCardList.fbe0a418.css b/_app/immutable/assets/PostCardList.fbe0a418.css new file mode 100644 index 00000000..718c0ccb --- /dev/null +++ b/_app/immutable/assets/PostCardList.fbe0a418.css @@ -0,0 +1 @@ +h3.svelte-sf0lxt.svelte-sf0lxt{font-size:1rem;margin-bottom:0}.date.svelte-sf0lxt.svelte-sf0lxt{font-size:.8rem;color:var(--font-color-light);margin-top:.5rem}.card.svelte-sf0lxt.svelte-sf0lxt{display:flex;flex-direction:column;justify-content:start}.spacer.svelte-sf0lxt.svelte-sf0lxt{flex-grow:1;min-height:0px}.card-content.svelte-sf0lxt.svelte-sf0lxt{padding:1rem;text-align:left;overflow-wrap:break-word}.links-wrap.svelte-sf0lxt.svelte-sf0lxt{display:flex;flex-direction:column;gap:.5rem}.links.svelte-sf0lxt.svelte-sf0lxt{display:flex;justify-content:space-between;min-height:48px;gap:1rem}.links.svelte-sf0lxt span.svelte-sf0lxt{display:flex;flex-basis:0;flex-grow:1;flex-shrink:0;justify-content:center;align-items:center;background-color:var(--background-color-light-2);border-radius:.5rem}img.svelte-sf0lxt.svelte-sf0lxt{max-width:100%}.proj-wrap.svelte-yd3yl6{display:grid;grid-template-columns:repeat(auto-fill,minmax(350px,1fr));gap:1rem;justify-items:stretch;align-items:stretch;justify-content:center} diff --git a/_app/immutable/assets/_layout.8d503a92.css b/_app/immutable/assets/_layout.8d503a92.css new file mode 100644 index 00000000..eb7691ec --- /dev/null +++ b/_app/immutable/assets/_layout.8d503a92.css @@ -0,0 +1 @@ +.header-checkbox.svelte-1qkwc1i.svelte-1qkwc1i{display:none}.toggle.svelte-1qkwc1i.svelte-1qkwc1i{position:relative;cursor:pointer;display:block;width:36px;height:36px}.toggle.svelte-1qkwc1i.svelte-1qkwc1i:hover{background-color:var(--background-color-light-2)}.toggle.svelte-1qkwc1i span.svelte-1qkwc1i{position:absolute;display:block;width:26px;right:5px;height:2px;background-color:#fff;border-radius:2px;transition:background-color .2s ease-in-out}.toggle.svelte-1qkwc1i span.svelte-1qkwc1i:nth-child(1){top:10px}.toggle.svelte-1qkwc1i span.svelte-1qkwc1i:nth-child(2){top:calc(50% - 1px)}.toggle.svelte-1qkwc1i span.svelte-1qkwc1i:nth-child(3){bottom:10px}.mobile-dropdown.svelte-1qkwc1i.svelte-1qkwc1i{position:absolute;top:80px;right:10px;display:flex;flex-direction:column;background-color:var(--background-color-light-2);border-radius:.5rem}.navbar.svelte-1qkwc1i.svelte-1qkwc1i{height:60px;padding:1rem var(--padding-side);display:flex;align-items:stretch;gap:var(--header-gap);background-color:var(--background-color-light);position:relative}.navbar-brand.svelte-1qkwc1i.svelte-1qkwc1i{font-weight:bolder;display:none}.navbar-brand.svelte-1qkwc1i .navbar-item.svelte-1qkwc1i{color:var(--color-syntax-1);font-size:1.1rem;letter-spacing:2px;margin-right:2rem}.navbar-brand.svelte-1qkwc1i.svelte-1qkwc1i,.navbar-menu.svelte-1qkwc1i.svelte-1qkwc1i{display:flex;align-items:center;gap:var(--header-gap)}.navbar-menu.mobile.svelte-1qkwc1i.svelte-1qkwc1i{display:none}@media only screen and (max-width: 800px){.navbar-menu.desktop.svelte-1qkwc1i.svelte-1qkwc1i{display:none}.navbar-menu.mobile.svelte-1qkwc1i.svelte-1qkwc1i{display:flex}.navbar.svelte-1qkwc1i.svelte-1qkwc1i{justify-content:space-between}}.navbar-item.svelte-1qkwc1i.svelte-1qkwc1i{padding:1rem;display:inline;text-decoration:none;text-align:center;color:var(--font-color-light);border-radius:.5rem}.navbar-brand.svelte-1qkwc1i .navbar-item.svelte-1qkwc1i{padding:0;background-color:transparent!important}.navbar-item.svelte-1qkwc1i.svelte-1qkwc1i:hover{background-color:var(--background-color-light-2)}.mobile.svelte-1qkwc1i .navbar-item.svelte-1qkwc1i:hover{background-color:var(--background-color-light-3)}.copyright.svelte-18j7aqw{text-align:center;margin-top:3rem;font-size:.8rem}footer.svelte-18j7aqw{margin-top:2rem;border-top:dashed 2px var(--color-ui-12);background-color:var(--background-color-light);color:var(--font-color-muted);padding:2rem}/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{--color-syntax-1: #b877db;--color-syntax-2: #25b2bc;--color-syntax-3: #e95678;--color-syntax-4: #f09383;--color-syntax-5: #fab795;--color-syntax-6: #fac29a;--color-ui-1: #16161c;--color-ui-2: #1a1c23;--color-ui-3: #1c1e26;--color-ui-4: #232530;--color-ui-5: #2e303e;--color-ui-6: #6c6f93;--color-ui-7: #e9436f;--color-ui-8: #e95379;--color-ui-9: #f43e5c;--color-ui-10: #09f7a0;--color-ui-11: #27d796;--color-ui-12: #21bfc2;--color-ui-13: #fab28e;--background-color: var(--color-ui-1);--background-color-light: var(--color-ui-3);--background-color-light-2: var(--color-ui-5);--background-color-light-3: var(--color-ui-6);--font-color: white;--font-color-muted: #dcdcdc;--font-color-light: var(--color-syntax-6);--font-color-link: var(--color-syntax-2);--font-color-link-hover: var(--color-syntax-3);--line-color: var(--color-ui-6);--shadow-color: var(--color-ui-11);--header-gap: .5rem;--padding-side: 2rem;font-family:Fira Code,Consolas,Courier New,monospace;font-weight:400;font-size:1em;line-height:1.5;color:var(--font-color)}*{box-sizing:border-box}body{background-color:var(--background-color)}h1,h2{color:var(--color-syntax-1)}h3{color:var(--color-syntax-3)}h4{color:var(--color-syntax-4)}h5{color:var(--color-syntax-5)}pre{background-color:var(--background-color-light);overflow-x:auto;padding:1rem;border-radius:1rem}code{background-color:var(--background-color-light-2);padding:0rem .25rem;color:var(--color-syntax-4);border-radius:5px}a{text-decoration:none;color:var(--font-color-link)}a:hover{color:var(--font-color-link-hover)}article{margin:0}blockquote{border-left:4px solid var(--color-ui-11);padding:.5rem 1rem;background-color:var(--background-color-light);margin:1rem}input,textarea,button{color:var(--font-color);width:100%;max-width:100%;padding:.5rem;border-radius:5px;border:0px;outline:none;background-color:var(--color-ui-3);resize:none}button{cursor:pointer}button:hover{background-color:var(--color-ui-4)}.container img{max-width:100%;display:block;margin:2rem auto}.container{padding:0 var(--padding-side);max-width:1344px;margin:0 auto}.section{padding:3rem 0rem}.columns{display:flex;justify-content:center;row-gap:2rem}.columns .column{flex:1}.columns .column.column-shrink{flex:0}.columns.multiline{flex-wrap:wrap}.has-text-centered{text-align:center}.card{border-radius:1rem;background-color:var(--background-color-light);padding:.5rem 1.25rem}.links p{margin:0}.tags{display:flex;flex-wrap:wrap;justify-content:center;gap:.5rem}.tag{padding:.2rem .5rem;border-radius:.25rem;background-color:var(--background-color-light-2);color:var(--font-color-light)}body::-webkit-scrollbar{width:12px}html{-webkit-scrollbar-width:thin;-moz-scrollbar-width:thin;-ms-scrollbar-width:thin;scrollbar-width:thin;-webkit-scrollbar-color:var(--color-syntax-2) var(--color-ui-1);-moz-scrollbar-color:var(--color-syntax-2) var(--color-ui-1);-ms-scrollbar-color:var(--color-syntax-2) var(--color-ui-1);scrollbar-color:var(--color-syntax-2) var(--color-ui-1)}body::-webkit-scrollbar-track{background:var(--color-ui-1)}body::-webkit-scrollbar-thumb{background-color:var(--color-syntax-2);border-radius:6px;border:3px solid var(--color-ui-1)}.callout{background-color:var(--background-color-light-2);padding:0;padding-bottom:.5rem;border-left:4px solid rgba(var(--ccolor),1);--opacity: .2;--opacity-icon: .4;--ccolor: rgba(158, 158, 158, var(--opacity))}.callout>*{padding-left:1rem;padding-right:1rem}.callout-title{display:flex;align-items:center;width:100%;font-weight:700;background-color:rgba(var(--ccolor),var(--opacity));padding:.5rem 1rem;gap:1rem}.callout-icon{width:1.2rem;height:1.2rem;margin:0!important;padding:0;fill:currentColor;opacity:var(--opacity-icon)}.callout-note{--ccolor: 68, 138, 255}.callout-abstract,.callout-summary,.callout-tldr{--ccolor: 0, 176, 255}.callout-info,.callout-todo{--ccolor: 0, 184, 212}.callout-tip,.callout-hint,.callout-important{--ccolor: 0, 191, 165}.callout-success,.callout-check,.callout-done{--ccolor: 0, 200, 83}.callout-question,.callout-help,.callout-faq{--ccolor: 100, 221, 23}.callout-warning,.callout-caution,.callout-attention{--ccolor: 255, 145, 0}.callout-failure,.callout-fail,.callout-missing{--ccolor: 255, 82, 82}.callout-danger,.callout-error{--ccolor: 255, 23, 68}.callout-bug{--ccolor: 245, 0, 87}.callout-quote,.callout-cite{--ccolor: 158, 158, 158} diff --git a/_app/immutable/assets/_page.32b0d823.css b/_app/immutable/assets/_page.32b0d823.css new file mode 100644 index 00000000..a327fd26 --- /dev/null +++ b/_app/immutable/assets/_page.32b0d823.css @@ -0,0 +1 @@ +.outro.svelte-1ysgxol{margin-top:4em;font-size:.8em;color:var(--font-color-light)} diff --git a/_app/immutable/assets/_page.3bfdc038.css b/_app/immutable/assets/_page.3bfdc038.css new file mode 100644 index 00000000..08ed9615 --- /dev/null +++ b/_app/immutable/assets/_page.3bfdc038.css @@ -0,0 +1 @@ +.tag-item.svelte-zwjyck p{margin:0;display:inline-block} diff --git a/_app/immutable/assets/_page.83f5b710.css b/_app/immutable/assets/_page.83f5b710.css new file mode 100644 index 00000000..7a8f26c3 --- /dev/null +++ b/_app/immutable/assets/_page.83f5b710.css @@ -0,0 +1 @@ +h1.svelte-1kpxhvr{text-align:center}.wrap.svelte-1kpxhvr{display:flex;justify-content:center;align-items:center;margin:2rem 0}.ico.svelte-1kpxhvr{font-size:2rem}.item.svelte-1kpxhvr{display:flex;flex-direction:row;align-items:center;padding:1rem;gap:1rem;border-radius:1rem}.text.svelte-1kpxhvr{display:flex;flex-direction:column;gap:.25rem}.handle.svelte-1kpxhvr{font-size:.8rem;color:var(--font-color-muted)}.grid.svelte-1kpxhvr{display:grid;grid-template-columns:repeat(2,1fr);grid-gap:3rem;background-color:var(--color-ui-3);padding:1rem;border-radius:1rem} diff --git a/_app/immutable/assets/_page.9c80008b.css b/_app/immutable/assets/_page.9c80008b.css new file mode 100644 index 00000000..e64a7188 --- /dev/null +++ b/_app/immutable/assets/_page.9c80008b.css @@ -0,0 +1 @@ +.column.svelte-l4ofsj{flex:none;min-width:18%;margin:.5rem}.content.svelte-1t74pc strong{font-size:1.25rem;color:var(--font-color-light);font-weight:400}.profile-picture.svelte-1t74pc{border-radius:50%;width:128px;height:128px;display:block}.column.svelte-1t74pc{display:flex;justify-content:center;min-width:min(100%,370px)}.hidden.svelte-1t74pc{display:none} diff --git a/_app/immutable/assets/_page.9fa4e00e.css b/_app/immutable/assets/_page.9fa4e00e.css new file mode 100644 index 00000000..a08eba2c --- /dev/null +++ b/_app/immutable/assets/_page.9fa4e00e.css @@ -0,0 +1 @@ +h1.svelte-1s7r06i{text-align:center}.wrap.svelte-1s7r06i{display:flex;flex-direction:column;justify-content:center;align-items:center;margin:1rem 0;gap:6rem}.ico.svelte-1s7r06i{font-size:2rem}.item.svelte-1s7r06i{display:flex;flex-direction:row;align-items:center;padding:1rem;gap:1rem;border-radius:1rem}.text.svelte-1s7r06i{display:flex;flex-direction:column;gap:.25rem}.handle.svelte-1s7r06i{font-size:.8rem;color:var(--font-color-muted)}.grid.svelte-1s7r06i{display:grid;grid-template-columns:repeat(2,1fr);grid-gap:3rem;background-color:var(--color-ui-3);padding:1rem;border-radius:1rem} diff --git a/_app/immutable/assets/_page.a9b58653.css b/_app/immutable/assets/_page.a9b58653.css new file mode 100644 index 00000000..55b5de09 --- /dev/null +++ b/_app/immutable/assets/_page.a9b58653.css @@ -0,0 +1 @@ +.tag-content.svelte-2aohp6{text-align:left} diff --git a/_app/immutable/assets/callout.cf69ba0f.svg b/_app/immutable/assets/callout.cf69ba0f.svg new file mode 100644 index 00000000..bb19ca2a --- /dev/null +++ b/_app/immutable/assets/callout.cf69ba0f.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/_app/immutable/assets/failure.014ad831.svg b/_app/immutable/assets/failure.014ad831.svg new file mode 100644 index 00000000..c8bbba90 --- /dev/null +++ b/_app/immutable/assets/failure.014ad831.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/_app/immutable/assets/hint.0095cf06.svg b/_app/immutable/assets/hint.0095cf06.svg new file mode 100644 index 00000000..a637d676 --- /dev/null +++ b/_app/immutable/assets/hint.0095cf06.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/_app/immutable/assets/note.97d6c862.svg b/_app/immutable/assets/note.97d6c862.svg new file mode 100644 index 00000000..35a7bee4 --- /dev/null +++ b/_app/immutable/assets/note.97d6c862.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/_app/immutable/assets/question.801abb10.svg b/_app/immutable/assets/question.801abb10.svg new file mode 100644 index 00000000..a37747cb --- /dev/null +++ b/_app/immutable/assets/question.801abb10.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/_app/immutable/assets/success.d3ec86d5.svg b/_app/immutable/assets/success.d3ec86d5.svg new file mode 100644 index 00000000..7dd4f027 --- /dev/null +++ b/_app/immutable/assets/success.d3ec86d5.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/_app/immutable/assets/tldr.d9189f53.svg b/_app/immutable/assets/tldr.d9189f53.svg new file mode 100644 index 00000000..f512208e --- /dev/null +++ b/_app/immutable/assets/tldr.d9189f53.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/_app/immutable/assets/todo.4179c845.svg b/_app/immutable/assets/todo.4179c845.svg new file mode 100644 index 00000000..e8016575 --- /dev/null +++ b/_app/immutable/assets/todo.4179c845.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/_app/immutable/assets/warn.048dfb23.svg b/_app/immutable/assets/warn.048dfb23.svg new file mode 100644 index 00000000..a4579f4a --- /dev/null +++ b/_app/immutable/assets/warn.048dfb23.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/_app/immutable/chunks/MarkdownSite.7d9e3e05.js b/_app/immutable/chunks/MarkdownSite.7d9e3e05.js new file mode 100644 index 00000000..ea3e9bc6 --- /dev/null +++ b/_app/immutable/chunks/MarkdownSite.7d9e3e05.js @@ -0,0 +1,7 @@ +import{S as Re,i as He,s as je,k as h,a as I,q as P,l as _,m as v,h as c,c as A,r as L,N as qe,n as f,b as V,C as o,E as Qe,y as ze,z as Ve,A as Be,O as Me,D as De,P as Lt,g as de,d as ge,B as Oe,F as Dt,Q as Ct,u as me,v as wt,f as St,L as Ue,e as Se,G as Mt,M as zt,H as Vt,I as Bt,J as Ot}from"./index.8c1dbec0.js";import{b as Xe}from"./paths.c95190e0.js";import{m as Le}from"./mf2.47734ed4.js";import{F as Nt}from"./fa.9f83fec8.js";function qt(a){let e,l,t,n,i,s,r,u=a[0].html+"",b,p,k,T=Le.gender_identity+"",E,B,w,g=Le.email+"",D,m,y,C=Le.url+"",S;return{c(){e=h("div"),l=h("div"),t=h("img"),i=I(),s=h("div"),r=h("p"),b=I(),p=h("div"),k=h("span"),E=P(T),B=I(),w=h("a"),D=P(g),m=I(),y=h("a"),S=P(C),this.h()},l(M){e=_(M,"DIV",{class:!0});var U=v(e);l=_(U,"DIV",{class:!0});var J=v(l);t=_(J,"IMG",{src:!0,alt:!0,class:!0}),J.forEach(c),i=A(U),s=_(U,"DIV",{});var X=v(s);r=_(X,"P",{class:!0});var W=v(r);W.forEach(c),b=A(X),p=_(X,"DIV",{class:!0});var O=v(p);k=_(O,"SPAN",{class:!0});var F=v(k);E=L(F,T),F.forEach(c),B=A(O),w=_(O,"A",{href:!0,class:!0});var q=v(w);D=L(q,g),q.forEach(c),m=A(O),y=_(O,"A",{class:!0,href:!0});var N=v(y);S=L(N,C),N.forEach(c),O.forEach(c),X.forEach(c),U.forEach(c),this.h()},h(){qe(t.src,n=Le.photo)||f(t,"src",n),f(t,"alt",`Photo of ${Le.name}`),f(t,"class","u-photo svelte-gqjfyc"),f(l,"class","author-avatar svelte-gqjfyc"),f(r,"class","author-bio p-note"),f(k,"class","hidden p-gender-identity svelte-gqjfyc"),f(w,"href",`mailto:${Le.email}`),f(w,"class","p-email"),f(y,"class","p-url"),f(y,"href",Le.url),f(p,"class","author-detail svelte-gqjfyc"),f(e,"class","author-info p-author h-card svelte-gqjfyc")},m(M,U){V(M,e,U),o(e,l),o(l,t),o(e,i),o(e,s),o(s,r),r.innerHTML=u,o(s,b),o(s,p),o(p,k),o(k,E),o(p,B),o(p,w),o(w,D),o(p,m),o(p,y),o(y,S)},p(M,[U]){U&1&&u!==(u=M[0].html+"")&&(r.innerHTML=u)},i:Qe,o:Qe,d(M){M&&c(e)}}}function Ut(a,e,l){let{about:t}=e;return a.$$set=n=>{"about"in n&&l(0,t=n.about)},[t]}class Rt extends Re{constructor(e){super(),He(this,e,Ut,qt,je,{about:0})}}function Ze(a){let e,l,t,n,i,s=a[0].name+"",r,u,b,p,k,T,E,B=a[0].content+"",w,g,D;return{c(){e=h("div"),l=h("span"),t=h("span"),n=P(`Replying to + `),i=h("b"),r=P(s),u=I(),b=h("span"),p=h("button"),k=P("X"),T=I(),E=h("blockquote"),w=P(B),this.h()},l(m){e=_(m,"DIV",{});var y=v(e);l=_(y,"SPAN",{class:!0});var C=v(l);t=_(C,"SPAN",{});var S=v(t);n=L(S,`Replying to + `),i=_(S,"B",{});var M=v(i);r=L(M,s),M.forEach(c),S.forEach(c),u=A(C),b=_(C,"SPAN",{});var U=v(b);p=_(U,"BUTTON",{class:!0});var J=v(p);k=L(J,"X"),J.forEach(c),U.forEach(c),C.forEach(c),T=A(y),E=_(y,"BLOCKQUOTE",{class:!0});var X=v(E);w=L(X,B),X.forEach(c),y.forEach(c),this.h()},h(){f(p,"class","svelte-1xby3i5"),f(l,"class","reply-to svelte-1xby3i5"),f(E,"class","svelte-1xby3i5")},m(m,y){V(m,e,y),o(e,l),o(l,t),o(t,n),o(t,i),o(i,r),o(l,u),o(l,b),o(b,p),o(p,k),o(e,T),o(e,E),o(E,w),g||(D=De(p,"click",a[13]),g=!0)},p(m,y){y&1&&s!==(s=m[0].name+"")&&me(r,s),y&1&&B!==(B=m[0].content+"")&&me(w,B)},d(m){m&&c(e),g=!1,D()}}}function xe(a){let e,l;return{c(){e=h("div"),l=P(a[5]),this.h()},l(t){e=_(t,"DIV",{class:!0});var n=v(e);l=L(n,a[5]),n.forEach(c),this.h()},h(){f(e,"class","error svelte-1xby3i5")},m(t,n){V(t,e,n),o(e,l)},p(t,n){n&32&&me(l,t[5])},d(t){t&&c(e)}}}function Ht(a){let e,l,t,n,i,s,r,u,b,p,k,T,E,B,w,g,D,m,y,C,S,M,U,J,X,W,O,F,q,N,G,ve,le,ne,be,Ee;n=new Nt({props:{icon:"user"}});let K=a[0]&&Ze(a),j=a[5]&&xe(a);return{c(){e=h("div"),l=h("div"),t=h("span"),ze(n.$$.fragment),i=I(),s=h("div"),r=h("label"),u=P("Name"),b=I(),p=h("input"),k=I(),T=h("div"),E=h("label"),B=P("E-mail"),w=I(),g=h("div"),D=h("input"),m=I(),y=h("div"),C=h("input"),S=I(),M=h("label"),U=P("Notify me on replies"),J=I(),K&&K.c(),X=I(),W=h("label"),O=P("Comment"),F=I(),q=h("textarea"),N=I(),G=h("button"),ve=P("Post"),le=I(),j&&j.c(),this.h()},l(R){e=_(R,"DIV",{class:!0});var $=v(e);l=_($,"DIV",{id:!0,class:!0});var ye=v(l);t=_(ye,"SPAN",{class:!0});var pe=v(t);Ve(n.$$.fragment,pe),pe.forEach(c),i=A(ye),s=_(ye,"DIV",{class:!0});var Y=v(s);r=_(Y,"LABEL",{for:!0});var Ie=v(r);u=L(Ie,"Name"),Ie.forEach(c),b=A(Y),p=_(Y,"INPUT",{id:!0,type:!0,placeholder:!0}),k=A(Y),T=_(Y,"DIV",{class:!0});var ke=v(T);E=_(ke,"LABEL",{for:!0});var Ne=v(E);B=L(Ne,"E-mail"),Ne.forEach(c),w=A(ke),g=_(ke,"DIV",{class:!0});var Ae=v(g);D=_(Ae,"INPUT",{id:!0,type:!0,placeholder:!0}),m=A(Ae),y=_(Ae,"DIV",{class:!0});var x=v(y);C=_(x,"INPUT",{type:!0,id:!0,class:!0}),S=A(x),M=_(x,"LABEL",{for:!0});var ee=v(M);U=L(ee,"Notify me on replies"),ee.forEach(c),x.forEach(c),Ae.forEach(c),ke.forEach(c),J=A(Y),K&&K.l(Y),X=A(Y),W=_(Y,"LABEL",{for:!0});var te=v(W);O=L(te,"Comment"),te.forEach(c),F=A(Y),q=_(Y,"TEXTAREA",{id:!0,placeholder:!0,class:!0}),v(q).forEach(c),N=A(Y),G=_(Y,"BUTTON",{});var Te=v(G);ve=L(Te,"Post"),Te.forEach(c),le=A(Y),j&&j.l(Y),Y.forEach(c),ye.forEach(c),$.forEach(c),this.h()},h(){f(t,"class","avatar svelte-1xby3i5"),f(r,"for","comment-input-name"),f(p,"id","commentName"),f(p,"type","text"),f(p,"placeholder","Name"),f(E,"for","comment-input-email"),f(D,"id","commentEmail"),f(D,"type","email"),f(D,"placeholder","Email (optional)"),f(C,"type","checkbox"),f(C,"id","comment-input-notify"),f(C,"class","svelte-1xby3i5"),f(M,"for","comment-input-notify"),f(y,"class","checkbox svelte-1xby3i5"),f(g,"class","horizontal svelte-1xby3i5"),f(T,"class","comment-input-content svelte-1xby3i5"),f(W,"for","commentContent"),f(q,"id","commentContent"),f(q,"placeholder","Comment"),f(q,"class","svelte-1xby3i5"),f(s,"class","comment-input-content svelte-1xby3i5"),f(l,"id","comment-section"),f(l,"class","comment-input svelte-1xby3i5"),f(e,"class","comment-form-wrap")},m(R,$){V(R,e,$),o(e,l),o(l,t),Be(n,t,null),o(l,i),o(l,s),o(s,r),o(r,u),o(s,b),o(s,p),Me(p,a[1]),o(s,k),o(s,T),o(T,E),o(E,B),o(T,w),o(T,g),o(g,D),Me(D,a[2]),o(g,m),o(g,y),o(y,C),C.checked=a[4],o(y,S),o(y,M),o(M,U),o(s,J),K&&K.m(s,null),o(s,X),o(s,W),o(W,O),o(s,F),o(s,q),Me(q,a[3]),o(s,N),o(s,G),o(G,ve),o(s,le),j&&j.m(s,null),ne=!0,be||(Ee=[De(p,"input",a[10]),De(D,"input",a[11]),De(C,"change",a[12]),De(q,"input",a[14]),De(G,"click",Lt(a[7]))],be=!0)},p(R,[$]){$&2&&p.value!==R[1]&&Me(p,R[1]),$&4&&D.value!==R[2]&&Me(D,R[2]),$&16&&(C.checked=R[4]),R[0]?K?K.p(R,$):(K=Ze(R),K.c(),K.m(s,X)):K&&(K.d(1),K=null),$&8&&Me(q,R[3]),R[5]?j?j.p(R,$):(j=xe(R),j.c(),j.m(s,null)):j&&(j.d(1),j=null)},i(R){ne||(de(n.$$.fragment,R),ne=!0)},o(R){ge(n.$$.fragment,R),ne=!1},d(R){R&&c(e),Oe(n),K&&K.d(),j&&j.d(),be=!1,Dt(Ee)}}}function jt(a,e,l){let{url:t}=e,{page:n}=e,{reply:i}=e;const s=Ct();let r,u,b,p,k=null;async function T(){if(l(1,r=r==null?void 0:r.trim()),l(3,b=b==null?void 0:b.trim()),l(2,u=u==null?void 0:u.trim()),!r){l(5,k="Please enter a name");return}if(!b){l(5,k="Please enter a comment");return}if(!u&&p){l(5,k="Please enter an email if you want to be notified of replies");return}if(u&&!u.includes("@")){l(5,k="Please enter a valid email");return}const m=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:r,email:u,content:b,page:n,notify:p,reply_to:i&&i.id||void 0})});if(m.ok){const y=await m.json();s("newcomment",y),l(5,k=null),l(3,b="")}else{const y=await m.text();console.error(y),l(5,k=y)}}function E(){r=this.value,l(1,r)}function B(){u=this.value,l(2,u)}function w(){p=this.checked,l(4,p)}const g=()=>s("unselect-reply");function D(){b=this.value,l(3,b)}return a.$$set=m=>{"url"in m&&l(8,t=m.url),"page"in m&&l(9,n=m.page),"reply"in m&&l(0,i=m.reply)},[i,r,u,b,p,k,s,T,t,n,E,B,w,g,D]}class Ft extends Re{constructor(e){super(),He(this,e,jt,Ht,je,{url:8,page:9,reply:0})}}function et(a,e,l){const t=a.slice();return t[13]=e[l],t}function tt(a){let e,l;return{c(){e=h("p"),l=P("Mentioned this post"),this.h()},l(t){e=_(t,"P",{class:!0});var n=v(e);l=L(n,"Mentioned this post"),n.forEach(c),this.h()},h(){f(e,"class","comment-text")},m(t,n){V(t,e,n),o(e,l)},d(t){t&&c(e)}}}function lt(a){let e,l,t,n,i;function s(){return a[9](a[13])}return{c(){e=h("span"),l=h("button"),t=P("Reply"),this.h()},l(r){e=_(r,"SPAN",{});var u=v(e);l=_(u,"BUTTON",{class:!0});var b=v(l);t=L(b,"Reply"),b.forEach(c),u.forEach(c),this.h()},h(){f(l,"class","svelte-z0478z")},m(r,u){V(r,e,u),o(e,l),o(l,t),n||(i=De(l,"click",s),n=!0)},p(r,u){a=r},d(r){r&&c(e),n=!1,i()}}}function nt(a){var u;let e,l,t,n=((u=a[2].find(r))==null?void 0:u.name)+"",i,s;function r(...b){return a[10](a[13],...b)}return{c(){e=h("span"),l=P(`In reply to + `),t=h("a"),i=P(n),this.h()},l(b){e=_(b,"SPAN",{class:!0});var p=v(e);l=L(p,`In reply to + `),t=_(p,"A",{href:!0});var k=v(t);i=L(k,n),k.forEach(c),p.forEach(c),this.h()},h(){f(t,"href",s=`#${a[13].replyTo}`),f(e,"class","reply-notice svelte-z0478z")},m(b,p){V(b,e,p),o(e,l),o(e,t),o(t,i)},p(b,p){var k;a=b,p&4&&n!==(n=((k=a[2].find(r))==null?void 0:k.name)+"")&&me(i,n),p&4&&s!==(s=`#${a[13].replyTo}`)&&f(t,"href",s)},d(b){b&&c(e)}}}function at(a){let e,l,t,n,i,s,r,u=a[13].name+"",b,p,k,T,E,B=a[13].timestamp.substring(0,10)+" "+new Date(a[13].timestamp).toLocaleTimeString(),w,g,D,m,y,C,S,M,U=a[13].content+"",J,X,W;t=new Nt({props:{icon:"user"}});let O=a[13].type==="webmention"&&tt(),F=a[13].type==="comment"&<(a),q=a[13].replyTo&&nt(a);return{c(){e=h("li"),l=h("span"),ze(t.$$.fragment),n=I(),i=h("div"),s=h("div"),r=h("h3"),b=P(u),k=I(),T=h("span"),E=h("a"),w=P(B),D=I(),m=h("span"),O&&O.c(),y=I(),F&&F.c(),C=I(),q&&q.c(),S=I(),M=h("p"),J=P(U),X=I(),this.h()},l(N){e=_(N,"LI",{class:!0});var G=v(e);l=_(G,"SPAN",{class:!0});var ve=v(l);Ve(t.$$.fragment,ve),ve.forEach(c),n=A(G),i=_(G,"DIV",{class:!0});var le=v(i);s=_(le,"DIV",{class:!0});var ne=v(s);r=_(ne,"H3",{id:!0,class:!0});var be=v(r);b=L(be,u),be.forEach(c),k=A(ne),T=_(ne,"SPAN",{class:!0});var Ee=v(T);E=_(Ee,"A",{href:!0});var K=v(E);w=L(K,B),K.forEach(c),Ee.forEach(c),D=A(ne),m=_(ne,"SPAN",{class:!0});var j=v(m);O&&O.l(j),j.forEach(c),y=A(ne),F&&F.l(ne),ne.forEach(c),C=A(le),q&&q.l(le),S=A(le),M=_(le,"P",{class:!0});var R=v(M);J=L(R,U),R.forEach(c),le.forEach(c),X=A(G),G.forEach(c),this.h()},h(){f(l,"class","avatar svelte-z0478z"),f(r,"id",p=a[13].id),f(r,"class","svelte-z0478z"),f(E,"href",g=a[13].url),f(T,"class","time svelte-z0478z"),f(m,"class","reply-notice svelte-z0478z"),f(s,"class","comment-header svelte-z0478z"),f(M,"class","comment-content svelte-z0478z"),f(i,"class","comment-card svelte-z0478z"),f(e,"class","comment svelte-z0478z")},m(N,G){V(N,e,G),o(e,l),Be(t,l,null),o(e,n),o(e,i),o(i,s),o(s,r),o(r,b),o(s,k),o(s,T),o(T,E),o(E,w),o(s,D),o(s,m),O&&O.m(m,null),o(s,y),F&&F.m(s,null),o(i,C),q&&q.m(i,null),o(i,S),o(i,M),o(M,J),o(e,X),W=!0},p(N,G){(!W||G&4)&&u!==(u=N[13].name+"")&&me(b,u),(!W||G&4&&p!==(p=N[13].id))&&f(r,"id",p),(!W||G&4)&&B!==(B=N[13].timestamp.substring(0,10)+" "+new Date(N[13].timestamp).toLocaleTimeString())&&me(w,B),(!W||G&4&&g!==(g=N[13].url))&&f(E,"href",g),N[13].type==="webmention"?O||(O=tt(),O.c(),O.m(m,null)):O&&(O.d(1),O=null),N[13].type==="comment"?F?F.p(N,G):(F=lt(N),F.c(),F.m(s,null)):F&&(F.d(1),F=null),N[13].replyTo?q?q.p(N,G):(q=nt(N),q.c(),q.m(i,S)):q&&(q.d(1),q=null),(!W||G&4)&&U!==(U=N[13].content+"")&&me(J,U)},i(N){W||(de(t.$$.fragment,N),W=!0)},o(N){ge(t.$$.fragment,N),W=!1},d(N){N&&c(e),Oe(t),O&&O.d(),F&&F.d(),q&&q.d()}}}function Gt(a){let e,l=a[2].length+"",t,n,i,s,r,u,b,p,k,T,E,B;k=new Ft({props:{reply:a[1],page:a[0].slug,url:a[3]}}),k.$on("newcomment",a[4]),k.$on("unselect-reply",a[8]);let w=a[2],g=[];for(let m=0;mge(g[m],1,1,()=>{g[m]=null});return{c(){e=h("h2"),t=P(l),n=P(" Comments and Interactions"),i=I(),s=h("span"),r=P(`Leave a comment or interact with this page via + `),u=h("a"),b=P("WebMention"),p=I(),ze(k.$$.fragment),T=I(),E=h("ul");for(let m=0;mD.json());l(7,r=g)}catch(g){console.warn("Could not fetch comments: ",g.message)}}function k(g){l(7,r=[g.detail,...r]),l(1,s=null)}function T(g){l(1,s=g),window.location.href="#comment-section"}const E=()=>l(1,s=null),B=g=>T(g),w=(g,D)=>D.id===g.reply_to;return a.$$set=g=>{"page"in g&&l(0,n=g.page),"comments"in g&&l(6,i=g.comments)},a.$$.update=()=>{a.$$.dirty&1&&(u=n.latestComment),a.$$.dirty&192&&l(2,b=[...r,...i.filter(g=>!r.find(D=>D.id===g.id))]),a.$$.dirty&1&&n&&p()},[n,s,b,t,k,T,i,r,E,B,w]}class Xt extends Re{constructor(e){super(),He(this,e,Qt,Gt,je,{page:0,comments:6})}}function st(a){let e,l,t,n=a[1].toLocaleDateString(void 0,{weekday:"long",year:"numeric",month:"long",day:"numeric"})+"",i,s;return{c(){e=h("span"),l=P("published on "),t=h("time"),i=P(n),this.h()},l(r){e=_(r,"SPAN",{});var u=v(e);l=L(u,"published on "),t=_(u,"TIME",{pubdate:!0,datetime:!0,class:!0});var b=v(t);i=L(b,n),b.forEach(c),u.forEach(c),this.h()},h(){f(t,"pubdate",""),f(t,"datetime",s=a[1].toISOString()),f(t,"class","date dt-published")},m(r,u){V(r,e,u),o(e,l),o(e,t),o(t,i)},p(r,u){u&2&&n!==(n=r[1].toLocaleDateString(void 0,{weekday:"long",year:"numeric",month:"long",day:"numeric"})+"")&&me(i,n),u&2&&s!==(s=r[1].toISOString())&&f(t,"datetime",s)},d(r){r&&c(e)}}}function it(a){let e,l,t=a[0].toLocaleDateString(void 0,{weekday:"long",year:"numeric",month:"long",day:"numeric"})+"",n,i;return{c(){e=P("last updated on "),l=h("time"),n=P(t),this.h()},l(s){e=L(s,"last updated on "),l=_(s,"TIME",{pubdate:!0,datetime:!0,class:!0});var r=v(l);n=L(r,t),r.forEach(c),this.h()},h(){f(l,"pubdate",""),f(l,"datetime",i=a[0].toISOString()),f(l,"class","date dt-updated")},m(s,r){V(s,e,r),V(s,l,r),o(l,n)},p(s,r){r&1&&t!==(t=s[0].toLocaleDateString(void 0,{weekday:"long",year:"numeric",month:"long",day:"numeric"})+"")&&me(n,t),r&1&&i!==(i=s[0].toISOString())&&f(l,"datetime",i)},d(s){s&&c(e),s&&c(l)}}}function Jt(a){let e,l,t=a[1]&&st(a),n=a[0]&&it(a);return{c(){t&&t.c(),e=I(),n&&n.c(),l=Se()},l(i){t&&t.l(i),e=A(i),n&&n.l(i),l=Se()},m(i,s){t&&t.m(i,s),V(i,e,s),n&&n.m(i,s),V(i,l,s)},p(i,[s]){i[1]?t?t.p(i,s):(t=st(i),t.c(),t.m(e.parentNode,e)):t&&(t.d(1),t=null),i[0]?n?n.p(i,s):(n=it(i),n.c(),n.m(l.parentNode,l)):n&&(n.d(1),n=null)},i:Qe,o:Qe,d(i){t&&t.d(i),i&&c(e),n&&n.d(i),i&&c(l)}}}function Kt(a,e,l){let t,n,{site:i}=e;return a.$$set=s=>{"site"in s&&l(2,i=s.site)},a.$$.update=()=>{a.$$.dirty&4&&l(1,t=i.date?new Date(i.date):null),a.$$.dirty&4&&l(0,n=i.modified&&i.modified!==i.date?new Date(i.modified):null)},[n,t,i]}class Wt extends Re{constructor(e){super(),He(this,e,Kt,Jt,je,{site:2})}}const $t=a=>({}),ot=a=>({});function rt(a,e,l){const t=a.slice();return t[5]=e[l],t}function ft(a,e,l){const t=a.slice();return t[8]=e[l],t}function ct(a,e,l){const t=a.slice();return t[11]=e[l],t}function ut(a,e,l){const t=a.slice();return t[11]=e[l],t}function mt(a){let e,l;return{c(){e=h("meta"),this.h()},l(t){e=_(t,"META",{property:!0,content:!0}),this.h()},h(){f(e,"property","article:published_time"),f(e,"content",l=a[0].date)},m(t,n){V(t,e,n)},p(t,n){n&1&&l!==(l=t[0].date)&&f(e,"content",l)},d(t){t&&c(e)}}}function ht(a){let e,l;return{c(){e=h("meta"),this.h()},l(t){e=_(t,"META",{property:!0,content:!0}),this.h()},h(){f(e,"property","article:modified_time"),f(e,"content",l=a[0].modified||a[0].date)},m(t,n){V(t,e,n)},p(t,n){n&1&&l!==(l=t[0].modified||t[0].date)&&f(e,"content",l)},d(t){t&&c(e)}}}function _t(a){let e,l;return{c(){e=h("meta"),this.h()},l(t){e=_(t,"META",{property:!0,content:!0}),this.h()},h(){f(e,"property","article:tag"),f(e,"content",l=a[11])},m(t,n){V(t,e,n)},p(t,n){n&1&&l!==(l=t[11])&&f(e,"content",l)},d(t){t&&c(e)}}}function dt(a){let e,l;return{c(){e=h("meta"),this.h()},l(t){e=_(t,"META",{property:!0,content:!0}),this.h()},h(){f(e,"property","og:image"),f(e,"content",l=Xe+a[0].cover_image)},m(t,n){V(t,e,n)},p(t,n){n&1&&l!==(l=Xe+t[0].cover_image)&&f(e,"content",l)},d(t){t&&c(e)}}}function pt(a){let e,l,t,n,i,s=a[0].cover_image_txt&&vt(a);return{c(){e=h("figure"),l=h("img"),i=I(),s&&s.c(),this.h()},l(r){e=_(r,"FIGURE",{class:!0});var u=v(e);l=_(u,"IMG",{alt:!0,src:!0,class:!0}),i=A(u),s&&s.l(u),u.forEach(c),this.h()},h(){f(l,"alt",t=a[0].title),qe(l.src,n=Xe+a[0].cover_image)||f(l,"src",n),f(l,"class","svelte-1vgyhou"),f(e,"class","svelte-1vgyhou")},m(r,u){V(r,e,u),o(e,l),o(e,i),s&&s.m(e,null)},p(r,u){u&1&&t!==(t=r[0].title)&&f(l,"alt",t),u&1&&!qe(l.src,n=Xe+r[0].cover_image)&&f(l,"src",n),r[0].cover_image_txt?s?s.p(r,u):(s=vt(r),s.c(),s.m(e,null)):s&&(s.d(1),s=null)},d(r){r&&c(e),s&&s.d()}}}function vt(a){let e,l=a[0].cover_image_txt+"",t;return{c(){e=h("figcaption"),t=P(l),this.h()},l(n){e=_(n,"FIGCAPTION",{class:!0});var i=v(e);t=L(i,l),i.forEach(c),this.h()},h(){f(e,"class","svelte-1vgyhou")},m(n,i){V(n,e,i),o(e,t)},p(n,i){i&1&&l!==(l=n[0].cover_image_txt+"")&&me(t,l)},d(n){n&&c(e)}}}function gt(a){let e,l;return{c(){e=h("p"),l=P("This site is not published!"),this.h()},l(t){e=_(t,"P",{class:!0});var n=v(e);l=L(n,"This site is not published!"),n.forEach(c),this.h()},h(){f(e,"class","notification svelte-1vgyhou")},m(t,n){V(t,e,n),o(e,l)},d(t){t&&c(e)}}}function bt(a){let e,l=a[11]+"",t,n,i;return{c(){e=h("a"),t=P(l),n=I(),this.h()},l(s){e=_(s,"A",{class:!0,href:!0});var r=v(e);t=L(r,l),n=A(r),r.forEach(c),this.h()},h(){f(e,"class","tag p-category"),f(e,"href",i=`/tags/${a[11]}`)},m(s,r){V(s,e,r),o(e,t),o(e,n)},p(s,r){r&1&&l!==(l=s[11]+"")&&me(t,l),r&1&&i!==(i=`/tags/${s[11]}`)&&f(e,"href",i)},d(s){s&&c(e)}}}function Et(a){let e,l,t,n,i,s=a[0].links,r=[];for(let u=0;u{Z=null}),St())},i(d){x||(de(N.$$.fragment,d),de(_e,d),de(j.$$.fragment,d),de(Z),x=!0)},o(d){ge(N.$$.fragment,d),ge(_e,d),ge(j.$$.fragment,d),ge(Z),x=!1},d(d){c(l),c(n),c(i),ee&&ee.d(d),c(r),te&&te.d(d),c(u),Ue(ae,d),c(b),oe&&oe.d(d),c(p),d&&c(k),d&&c(T),re&&re.d(),he&&he.d(),Ue(se,d),Oe(N),fe&&fe.d(),ce&&ce.d(),_e&&_e.d(d),Oe(j),ue&&ue.d(),Z&&Z.d()}}}function Zt(a,e,l){let{$$slots:t={},$$scope:n}=e,{site:i}=e,{about:s=null}=e,r;return a.$$set=u=>{"site"in u&&l(0,i=u.site),"about"in u&&l(1,s=u.about),"$$scope"in u&&l(3,n=u.$$scope)},a.$$.update=()=>{a.$$.dirty&1&&l(2,r={rsvp:"💌RSVP: ",reply:"🗨️Reply: ",repost:"🔁Share: ",like:"👍Like: ",projects:"⚙️Project: ",note:"📔 "}[i==null?void 0:i.type]||"")},[i,s,r,n,t]}class nl extends Re{constructor(e){super(),He(this,e,Zt,Yt,je,{site:0,about:1})}}export{nl as M}; diff --git a/_app/immutable/chunks/PostCardList.ba348fd5.js b/_app/immutable/chunks/PostCardList.ba348fd5.js new file mode 100644 index 00000000..b564ccd2 --- /dev/null +++ b/_app/immutable/chunks/PostCardList.ba348fd5.js @@ -0,0 +1 @@ +import{S as F,i as K,s as O,G as he,k as g,q as B,a as w,l as m,m as H,r as G,h as o,c as C,n as h,b as I,C as d,u as R,H as ue,I as _e,J as de,g as M,d as T,N as W,L as j,y as re,z as fe,A as ce,B as oe,v as ge,f as me}from"./index.8c1dbec0.js";function X(i,t,n){const e=i.slice();return e[8]=t[n],e}function Y(i,t,n){const e=i.slice();return e[8]=t[n],e}function Z(i,t,n){const e=i.slice();return e[13]=t[n],e}function y(i){let t,n=new Date(i[5]).toLocaleDateString()+"",e;return{c(){t=g("span"),e=B(n),this.h()},l(l){t=m(l,"SPAN",{class:!0});var a=H(t);e=G(a,n),a.forEach(o),this.h()},h(){h(t,"class","date svelte-sf0lxt")},m(l,a){I(l,t,a),d(t,e)},p(l,a){a&32&&n!==(n=new Date(l[5]).toLocaleDateString()+"")&&R(e,n)},d(l){l&&o(t)}}}function $(i){let t,n,e;return{c(){t=g("img"),this.h()},l(l){t=m(l,"IMG",{src:!0,alt:!0,class:!0}),this.h()},h(){var l,a;W(t.src,n=(l=i[1])==null?void 0:l.src)||h(t,"src",n),h(t,"alt",e=((a=i[1])==null?void 0:a.alt)||i[0]+" title image"),h(t,"class","svelte-sf0lxt")},m(l,a){I(l,t,a)},p(l,a){var s,r;a&2&&!W(t.src,n=(s=l[1])==null?void 0:s.src)&&h(t,"src",n),a&3&&e!==(e=((r=l[1])==null?void 0:r.alt)||l[0]+" title image")&&h(t,"alt",e)},d(l){l&&o(t)}}}function x(i){let t,n=i[4],e=[];for(let l=0;l0&&x(i),b=i[2]&&i[2].length>0&&te(i),E=i[3]&&i[3].length>0&&se(i);return{c(){t=g("article"),n=g("h3"),e=B(i[0]),l=w(),v&&v.c(),a=w(),s=g("div"),p&&p.c(),r=w(),f=g("p"),D&&D.c(),A=w(),k&&k.c(),V=w(),_=g("div"),z=w(),L=g("div"),b&&b.c(),N=w(),E&&E.c(),this.h()},l(c){t=m(c,"ARTICLE",{class:!0});var u=H(t);n=m(u,"H3",{class:!0});var Q=H(n);e=G(Q,i[0]),Q.forEach(o),l=C(u),v&&v.l(u),a=C(u),s=m(u,"DIV",{class:!0});var S=H(s);p&&p.l(S),r=C(S),f=m(S,"P",{});var U=H(f);D&&D.l(U),U.forEach(o),A=C(S),k&&k.l(S),S.forEach(o),V=C(u),_=m(u,"DIV",{class:!0}),H(_).forEach(o),z=C(u),L=m(u,"DIV",{class:!0});var q=H(L);b&&b.l(q),N=C(q),E&&E.l(q),q.forEach(o),u.forEach(o),this.h()},h(){h(n,"class","card-header svelte-sf0lxt"),h(s,"class","card-content svelte-sf0lxt"),h(_,"class","spacer svelte-sf0lxt"),h(L,"class","links-wrap svelte-sf0lxt"),h(t,"class","card svelte-sf0lxt")},m(c,u){I(c,t,u),d(t,n),d(n,e),d(t,l),v&&v.m(t,null),d(t,a),d(t,s),p&&p.m(s,null),d(s,r),d(s,f),D&&D.m(f,null),d(s,A),k&&k.m(s,null),d(t,V),d(t,_),d(t,z),d(t,L),b&&b.m(L,null),d(L,N),E&&E.m(L,null),P=!0},p(c,[u]){(!P||u&1)&&R(e,c[0]),c[5]?v?v.p(c,u):(v=y(c),v.c(),v.m(t,a)):v&&(v.d(1),v=null),c[1]?p?p.p(c,u):(p=$(c),p.c(),p.m(s,r)):p&&(p.d(1),p=null),D&&D.p&&(!P||u&64)&&ue(D,J,c,c[6],P?de(J,c[6],u,null):_e(c[6]),null),c[4]&&c[4].length>0?k?k.p(c,u):(k=x(c),k.c(),k.m(s,null)):k&&(k.d(1),k=null),c[2]&&c[2].length>0?b?b.p(c,u):(b=te(c),b.c(),b.m(L,N)):b&&(b.d(1),b=null),c[3]&&c[3].length>0?E?E.p(c,u):(E=se(c),E.c(),E.m(L,null)):E&&(E.d(1),E=null)},i(c){P||(M(D,c),P=!0)},o(c){T(D,c),P=!1},d(c){c&&o(t),v&&v.d(),p&&p.d(),D&&D.d(c),k&&k.d(),b&&b.d(),E&&E.d()}}}function pe(i,t,n){let{$$slots:e={},$$scope:l}=t,{title:a=""}=t,{image:s=null}=t,{linksHtml:r=[]}=t,{links:f=[]}=t,{tags:A=[]}=t,{date:V=null}=t;return i.$$set=_=>{"title"in _&&n(0,a=_.title),"image"in _&&n(1,s=_.image),"linksHtml"in _&&n(2,r=_.linksHtml),"links"in _&&n(3,f=_.links),"tags"in _&&n(4,A=_.tags),"date"in _&&n(5,V=_.date),"$$scope"in _&&n(6,l=_.$$scope)},[a,s,r,f,A,V,l,e]}class ke extends F{constructor(t){super(),K(this,t,pe,ve,O,{title:0,image:1,linksHtml:2,links:3,tags:4,date:5})}}function be(i){let t,n=(i[0].description||i[0].abstract)+"";return{c(){t=g("div")},l(e){t=m(e,"DIV",{});var l=H(t);l.forEach(o)},m(e,l){I(e,t,l),t.innerHTML=n},p(e,l){l&1&&n!==(n=(e[0].description||e[0].abstract)+"")&&(t.innerHTML=n)},d(e){e&&o(t)}}}function Ee(i){let t,n;return t=new ke({props:{title:i[0].title||"",image:i[2],tags:i[0].tags,links:i[3],linksHtml:i[1],date:i[0].date,$$slots:{default:[be]},$$scope:{ctx:i}}}),{c(){re(t.$$.fragment)},l(e){fe(t.$$.fragment,e)},m(e,l){ce(t,e,l),n=!0},p(e,[l]){const a={};l&1&&(a.title=e[0].title||""),l&4&&(a.image=e[2]),l&1&&(a.tags=e[0].tags),l&2&&(a.linksHtml=e[1]),l&1&&(a.date=e[0].date),l&17&&(a.$$scope={dirty:l,ctx:e}),t.$set(a)},i(e){n||(M(t.$$.fragment,e),n=!0)},o(e){T(t.$$.fragment,e),n=!1},d(e){oe(t,e)}}}function He(i,t,n){let{post:e}=t,l=[],a=[{href:`/${e.slug}`,text:"Read More..."}],s;return i.$$set=r=>{"post"in r&&n(0,e=r.post)},i.$$.update=()=>{i.$$.dirty&1&&n(1,l=Array.isArray(e.links)?e.links:void 0),i.$$.dirty&1&&n(2,s=e.cover_image?{src:e.cover_image,alt:e.title}:void 0)},[e,l,s,a]}class De extends F{constructor(t){super(),K(this,t,He,Ee,O,{post:0})}}function ie(i,t,n){const e=i.slice();return e[1]=t[n],e}function ae(i){let t,n;return t=new De({props:{post:i[1]}}),{c(){re(t.$$.fragment)},l(e){fe(t.$$.fragment,e)},m(e,l){ce(t,e,l),n=!0},p(e,l){const a={};l&1&&(a.post=e[1]),t.$set(a)},i(e){n||(M(t.$$.fragment,e),n=!0)},o(e){T(t.$$.fragment,e),n=!1},d(e){oe(t,e)}}}function Ie(i){let t,n,e=i[0],l=[];for(let s=0;sT(l[s],1,1,()=>{l[s]=null});return{c(){t=g("div");for(let s=0;s{"posts"in l&&n(0,e=l.posts)},[e]}class we extends F{constructor(t){super(),K(this,t,Le,Ie,O,{posts:0})}}export{we as P}; diff --git a/_app/immutable/chunks/fa.9f83fec8.js b/_app/immutable/chunks/fa.9f83fec8.js new file mode 100644 index 00000000..d041a4ea --- /dev/null +++ b/_app/immutable/chunks/fa.9f83fec8.js @@ -0,0 +1,461 @@ +import{S as Vn,i as Wn,s as Un,R as Yn,e as rn,T as Xn,b as Bn,E as on,h as Gn,U as qn,V as fn,W as sn}from"./index.8c1dbec0.js";/*! + * Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */function _(n){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(t){return typeof t}:_=function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_(n)}function Zn(n,t){if(!(n instanceof t))throw new TypeError("Cannot call a class as a function")}function ln(n,t){for(var a=0;a"u"?setTimeout:setImmediate,T=[],J;function mt(){for(var n=0;n-1;r--){var i=a[r],o=(i.tagName||"").toUpperCase();["STYLE","LINK"].indexOf(o)>-1&&(e=i)}return g.head.insertBefore(t,e),n}}var bt="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";function W(){for(var n=12,t="";n-- >0;)t+=bt[Math.random()*62|0];return t}function Ln(n){return"".concat(n).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function pt(n){return Object.keys(n||{}).reduce(function(t,a){return t+"".concat(a,'="').concat(Ln(n[a]),'" ')},"").trim()}function Pn(n){return Object.keys(n||{}).reduce(function(t,a){return t+"".concat(a,": ").concat(n[a],";")},"")}function Tn(n){return n.size!==P.size||n.x!==P.x||n.y!==P.y||n.rotate!==P.rotate||n.flipX||n.flipY}function _n(n){var t=n.transform,a=n.containerWidth,e=n.iconWidth,r={transform:"translate(".concat(a/2," 256)")},i="translate(".concat(t.x*32,", ").concat(t.y*32,") "),o="scale(".concat(t.size/16*(t.flipX?-1:1),", ").concat(t.size/16*(t.flipY?-1:1),") "),f="rotate(".concat(t.rotate," 0 0)"),l={transform:"".concat(i," ").concat(o," ").concat(f)},u={transform:"translate(".concat(e/2*-1," -256)")};return{outer:r,inner:l,path:u}}var B={x:0,y:0,width:"100%",height:"100%"};function dn(n){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0;return n.attributes&&(n.attributes.fill||t)&&(n.attributes.fill="black"),n}function wt(n){return n.tag==="g"?n.children:[n]}function kt(n){var t=n.children,a=n.attributes,e=n.main,r=n.mask,i=n.maskId,o=n.transform,f=e.width,l=e.icon,u=r.width,d=r.icon,m=_n({transform:o,containerWidth:u,iconWidth:f}),w={tag:"rect",attributes:c({},B,{fill:"white"})},k=l.children?{children:l.children.map(dn)}:{},x={tag:"g",attributes:c({},m.inner),children:[dn(c({tag:l.tag,attributes:c({},l.attributes,m.path)},k))]},b={tag:"g",attributes:c({},m.outer),children:[x]},v="mask-".concat(i||W()),h="clip-".concat(i||W()),z={tag:"mask",attributes:c({},B,{id:v,maskUnits:"userSpaceOnUse",maskContentUnits:"userSpaceOnUse"}),children:[w,b]},C={tag:"defs",children:[{tag:"clipPath",attributes:{id:h},children:wt(d)},z]};return t.push(C,{tag:"rect",attributes:c({fill:"currentColor","clip-path":"url(#".concat(h,")"),mask:"url(#".concat(v,")")},B)}),{children:t,attributes:a}}function xt(n){var t=n.children,a=n.attributes,e=n.main,r=n.transform,i=n.styles,o=Pn(i);if(o.length>0&&(a.style=o),Tn(r)){var f=_n({transform:r,containerWidth:e.width,iconWidth:e.width});t.push({tag:"g",attributes:c({},f.outer),children:[{tag:"g",attributes:c({},f.inner),children:[{tag:e.icon.tag,children:e.icon.children,attributes:c({},e.icon.attributes,f.path)}]}]})}else t.push(e.icon);return{children:t,attributes:a}}function zt(n){var t=n.children,a=n.main,e=n.mask,r=n.attributes,i=n.styles,o=n.transform;if(Tn(o)&&a.found&&!e.found){var f=a.width,l=a.height,u={x:f/l/2,y:.5};r.style=Pn(c({},i,{"transform-origin":"".concat(u.x+o.x/16,"em ").concat(u.y+o.y/16,"em")}))}return[{tag:"svg",attributes:r,children:t}]}function Mt(n){var t=n.prefix,a=n.iconName,e=n.children,r=n.attributes,i=n.symbol,o=i===!0?"".concat(t,"-").concat(y.familyPrefix,"-").concat(a):i;return[{tag:"svg",attributes:{style:"display: none;"},children:[{tag:"symbol",attributes:c({},r,{id:o}),children:e}]}]}function It(n){var t=n.icons,a=t.main,e=t.mask,r=n.prefix,i=n.iconName,o=n.transform,f=n.symbol,l=n.title,u=n.maskId,d=n.titleId,m=n.extra,w=n.watchable,k=w===void 0?!1:w,x=e.found?e:a,b=x.width,v=x.height,h=r==="fak",z=h?"":"fa-w-".concat(Math.ceil(b/v*16)),C=[y.replacementClass,i?"".concat(y.familyPrefix,"-").concat(i):"",z].filter(function(H){return m.classes.indexOf(H)===-1}).filter(function(H){return H!==""||!!H}).concat(m.classes).join(" "),M={children:[],attributes:c({},m.attributes,{"data-prefix":r,"data-icon":i,class:C,role:m.attributes.role||"img",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 ".concat(b," ").concat(v)})},N=h&&!~m.classes.indexOf("fa-fw")?{width:"".concat(b/v*16*.0625,"em")}:{};k&&(M.attributes[at]=""),l&&M.children.push({tag:"title",attributes:{id:M.attributes["aria-labelledby"]||"title-".concat(d||W())},children:[l]});var s=c({},M,{prefix:r,iconName:i,main:a,mask:e,maskId:u,transform:o,symbol:f,styles:c({},N,m.styles)}),A=e.found&&a.found?kt(s):xt(s),L=A.children,S=A.attributes;return s.children=L,s.attributes=S,f?Mt(s):zt(s)}var hn=function(){};y.measurePerformance&&j&&j.mark&&j.measure;var Ct=function(t,a){return function(e,r,i,o){return t.call(a,e,r,i,o)}},G=function(t,a,e,r){var i=Object.keys(t),o=i.length,f=r!==void 0?Ct(a,r):a,l,u,d;for(e===void 0?(l=1,d=t[i[0]]):(l=0,d=e);l2&&arguments[2]!==void 0?arguments[2]:{},e=a.skipHooks,r=e===void 0?!1:e,i=Object.keys(t).reduce(function(o,f){var l=t[f],u=!!l.icon;return u?o[l.iconName]=l.icon:o[f]=l,o},{});typeof I.hooks.addPack=="function"&&!r?I.hooks.addPack(n,i):I.styles[n]=c({},I.styles[n]||{},i),n==="fas"&&Rn("fa",t)}var gn=I.styles,At=I.shims,Hn=function(){var t=function(r){return G(gn,function(i,o,f){return i[f]=G(o,r,{}),i},{})};t(function(e,r,i){return r[3]&&(e[r[3]]=i),e}),t(function(e,r,i){var o=r[2];return e[i]=i,o.forEach(function(f){e[f]=i}),e});var a="far"in gn;G(At,function(e,r){var i=r[0],o=r[1],f=r[2];return o==="far"&&!a&&(o="fas"),e[i]={prefix:o,iconName:f},e},{})};Hn();I.styles;function vn(n,t,a){if(n&&n[t]&&n[t][a])return{prefix:t,iconName:a,icon:n[t][a]}}function jn(n){var t=n.tag,a=n.attributes,e=a===void 0?{}:a,r=n.children,i=r===void 0?[]:r;return typeof n=="string"?Ln(n):"<".concat(t," ").concat(pt(e),">").concat(i.map(jn).join(""),"")}var Et=function(t){var a={size:16,x:0,y:0,flipX:!1,flipY:!1,rotate:0};return t?t.toLowerCase().split(" ").reduce(function(e,r){var i=r.toLowerCase().split("-"),o=i[0],f=i.slice(1).join("-");if(o&&f==="h")return e.flipX=!0,e;if(o&&f==="v")return e.flipY=!0,e;if(f=parseFloat(f),isNaN(f))return e;switch(o){case"grow":e.size=e.size+f;break;case"shrink":e.size=e.size-f;break;case"left":e.x=e.x-f;break;case"right":e.x=e.x+f;break;case"up":e.y=e.y-f;break;case"down":e.y=e.y+f;break;case"rotate":e.rotate=e.rotate+f;break}return e},a):a};function Q(n){this.name="MissingIcon",this.message=n||"Icon unavailable",this.stack=new Error().stack}Q.prototype=Object.create(Error.prototype);Q.prototype.constructor=Q;var Y={fill:"currentColor"},Dn={attributeType:"XML",repeatCount:"indefinite",dur:"2s"};c({},Y,{d:"M156.5,447.7l-12.6,29.5c-18.7-9.5-35.9-21.2-51.5-34.9l22.7-22.7C127.6,430.5,141.5,440,156.5,447.7z M40.6,272H8.5 c1.4,21.2,5.4,41.7,11.7,61.1L50,321.2C45.1,305.5,41.8,289,40.6,272z M40.6,240c1.4-18.8,5.2-37,11.1-54.1l-29.5-12.6 C14.7,194.3,10,216.7,8.5,240H40.6z M64.3,156.5c7.8-14.9,17.2-28.8,28.1-41.5L69.7,92.3c-13.7,15.6-25.5,32.8-34.9,51.5 L64.3,156.5z M397,419.6c-13.9,12-29.4,22.3-46.1,30.4l11.9,29.8c20.7-9.9,39.8-22.6,56.9-37.6L397,419.6z M115,92.4 c13.9-12,29.4-22.3,46.1-30.4l-11.9-29.8c-20.7,9.9-39.8,22.6-56.8,37.6L115,92.4z M447.7,355.5c-7.8,14.9-17.2,28.8-28.1,41.5 l22.7,22.7c13.7-15.6,25.5-32.9,34.9-51.5L447.7,355.5z M471.4,272c-1.4,18.8-5.2,37-11.1,54.1l29.5,12.6 c7.5-21.1,12.2-43.5,13.6-66.8H471.4z M321.2,462c-15.7,5-32.2,8.2-49.2,9.4v32.1c21.2-1.4,41.7-5.4,61.1-11.7L321.2,462z M240,471.4c-18.8-1.4-37-5.2-54.1-11.1l-12.6,29.5c21.1,7.5,43.5,12.2,66.8,13.6V471.4z M462,190.8c5,15.7,8.2,32.2,9.4,49.2h32.1 c-1.4-21.2-5.4-41.7-11.7-61.1L462,190.8z M92.4,397c-12-13.9-22.3-29.4-30.4-46.1l-29.8,11.9c9.9,20.7,22.6,39.8,37.6,56.9 L92.4,397z M272,40.6c18.8,1.4,36.9,5.2,54.1,11.1l12.6-29.5C317.7,14.7,295.3,10,272,8.5V40.6z M190.8,50 c15.7-5,32.2-8.2,49.2-9.4V8.5c-21.2,1.4-41.7,5.4-61.1,11.7L190.8,50z M442.3,92.3L419.6,115c12,13.9,22.3,29.4,30.5,46.1 l29.8-11.9C470,128.5,457.3,109.4,442.3,92.3z M397,92.4l22.7-22.7c-15.6-13.7-32.8-25.5-51.5-34.9l-12.6,29.5 C370.4,72.1,384.4,81.5,397,92.4z"});var an=c({},Dn,{attributeName:"opacity"});c({},Y,{cx:"256",cy:"364",r:"28"}),c({},Dn,{attributeName:"r",values:"28;14;28;28;14;28;"}),c({},an,{values:"1;0;1;1;0;1;"});c({},Y,{opacity:"1",d:"M263.7,312h-16c-6.6,0-12-5.4-12-12c0-71,77.4-63.9,77.4-107.8c0-20-17.8-40.2-57.4-40.2c-29.1,0-44.3,9.6-59.2,28.7 c-3.9,5-11.1,6-16.2,2.4l-13.1-9.2c-5.6-3.9-6.9-11.8-2.6-17.2c21.2-27.2,46.4-44.7,91.2-44.7c52.3,0,97.4,29.8,97.4,80.2 c0,67.6-77.4,63.5-77.4,107.8C275.7,306.6,270.3,312,263.7,312z"}),c({},an,{values:"1;0;0;0;0;1;"});c({},Y,{opacity:"0",d:"M232.5,134.5l7,168c0.3,6.4,5.6,11.5,12,11.5h9c6.4,0,11.7-5.1,12-11.5l7-168c0.3-6.8-5.2-12.5-12-12.5h-23 C237.7,122,232.2,127.7,232.5,134.5z"}),c({},an,{values:"0;0;1;1;0;0;"});I.styles;function yn(n){var t=n[0],a=n[1],e=n.slice(4),r=wn(e,1),i=r[0],o=null;return Array.isArray(i)?o={tag:"g",attributes:{class:"".concat(y.familyPrefix,"-").concat(X.GROUP)},children:[{tag:"path",attributes:{class:"".concat(y.familyPrefix,"-").concat(X.SECONDARY),fill:"currentColor",d:i[0]}},{tag:"path",attributes:{class:"".concat(y.familyPrefix,"-").concat(X.PRIMARY),fill:"currentColor",d:i[1]}}]}:o={tag:"path",attributes:{fill:"currentColor",d:i}},{found:!0,width:t,height:a,icon:o}}I.styles;var Ot=`svg:not(:root).svg-inline--fa { + overflow: visible; +} + +.svg-inline--fa { + display: inline-block; + font-size: inherit; + height: 1em; + overflow: visible; + vertical-align: -0.125em; +} +.svg-inline--fa.fa-lg { + vertical-align: -0.225em; +} +.svg-inline--fa.fa-w-1 { + width: 0.0625em; +} +.svg-inline--fa.fa-w-2 { + width: 0.125em; +} +.svg-inline--fa.fa-w-3 { + width: 0.1875em; +} +.svg-inline--fa.fa-w-4 { + width: 0.25em; +} +.svg-inline--fa.fa-w-5 { + width: 0.3125em; +} +.svg-inline--fa.fa-w-6 { + width: 0.375em; +} +.svg-inline--fa.fa-w-7 { + width: 0.4375em; +} +.svg-inline--fa.fa-w-8 { + width: 0.5em; +} +.svg-inline--fa.fa-w-9 { + width: 0.5625em; +} +.svg-inline--fa.fa-w-10 { + width: 0.625em; +} +.svg-inline--fa.fa-w-11 { + width: 0.6875em; +} +.svg-inline--fa.fa-w-12 { + width: 0.75em; +} +.svg-inline--fa.fa-w-13 { + width: 0.8125em; +} +.svg-inline--fa.fa-w-14 { + width: 0.875em; +} +.svg-inline--fa.fa-w-15 { + width: 0.9375em; +} +.svg-inline--fa.fa-w-16 { + width: 1em; +} +.svg-inline--fa.fa-w-17 { + width: 1.0625em; +} +.svg-inline--fa.fa-w-18 { + width: 1.125em; +} +.svg-inline--fa.fa-w-19 { + width: 1.1875em; +} +.svg-inline--fa.fa-w-20 { + width: 1.25em; +} +.svg-inline--fa.fa-pull-left { + margin-right: 0.3em; + width: auto; +} +.svg-inline--fa.fa-pull-right { + margin-left: 0.3em; + width: auto; +} +.svg-inline--fa.fa-border { + height: 1.5em; +} +.svg-inline--fa.fa-li { + width: 2em; +} +.svg-inline--fa.fa-fw { + width: 1.25em; +} + +.fa-layers svg.svg-inline--fa { + bottom: 0; + left: 0; + margin: auto; + position: absolute; + right: 0; + top: 0; +} + +.fa-layers { + display: inline-block; + height: 1em; + position: relative; + text-align: center; + vertical-align: -0.125em; + width: 1em; +} +.fa-layers svg.svg-inline--fa { + -webkit-transform-origin: center center; + transform-origin: center center; +} + +.fa-layers-counter, .fa-layers-text { + display: inline-block; + position: absolute; + text-align: center; +} + +.fa-layers-text { + left: 50%; + top: 50%; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + -webkit-transform-origin: center center; + transform-origin: center center; +} + +.fa-layers-counter { + background-color: #ff253a; + border-radius: 1em; + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: #fff; + height: 1.5em; + line-height: 1; + max-width: 5em; + min-width: 1.5em; + overflow: hidden; + padding: 0.25em; + right: 0; + text-overflow: ellipsis; + top: 0; + -webkit-transform: scale(0.25); + transform: scale(0.25); + -webkit-transform-origin: top right; + transform-origin: top right; +} + +.fa-layers-bottom-right { + bottom: 0; + right: 0; + top: auto; + -webkit-transform: scale(0.25); + transform: scale(0.25); + -webkit-transform-origin: bottom right; + transform-origin: bottom right; +} + +.fa-layers-bottom-left { + bottom: 0; + left: 0; + right: auto; + top: auto; + -webkit-transform: scale(0.25); + transform: scale(0.25); + -webkit-transform-origin: bottom left; + transform-origin: bottom left; +} + +.fa-layers-top-right { + right: 0; + top: 0; + -webkit-transform: scale(0.25); + transform: scale(0.25); + -webkit-transform-origin: top right; + transform-origin: top right; +} + +.fa-layers-top-left { + left: 0; + right: auto; + top: 0; + -webkit-transform: scale(0.25); + transform: scale(0.25); + -webkit-transform-origin: top left; + transform-origin: top left; +} + +.fa-lg { + font-size: 1.3333333333em; + line-height: 0.75em; + vertical-align: -0.0667em; +} + +.fa-xs { + font-size: 0.75em; +} + +.fa-sm { + font-size: 0.875em; +} + +.fa-1x { + font-size: 1em; +} + +.fa-2x { + font-size: 2em; +} + +.fa-3x { + font-size: 3em; +} + +.fa-4x { + font-size: 4em; +} + +.fa-5x { + font-size: 5em; +} + +.fa-6x { + font-size: 6em; +} + +.fa-7x { + font-size: 7em; +} + +.fa-8x { + font-size: 8em; +} + +.fa-9x { + font-size: 9em; +} + +.fa-10x { + font-size: 10em; +} + +.fa-fw { + text-align: center; + width: 1.25em; +} + +.fa-ul { + list-style-type: none; + margin-left: 2.5em; + padding-left: 0; +} +.fa-ul > li { + position: relative; +} + +.fa-li { + left: -2em; + position: absolute; + text-align: center; + width: 2em; + line-height: inherit; +} + +.fa-border { + border: solid 0.08em #eee; + border-radius: 0.1em; + padding: 0.2em 0.25em 0.15em; +} + +.fa-pull-left { + float: left; +} + +.fa-pull-right { + float: right; +} + +.fa.fa-pull-left, +.fas.fa-pull-left, +.far.fa-pull-left, +.fal.fa-pull-left, +.fab.fa-pull-left { + margin-right: 0.3em; +} +.fa.fa-pull-right, +.fas.fa-pull-right, +.far.fa-pull-right, +.fal.fa-pull-right, +.fab.fa-pull-right { + margin-left: 0.3em; +} + +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} + +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + transform: rotate(180deg); +} + +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + transform: rotate(270deg); +} + +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); +} + +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + transform: scale(1, -1); +} + +.fa-flip-both, .fa-flip-horizontal.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); +} + +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical, +:root .fa-flip-both { + -webkit-filter: none; + filter: none; +} + +.fa-stack { + display: inline-block; + height: 2em; + position: relative; + width: 2.5em; +} + +.fa-stack-1x, +.fa-stack-2x { + bottom: 0; + left: 0; + margin: auto; + position: absolute; + right: 0; + top: 0; +} + +.svg-inline--fa.fa-stack-1x { + height: 1em; + width: 1.25em; +} +.svg-inline--fa.fa-stack-2x { + height: 2em; + width: 2.5em; +} + +.fa-inverse { + color: #fff; +} + +.sr-only { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +.sr-only-focusable:active, .sr-only-focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; +} + +.svg-inline--fa .fa-primary { + fill: var(--fa-primary-color, currentColor); + opacity: 1; + opacity: var(--fa-primary-opacity, 1); +} + +.svg-inline--fa .fa-secondary { + fill: var(--fa-secondary-color, currentColor); + opacity: 0.4; + opacity: var(--fa-secondary-opacity, 0.4); +} + +.svg-inline--fa.fa-swap-opacity .fa-primary { + opacity: 0.4; + opacity: var(--fa-secondary-opacity, 0.4); +} + +.svg-inline--fa.fa-swap-opacity .fa-secondary { + opacity: 1; + opacity: var(--fa-primary-opacity, 1); +} + +.svg-inline--fa mask .fa-primary, +.svg-inline--fa mask .fa-secondary { + fill: black; +} + +.fad.fa-inverse { + color: #fff; +}`;function St(){var n=zn,t=Mn,a=y.familyPrefix,e=y.replacementClass,r=Ot;if(a!==n||e!==t){var i=new RegExp("\\.".concat(n,"\\-"),"g"),o=new RegExp("\\--".concat(n,"\\-"),"g"),f=new RegExp("\\.".concat(t),"g");r=r.replace(i,".".concat(a,"-")).replace(o,"--".concat(a,"-")).replace(f,".".concat(e))}return r}var Nt=function(){function n(){Zn(this,n),this.definitions={}}return Jn(n,[{key:"add",value:function(){for(var a=this,e=arguments.length,r=new Array(e),i=0;i1&&arguments[1]!==void 0?arguments[1]:{},e=(t||{}).icon?t:K(t||{}),r=a.mask;return r&&(r=(r||{}).icon?r:K(r||{})),n(e,c({},a,{mask:r}))}}var Fn=new Nt,bn=!1,_t={transform:function(t){return Et(t)}},Rt=Tt(function(n){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},a=t.transform,e=a===void 0?P:a,r=t.symbol,i=r===void 0?!1:r,o=t.mask,f=o===void 0?null:o,l=t.maskId,u=l===void 0?null:l,d=t.title,m=d===void 0?null:d,w=t.titleId,k=w===void 0?null:w,x=t.classes,b=x===void 0?[]:x,v=t.attributes,h=v===void 0?{}:v,z=t.styles,C=z===void 0?{}:z;if(n){var M=n.prefix,N=n.iconName,s=n.icon;return Pt(c({type:"icon"},n),function(){return Lt(),y.autoA11y&&(m?h["aria-labelledby"]="".concat(y.replacementClass,"-title-").concat(k||W()):(h["aria-hidden"]="true",h.focusable="false")),It({icons:{main:yn(s),mask:f?yn(f.icon):{found:!1,width:null,height:null,icon:{}}},prefix:M,iconName:N,transform:c({},P,e),symbol:i,title:m,maskId:u,titleId:k,extra:{attributes:h,styles:C,classes:b}})})}});function pn(n){if(n===null||typeof n=="object"&&n.prefix&&n.iconName)return n;if(Array.isArray(n)&&n.length===2){const[t,a]=n;return{prefix:t,iconName:a}}if(typeof n=="string")return{prefix:"fas",iconName:n}}function Ht(n){let t,a;return{c(){t=new Yn(!1),a=rn(),this.h()},l(e){t=Xn(e,!1),a=rn(),this.h()},h(){t.a=a},m(e,r){t.m(n[0],e,r),Bn(e,a,r)},p(e,[r]){r&1&&t.p(e[0])},i:on,o:on,d(e){e&&Gn(a),e&&t.d()}}}function jt(n,t,a){let e,r,{border:i=!1}=t,{fixedWidth:o=!1}=t,{flip:f=null}=t,{icon:l=null}=t,{mask:u=null}=t,{listItem:d=!1}=t,{pull:m=null}=t,{pulse:w=!1}=t,{rotation:k=null}=t,{swapOpacity:x=!1}=t,{size:b=null}=t,{spin:v=!1}=t,{transform:h={}}=t,{symbol:z=!1}=t,{title:C=null}=t,{inverse:M=!1}=t,N="";return qn(()=>{const s=pn(l);if(!s)return;const A=K(s),L=Rt(A||l,{symbol:z,title:C,styles:t.style?r:{},classes:[...Object.keys(e).map(S=>e[S]?S:null).filter(S=>!!S),(t.class||"").split(" ")],transform:{...typeof h=="string"?_t.transform(h):h},mask:pn(u)});if(!L){console.warn("Could not find one or more icon(s)",A||l,u);return}a(0,N=L.html)}),n.$$set=s=>{a(19,t=fn(fn({},t),sn(s))),"border"in s&&a(1,i=s.border),"fixedWidth"in s&&a(2,o=s.fixedWidth),"flip"in s&&a(3,f=s.flip),"icon"in s&&a(4,l=s.icon),"mask"in s&&a(5,u=s.mask),"listItem"in s&&a(6,d=s.listItem),"pull"in s&&a(7,m=s.pull),"pulse"in s&&a(8,w=s.pulse),"rotation"in s&&a(9,k=s.rotation),"swapOpacity"in s&&a(10,x=s.swapOpacity),"size"in s&&a(11,b=s.size),"spin"in s&&a(12,v=s.spin),"transform"in s&&a(13,h=s.transform),"symbol"in s&&a(14,z=s.symbol),"title"in s&&a(15,C=s.title),"inverse"in s&&a(16,M=s.inverse)},n.$$.update=()=>{n.$$.dirty&73678&&(e={"fa-spin":v,"fa-pulse":w,"fa-fw":o,"fa-border":i,"fa-li":d,"fa-inverse":M,"fa-flip-horizontal":["both","horizontal"].includes(f),"fa-flip-vertical":["both","vertical"].includes(f),[`fa-${b}`]:b!==null,[`fa-rotate-${k}`]:k!==null,[`fa-pull-${m}`]:m!==null,"fa-swap-opacity":x}),r=(t.style||"").split(";").filter(s=>!!s).map(s=>s.split(":").map(A=>A.trim())).reduce((s,A)=>{const[L,S]=A;return s[L]=S,s},{})},t=sn(t),[N,i,o,f,l,u,d,m,w,k,x,b,v,h,z,C,M]}class Dt extends Vn{constructor(t){super(),Wn(this,t,jt,Ht,Un,{border:1,fixedWidth:2,flip:3,icon:4,mask:5,listItem:6,pull:7,pulse:8,rotation:9,swapOpacity:10,size:11,spin:12,transform:13,symbol:14,title:15,inverse:16})}}/*! + * Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */var Ft={prefix:"fas",iconName:"at",icon:[512,512,[],"f1fa","M256 8C118.941 8 8 118.919 8 256c0 137.059 110.919 248 248 248 48.154 0 95.342-14.14 135.408-40.223 12.005-7.815 14.625-24.288 5.552-35.372l-10.177-12.433c-7.671-9.371-21.179-11.667-31.373-5.129C325.92 429.757 291.314 440 256 440c-101.458 0-184-82.542-184-184S154.542 72 256 72c100.139 0 184 57.619 184 160 0 38.786-21.093 79.742-58.17 83.693-17.349-.454-16.91-12.857-13.476-30.024l23.433-121.11C394.653 149.75 383.308 136 368.225 136h-44.981a13.518 13.518 0 0 0-13.432 11.993l-.01.092c-14.697-17.901-40.448-21.775-59.971-21.775-74.58 0-137.831 62.234-137.831 151.46 0 65.303 36.785 105.87 96 105.87 26.984 0 57.369-15.637 74.991-38.333 9.522 34.104 40.613 34.103 70.71 34.103C462.609 379.41 504 307.798 504 232 504 95.653 394.023 8 256 8zm-21.68 304.43c-22.249 0-36.07-15.623-36.07-40.771 0-44.993 30.779-72.729 58.63-72.729 22.292 0 35.601 15.241 35.601 40.77 0 45.061-33.875 72.73-58.161 72.73z"]},Vt={prefix:"fas",iconName:"border-all",icon:[448,512,[],"f84c","M416 32H32A32 32 0 0 0 0 64v384a32 32 0 0 0 32 32h384a32 32 0 0 0 32-32V64a32 32 0 0 0-32-32zm-32 64v128H256V96zm-192 0v128H64V96zM64 416V288h128v128zm192 0V288h128v128z"]},Wt={prefix:"fas",iconName:"comment-alt",icon:[512,512,[],"f27a","M448 0H64C28.7 0 0 28.7 0 64v288c0 35.3 28.7 64 64 64h96v84c0 9.8 11.2 15.5 19.1 9.7L304 416h144c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64z"]},Ut={prefix:"fas",iconName:"external-link-alt",icon:[512,512,[],"f35d","M432,320H400a16,16,0,0,0-16,16V448H64V128H208a16,16,0,0,0,16-16V80a16,16,0,0,0-16-16H48A48,48,0,0,0,0,112V464a48,48,0,0,0,48,48H400a48,48,0,0,0,48-48V336A16,16,0,0,0,432,320ZM488,0h-128c-21.37,0-32.05,25.91-17,41l35.73,35.73L135,320.37a24,24,0,0,0,0,34L157.67,377a24,24,0,0,0,34,0L435.28,133.32,471,169c15,15,41,4.5,41-17V24A24,24,0,0,0,488,0Z"]},Yt={prefix:"fas",iconName:"hashtag",icon:[448,512,[],"f292","M440.667 182.109l7.143-40c1.313-7.355-4.342-14.109-11.813-14.109h-74.81l14.623-81.891C377.123 38.754 371.468 32 363.997 32h-40.632a12 12 0 0 0-11.813 9.891L296.175 128H197.54l14.623-81.891C213.477 38.754 207.822 32 200.35 32h-40.632a12 12 0 0 0-11.813 9.891L132.528 128H53.432a12 12 0 0 0-11.813 9.891l-7.143 40C33.163 185.246 38.818 192 46.289 192h74.81L98.242 320H19.146a12 12 0 0 0-11.813 9.891l-7.143 40C-1.123 377.246 4.532 384 12.003 384h74.81L72.19 465.891C70.877 473.246 76.532 480 84.003 480h40.632a12 12 0 0 0 11.813-9.891L151.826 384h98.634l-14.623 81.891C234.523 473.246 240.178 480 247.65 480h40.632a12 12 0 0 0 11.813-9.891L315.472 384h79.096a12 12 0 0 0 11.813-9.891l7.143-40c1.313-7.355-4.342-14.109-11.813-14.109h-74.81l22.857-128h79.096a12 12 0 0 0 11.813-9.891zM261.889 320h-98.634l22.857-128h98.634l-22.857 128z"]},Xt={prefix:"fas",iconName:"rss",icon:[448,512,[],"f09e","M128.081 415.959c0 35.369-28.672 64.041-64.041 64.041S0 451.328 0 415.959s28.672-64.041 64.041-64.041 64.04 28.673 64.04 64.041zm175.66 47.25c-8.354-154.6-132.185-278.587-286.95-286.95C7.656 175.765 0 183.105 0 192.253v48.069c0 8.415 6.49 15.472 14.887 16.018 111.832 7.284 201.473 96.702 208.772 208.772.547 8.397 7.604 14.887 16.018 14.887h48.069c9.149.001 16.489-7.655 15.995-16.79zm144.249.288C439.596 229.677 251.465 40.445 16.503 32.01 7.473 31.686 0 38.981 0 48.016v48.068c0 8.625 6.835 15.645 15.453 15.999 191.179 7.839 344.627 161.316 352.465 352.465.353 8.618 7.373 15.453 15.999 15.453h48.068c9.034-.001 16.329-7.474 16.005-16.504z"]},Bt={prefix:"fas",iconName:"user",icon:[448,512,[],"f007","M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"]};/*! + * Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */var Gt={prefix:"fab",iconName:"dev",icon:[448,512,[],"f6cc","M120.12 208.29c-3.88-2.9-7.77-4.35-11.65-4.35H91.03v104.47h17.45c3.88 0 7.77-1.45 11.65-4.35 3.88-2.9 5.82-7.25 5.82-13.06v-69.65c-.01-5.8-1.96-10.16-5.83-13.06zM404.1 32H43.9C19.7 32 .06 51.59 0 75.8v360.4C.06 460.41 19.7 480 43.9 480h360.2c24.21 0 43.84-19.59 43.9-43.8V75.8c-.06-24.21-19.7-43.8-43.9-43.8zM154.2 291.19c0 18.81-11.61 47.31-48.36 47.25h-46.4V172.98h47.38c35.44 0 47.36 28.46 47.37 47.28l.01 70.93zm100.68-88.66H201.6v38.42h32.57v29.57H201.6v38.41h53.29v29.57h-62.18c-11.16.29-20.44-8.53-20.72-19.69V193.7c-.27-11.15 8.56-20.41 19.71-20.69h63.19l-.01 29.52zm103.64 115.29c-13.2 30.75-36.85 24.63-47.44 0l-38.53-144.8h32.57l29.71 113.72 29.57-113.72h32.58l-38.46 144.8z"]},qt={prefix:"fab",iconName:"flickr",icon:[448,512,[],"f16e","M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM144.5 319c-35.1 0-63.5-28.4-63.5-63.5s28.4-63.5 63.5-63.5 63.5 28.4 63.5 63.5-28.4 63.5-63.5 63.5zm159 0c-35.1 0-63.5-28.4-63.5-63.5s28.4-63.5 63.5-63.5 63.5 28.4 63.5 63.5-28.4 63.5-63.5 63.5z"]},Zt={prefix:"fab",iconName:"github",icon:[496,512,[],"f09b","M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"]},Jt={prefix:"fab",iconName:"instagram",icon:[448,512,[],"f16d","M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"]},Qt={prefix:"fab",iconName:"mastodon",icon:[448,512,[],"f4f6","M433 179.11c0-97.2-63.71-125.7-63.71-125.7-62.52-28.7-228.56-28.4-290.48 0 0 0-63.72 28.5-63.72 125.7 0 115.7-6.6 259.4 105.63 289.1 40.51 10.7 75.32 13 103.33 11.4 50.81-2.8 79.32-18.1 79.32-18.1l-1.7-36.9s-36.31 11.4-77.12 10.1c-40.41-1.4-83-4.4-89.63-54a102.54 102.54 0 0 1-.9-13.9c85.63 20.9 158.65 9.1 178.75 6.7 56.12-6.7 105-41.3 111.23-72.9 9.8-49.8 9-121.5 9-121.5zm-75.12 125.2h-46.63v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.33V197c0-58.5-64-56.6-64-6.9v114.2H90.19c0-122.1-5.2-147.9 18.41-175 25.9-28.9 79.82-30.8 103.83 6.1l11.6 19.5 11.6-19.5c24.11-37.1 78.12-34.8 103.83-6.1 23.71 27.3 18.4 53 18.4 175z"]},Kt={prefix:"fab",iconName:"twitter",icon:[512,512,[],"f099","M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"]};Fn.add(Ft,Vt,Wt,Gt,Ut,qt,Zt,Yt,Jt,Qt,Xt,Kt,Bt);const ne=Dt;export{ne as F}; diff --git a/_app/immutable/chunks/index.8c1dbec0.js b/_app/immutable/chunks/index.8c1dbec0.js new file mode 100644 index 00000000..4b5a21f1 --- /dev/null +++ b/_app/immutable/chunks/index.8c1dbec0.js @@ -0,0 +1 @@ +function $(){}function I(t,e){for(const n in e)t[n]=e[n];return t}function B(t){return t()}function L(){return Object.create(null)}function g(t){t.forEach(B)}function O(t){return typeof t=="function"}function ht(t,e){return t!=t?e==e:t!==e||t&&typeof t=="object"||typeof t=="function"}let b;function mt(t,e){return b||(b=document.createElement("a")),b.href=e,t===b.href}function J(t){return Object.keys(t).length===0}function K(t,...e){if(t==null)return $;const n=t.subscribe(...e);return n.unsubscribe?()=>n.unsubscribe():n}function pt(t,e,n){t.$$.on_destroy.push(K(e,n))}function yt(t,e,n,i){if(t){const r=q(t,e,n,i);return t[0](r)}}function q(t,e,n,i){return t[1]&&i?I(n.ctx.slice(),t[1](i(e))):n.ctx}function gt(t,e,n,i){if(t[2]&&i){const r=t[2](i(n));if(e.dirty===void 0)return r;if(typeof r=="object"){const u=[],s=Math.max(e.dirty.length,r.length);for(let o=0;o32){const e=[],n=t.ctx.length/32;for(let i=0;i>1);n(r)<=i?t=r+1:e=r}return t}function Y(t){if(t.hydrate_init)return;t.hydrate_init=!0;let e=t.childNodes;if(t.nodeName==="HEAD"){const c=[];for(let l=0;l0&&e[n[r]].claim_order<=l?r+1:X(1,r,x=>e[n[x]].claim_order,l))-1;i[c]=n[f]+1;const a=f+1;n[a]=c,r=Math.max(a,r)}const u=[],s=[];let o=e.length-1;for(let c=n[r]+1;c!=0;c=i[c-1]){for(u.push(e[c-1]);o>=c;o--)s.push(e[o]);o--}for(;o>=0;o--)s.push(e[o]);u.reverse(),s.sort((c,l)=>c.claim_order-l.claim_order);for(let c=0,l=0;c=u[l].claim_order;)l++;const f=lt.removeEventListener(e,n,i)}function wt(t){return function(e){return e.preventDefault(),t.call(this,e)}}function At(t,e,n){n==null?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function it(t){return Array.from(t.childNodes)}function R(t){t.claim_info===void 0&&(t.claim_info={last_index:0,total_claimed:0})}function z(t,e,n,i,r=!1){R(t);const u=(()=>{for(let s=t.claim_info.last_index;s=0;s--){const o=t[s];if(e(o)){const c=n(o);return c===void 0?t.splice(s,1):t[s]=c,r?c===void 0&&t.claim_info.last_index--:t.claim_info.last_index=s,o}}return i()})();return u.claim_order=t.claim_info.total_claimed,t.claim_info.total_claimed+=1,u}function rt(t,e,n,i){return z(t,r=>r.nodeName===e,r=>{const u=[];for(let s=0;sr.removeAttribute(s))},()=>i(e))}function Mt(t,e,n){return rt(t,e,n,G)}function st(t,e){return z(t,n=>n.nodeType===3,n=>{const i=""+e;if(n.data.startsWith(i)){if(n.data.length!==i.length)return n.splitText(i.length)}else n.data=i},()=>S(e),!0)}function St(t){return st(t," ")}function j(t,e,n){for(let i=n;i0&&n.push(r);return n}class lt{constructor(e=!1){this.is_svg=!1,this.is_svg=e,this.e=this.n=null}c(e){this.h(e)}m(e,n,i=null){this.e||(this.is_svg?this.e=nt(n.nodeName):this.e=G(n.nodeType===11?"TEMPLATE":n.nodeName),this.t=n.tagName!=="TEMPLATE"?n:n.content,this.c(e)),this.i(i)}h(e){this.e.innerHTML=e,this.n=Array.from(this.e.nodeName==="TEMPLATE"?this.e.content.childNodes:this.e.childNodes)}i(e){for(let n=0;n{const r=t.$$.callbacks[e];if(r){const u=ct(e,n,{cancelable:i});return r.slice().forEach(s=>{s.call(t,u)}),!u.defaultPrevented}return!0}}const h=[],D=[];let m=[];const P=[],F=Promise.resolve();let A=!1;function U(){A||(A=!0,F.then(W))}function Gt(){return U(),F}function M(t){m.push(t)}const w=new Set;let d=0;function W(){if(d!==0)return;const t=y;do{try{for(;dt.indexOf(i)===-1?e.push(i):n.push(i)),n.forEach(i=>i()),m=e}const E=new Set;let _;function Rt(){_={r:0,c:[],p:_}}function zt(){_.r||g(_.c),_=_.p}function at(t,e){t&&t.i&&(E.delete(t),t.i(e))}function Ft(t,e,n,i){if(t&&t.o){if(E.has(t))return;E.add(t),_.c.push(()=>{E.delete(t),i&&(n&&t.d(1),i())}),t.o(e)}else i&&i()}function Ut(t){t&&t.c()}function Wt(t,e){t&&t.l(e)}function ft(t,e,n,i){const{fragment:r,after_update:u}=t.$$;r&&r.m(e,n),i||M(()=>{const s=t.$$.on_mount.map(B).filter(O);t.$$.on_destroy?t.$$.on_destroy.push(...s):g(s),t.$$.on_mount=[]}),u.forEach(M)}function _t(t,e){const n=t.$$;n.fragment!==null&&(ot(n.after_update),g(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function dt(t,e){t.$$.dirty[0]===-1&&(h.push(t),U(),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{const H=C.length?C[0]:x;return l.ctx&&r(l.ctx[a],l.ctx[a]=H)&&(!l.skip_bound&&l.bound[a]&&l.bound[a](H),f&&dt(t,a)),x}):[],l.update(),f=!0,g(l.before_update),l.fragment=i?i(l.ctx):!1,e.target){if(e.hydrate){Q();const a=it(e.target);l.fragment&&l.fragment.l(a),a.forEach(v)}else l.fragment&&l.fragment.c();e.intro&&at(t.$$.fragment),ft(t,e.target,e.anchor,e.customElement),V(),W()}p(c)}class Jt{$destroy(){_t(this,1),this.$destroy=$}$on(e,n){if(!O(n))return $;const i=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return i.push(n),()=>{const r=i.indexOf(n);r!==-1&&i.splice(r,1)}}$set(e){this.$$set&&!J(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}}export{ft as A,_t as B,Z as C,Nt as D,$ as E,g as F,yt as G,xt as H,bt as I,gt as J,pt as K,$t as L,kt as M,mt as N,Lt as O,wt as P,qt as Q,k as R,Jt as S,Ct as T,Pt as U,I as V,Et as W,vt as a,et as b,St as c,Ft as d,Tt as e,zt as f,at as g,v as h,It as i,Ot as j,G as k,Mt as l,it as m,At as n,Bt as o,jt as p,S as q,st as r,ht as s,Gt as t,Ht as u,Rt as v,D as w,Dt as x,Ut as y,Wt as z}; diff --git a/_app/immutable/chunks/mf2.47734ed4.js b/_app/immutable/chunks/mf2.47734ed4.js new file mode 100644 index 00000000..04051ba4 --- /dev/null +++ b/_app/immutable/chunks/mf2.47734ed4.js @@ -0,0 +1 @@ +const n="Tim Bachmann",t="Tim",e="Bachmann",i="Tiim",o="hey@tiim.ch",a="https://media.tiim.ch/tiim.jpg",m="https://tiim.ch/",c="Basel",s="Switzerland",l="University of Basel",r="",h="master graduate in computer science",y="male",_={name:n,given_name:t,family_name:e,nickname:i,email:o,photo:a,url:m,locality:c,country_name:s,org:l,job_title:r,role:h,gender_identity:y};export{_ as m}; diff --git a/_app/immutable/chunks/paths.c95190e0.js b/_app/immutable/chunks/paths.c95190e0.js new file mode 100644 index 00000000..9fc9e63b --- /dev/null +++ b/_app/immutable/chunks/paths.c95190e0.js @@ -0,0 +1 @@ +var s;const a=((s=globalThis.__sveltekit_t1zbm9)==null?void 0:s.base)??"";var t;const e=((t=globalThis.__sveltekit_t1zbm9)==null?void 0:t.assets)??a;export{e as a,a as b}; diff --git a/_app/immutable/chunks/singletons.0b9e2925.js b/_app/immutable/chunks/singletons.0b9e2925.js new file mode 100644 index 00000000..6f578ef1 --- /dev/null +++ b/_app/immutable/chunks/singletons.0b9e2925.js @@ -0,0 +1 @@ +import{E as d,s as m}from"./index.8c1dbec0.js";import{a as k}from"./paths.c95190e0.js";const u=[];function p(e,t=d){let n;const o=new Set;function r(s){if(m(e,s)&&(e=s,n)){const l=!u.length;for(const i of o)i[1](),u.push(i,e);if(l){for(let i=0;i{o.delete(i),o.size===0&&n&&(n(),n=null)}}return{set:r,update:c,subscribe:a}}const E="1695246146286",S="sveltekit:snapshot",y="sveltekit:scroll",I="sveltekit:index",_={tap:1,hover:2,viewport:3,eager:4,off:-1};function T(e){let t=e.baseURI;if(!t){const n=e.getElementsByTagName("base");t=n.length?n[0].href:e.URL}return t}function x(){return{x:pageXOffset,y:pageYOffset}}function f(e,t){return e.getAttribute(`data-sveltekit-${t}`)}const b={..._,"":_.hover};function h(e){let t=e.assignedSlot??e.parentNode;return(t==null?void 0:t.nodeType)===11&&(t=t.host),t}function O(e,t){for(;e&&e!==t;){if(e.nodeName.toUpperCase()==="A"&&e.hasAttribute("href"))return e;e=h(e)}}function U(e,t){let n;try{n=new URL(e instanceof SVGAElement?e.href.baseVal:e.href,document.baseURI)}catch{}const o=e instanceof SVGAElement?e.target.baseVal:e.target,r=!n||!!o||w(n,t)||(e.getAttribute("rel")||"").split(/\s+/).includes("external"),c=(n==null?void 0:n.origin)===location.origin&&e.hasAttribute("download");return{url:n,external:r,target:o,download:c}}function L(e){let t=null,n=null,o=null,r=null,c=null,a=null,s=e;for(;s&&s!==document.documentElement;)o===null&&(o=f(s,"preload-code")),r===null&&(r=f(s,"preload-data")),t===null&&(t=f(s,"keepfocus")),n===null&&(n=f(s,"noscroll")),c===null&&(c=f(s,"reload")),a===null&&(a=f(s,"replacestate")),s=h(s);function l(i){switch(i){case"":case"true":return!0;case"off":case"false":return!1;default:return null}}return{preload_code:b[o??"off"],preload_data:b[r??"off"],keep_focus:l(t),noscroll:l(n),reload:l(c),replace_state:l(a)}}function g(e){const t=p(e);let n=!0;function o(){n=!0,t.update(a=>a)}function r(a){n=!1,t.set(a)}function c(a){let s;return t.subscribe(l=>{(s===void 0||n&&l!==s)&&a(s=l)})}return{notify:o,set:r,subscribe:c}}function v(){const{set:e,subscribe:t}=p(!1);let n;async function o(){clearTimeout(n);try{const r=await fetch(`${k}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!r.ok)return!1;const a=(await r.json()).version!==E;return a&&(e(!0),clearTimeout(n)),a}catch{return!1}}return{subscribe:t,check:o}}function w(e,t){return e.origin!==location.origin||!e.pathname.startsWith(t)}function N(e){e.client}const P={url:g({}),page:g({}),navigating:p(null),updated:v()};export{I,_ as P,y as S,S as a,U as b,L as c,P as d,N as e,O as f,T as g,w as i,x as s}; diff --git a/_app/immutable/chunks/socialmedia.caeda4dc.js b/_app/immutable/chunks/socialmedia.caeda4dc.js new file mode 100644 index 00000000..57d5defd --- /dev/null +++ b/_app/immutable/chunks/socialmedia.caeda4dc.js @@ -0,0 +1 @@ +const i=[{site:"Matrix",handle:"@tiimb:matrix.org",link:"https://matrix.to/#/@tiimb:matrix.org",icon:["fas","comment-alt"],type:["contact"]},{site:"Email",handle:"hey@tiim.ch",link:"mailto:hey@tiim.ch",icon:["fas","at"],type:["contact"]},{site:"RSS Feed",link:"https://tiim.ch/blog/rss.xml",icon:["fas","rss"],type:["follow"]},{site:"IRC",handle:"tiim in ##tiim on libera.chat",link:"https://web.libera.chat/##tiim",icon:["fas","hashtag"],type:["contact"]},{site:"Github",handle:"Tiim",link:"https://github.com/Tiim",icon:["fab","github"],type:["follow"]},{site:"Mastodon",handle:"@Tiim@indieweb.social",link:"https://indieweb.social/@Tiim",icon:["fab","mastodon"],type:["follow"]},{site:"Dev.to",handle:"Tiim",link:"https://dev.to/tiim",icon:["fab","dev"],type:["follow"]},{site:"Twitter",handle:"@TiimB",link:"https://twitter.com/TiimB",icon:["fab","twitter"],type:["follow","contact"]},{site:"Instagram",handle:"@tiim.ba",link:"https://instagram.com/tiim.ba",icon:["fab","instagram"],type:["follow"]},{site:"Flickr",link:"https://www.flickr.com/people/152309161@N02/",icon:["fab","flickr"],type:["follow"]}];export{i as s}; diff --git a/_app/immutable/entry/app.fd794c3d.js b/_app/immutable/entry/app.fd794c3d.js new file mode 100644 index 00000000..41ff3b09 --- /dev/null +++ b/_app/immutable/entry/app.fd794c3d.js @@ -0,0 +1 @@ +import{S as C,i as j,s as q,a as U,e as d,c as z,b as w,d as h,f as P,g,h as E,j as W,o as F,k as G,l as H,m as J,n as D,p,q as K,r as M,u as Q,v as A,t as X,w as I,x as b,y as k,z as O,A as R,B as L}from"../chunks/index.8c1dbec0.js";const Y="modulepreload",Z=function(a,e){return new URL(a,e).href},T={},m=function(e,n,i){if(!n||n.length===0)return e();const r=document.getElementsByTagName("link");return Promise.all(n.map(_=>{if(_=Z(_,i),_ in T)return;T[_]=!0;const t=_.endsWith(".css"),s=t?'[rel="stylesheet"]':"";if(!!i)for(let l=r.length-1;l>=0;l--){const u=r[l];if(u.href===_&&(!t||u.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${_}"]${s}`))return;const o=document.createElement("link");if(o.rel=t?"stylesheet":Y,t||(o.as="script",o.crossOrigin=""),o.href=_,document.head.appendChild(o),t)return new Promise((l,u)=>{o.addEventListener("load",l),o.addEventListener("error",()=>u(new Error(`Unable to preload CSS for ${_}`)))})})).then(()=>e()).catch(_=>{const t=new Event("vite:preloadError",{cancelable:!0});if(t.payload=_,window.dispatchEvent(t),!t.defaultPrevented)throw _})},re={};function $(a){let e,n,i;var r=a[1][0];function _(t){return{props:{data:t[3],form:t[2]}}}return r&&(e=b(r,_(a)),a[12](e)),{c(){e&&k(e.$$.fragment),n=d()},l(t){e&&O(e.$$.fragment,t),n=d()},m(t,s){e&&R(e,t,s),w(t,n,s),i=!0},p(t,s){const c={};if(s&8&&(c.data=t[3]),s&4&&(c.form=t[2]),s&2&&r!==(r=t[1][0])){if(e){A();const o=e;h(o.$$.fragment,1,0,()=>{L(o,1)}),P()}r?(e=b(r,_(t)),t[12](e),k(e.$$.fragment),g(e.$$.fragment,1),R(e,n.parentNode,n)):e=null}else r&&e.$set(c)},i(t){i||(e&&g(e.$$.fragment,t),i=!0)},o(t){e&&h(e.$$.fragment,t),i=!1},d(t){a[12](null),t&&E(n),e&&L(e,t)}}}function x(a){let e,n,i;var r=a[1][0];function _(t){return{props:{data:t[3],$$slots:{default:[ee]},$$scope:{ctx:t}}}}return r&&(e=b(r,_(a)),a[11](e)),{c(){e&&k(e.$$.fragment),n=d()},l(t){e&&O(e.$$.fragment,t),n=d()},m(t,s){e&&R(e,t,s),w(t,n,s),i=!0},p(t,s){const c={};if(s&8&&(c.data=t[3]),s&8215&&(c.$$scope={dirty:s,ctx:t}),s&2&&r!==(r=t[1][0])){if(e){A();const o=e;h(o.$$.fragment,1,0,()=>{L(o,1)}),P()}r?(e=b(r,_(t)),t[11](e),k(e.$$.fragment),g(e.$$.fragment,1),R(e,n.parentNode,n)):e=null}else r&&e.$set(c)},i(t){i||(e&&g(e.$$.fragment,t),i=!0)},o(t){e&&h(e.$$.fragment,t),i=!1},d(t){a[11](null),t&&E(n),e&&L(e,t)}}}function ee(a){let e,n,i;var r=a[1][1];function _(t){return{props:{data:t[4],form:t[2]}}}return r&&(e=b(r,_(a)),a[10](e)),{c(){e&&k(e.$$.fragment),n=d()},l(t){e&&O(e.$$.fragment,t),n=d()},m(t,s){e&&R(e,t,s),w(t,n,s),i=!0},p(t,s){const c={};if(s&16&&(c.data=t[4]),s&4&&(c.form=t[2]),s&2&&r!==(r=t[1][1])){if(e){A();const o=e;h(o.$$.fragment,1,0,()=>{L(o,1)}),P()}r?(e=b(r,_(t)),t[10](e),k(e.$$.fragment),g(e.$$.fragment,1),R(e,n.parentNode,n)):e=null}else r&&e.$set(c)},i(t){i||(e&&g(e.$$.fragment,t),i=!0)},o(t){e&&h(e.$$.fragment,t),i=!1},d(t){a[10](null),t&&E(n),e&&L(e,t)}}}function V(a){let e,n=a[6]&&y(a);return{c(){e=G("div"),n&&n.c(),this.h()},l(i){e=H(i,"DIV",{id:!0,"aria-live":!0,"aria-atomic":!0,style:!0});var r=J(e);n&&n.l(r),r.forEach(E),this.h()},h(){D(e,"id","svelte-announcer"),D(e,"aria-live","assertive"),D(e,"aria-atomic","true"),p(e,"position","absolute"),p(e,"left","0"),p(e,"top","0"),p(e,"clip","rect(0 0 0 0)"),p(e,"clip-path","inset(50%)"),p(e,"overflow","hidden"),p(e,"white-space","nowrap"),p(e,"width","1px"),p(e,"height","1px")},m(i,r){w(i,e,r),n&&n.m(e,null)},p(i,r){i[6]?n?n.p(i,r):(n=y(i),n.c(),n.m(e,null)):n&&(n.d(1),n=null)},d(i){i&&E(e),n&&n.d()}}}function y(a){let e;return{c(){e=K(a[7])},l(n){e=M(n,a[7])},m(n,i){w(n,e,i)},p(n,i){i&128&&Q(e,n[7])},d(n){n&&E(e)}}}function te(a){let e,n,i,r,_;const t=[x,$],s=[];function c(l,u){return l[1][1]?0:1}e=c(a),n=s[e]=t[e](a);let o=a[5]&&V(a);return{c(){n.c(),i=U(),o&&o.c(),r=d()},l(l){n.l(l),i=z(l),o&&o.l(l),r=d()},m(l,u){s[e].m(l,u),w(l,i,u),o&&o.m(l,u),w(l,r,u),_=!0},p(l,[u]){let v=e;e=c(l),e===v?s[e].p(l,u):(A(),h(s[v],1,1,()=>{s[v]=null}),P(),n=s[e],n?n.p(l,u):(n=s[e]=t[e](l),n.c()),g(n,1),n.m(i.parentNode,i)),l[5]?o?o.p(l,u):(o=V(l),o.c(),o.m(r.parentNode,r)):o&&(o.d(1),o=null)},i(l){_||(g(n),_=!0)},o(l){h(n),_=!1},d(l){s[e].d(l),l&&E(i),o&&o.d(l),l&&E(r)}}}function ne(a,e,n){let{stores:i}=e,{page:r}=e,{constructors:_}=e,{components:t=[]}=e,{form:s}=e,{data_0:c=null}=e,{data_1:o=null}=e;W(i.page.notify);let l=!1,u=!1,v=null;F(()=>{const f=i.page.subscribe(()=>{l&&(n(6,u=!0),X().then(()=>{n(7,v=document.title||"untitled page")}))});return n(5,l=!0),f});function N(f){I[f?"unshift":"push"](()=>{t[1]=f,n(0,t)})}function S(f){I[f?"unshift":"push"](()=>{t[0]=f,n(0,t)})}function B(f){I[f?"unshift":"push"](()=>{t[0]=f,n(0,t)})}return a.$$set=f=>{"stores"in f&&n(8,i=f.stores),"page"in f&&n(9,r=f.page),"constructors"in f&&n(1,_=f.constructors),"components"in f&&n(0,t=f.components),"form"in f&&n(2,s=f.form),"data_0"in f&&n(3,c=f.data_0),"data_1"in f&&n(4,o=f.data_1)},a.$$.update=()=>{a.$$.dirty&768&&i.page.set(r)},[t,_,s,c,o,l,u,v,i,r,N,S,B]}class se extends C{constructor(e){super(),j(this,e,ne,te,q,{stores:8,page:9,constructors:1,components:0,form:2,data_0:3,data_1:4})}}const oe=[()=>m(()=>import("../nodes/0.9334d053.js"),["../nodes/0.9334d053.js","../chunks/index.8c1dbec0.js","../chunks/mf2.47734ed4.js","../assets/0.8d503a92.css"],import.meta.url),()=>m(()=>import("../nodes/1.1caf0f88.js"),["../nodes/1.1caf0f88.js","../chunks/index.8c1dbec0.js","../chunks/singletons.0b9e2925.js","../chunks/paths.c95190e0.js"],import.meta.url),()=>m(()=>import("../nodes/2.5e216872.js"),["../nodes/2.5e216872.js","../chunks/index.8c1dbec0.js","../chunks/socialmedia.caeda4dc.js","../chunks/fa.9f83fec8.js","../chunks/PostCardList.ba348fd5.js","../assets/PostCardList.fbe0a418.css","../assets/2.9c80008b.css"],import.meta.url),()=>m(()=>import("../nodes/3.035a3a74.js"),["../nodes/3.035a3a74.js","../chunks/index.8c1dbec0.js","../chunks/fa.9f83fec8.js","../chunks/PostCardList.ba348fd5.js","../assets/PostCardList.fbe0a418.css"],import.meta.url),()=>m(()=>import("../nodes/4.07141e4d.js"),["../nodes/4.07141e4d.js","../chunks/index.8c1dbec0.js","../chunks/MarkdownSite.7d9e3e05.js","../chunks/paths.c95190e0.js","../chunks/mf2.47734ed4.js","../chunks/fa.9f83fec8.js","../assets/MarkdownSite.52351ed7.css","../assets/4.32b0d823.css"],import.meta.url),()=>m(()=>import("../nodes/5.a52c5d6a.js"),["../nodes/5.a52c5d6a.js","../chunks/index.8c1dbec0.js","../chunks/socialmedia.caeda4dc.js","../chunks/fa.9f83fec8.js","../assets/5.9fa4e00e.css"],import.meta.url),()=>m(()=>import("../nodes/6.deabb5c6.js"),["../nodes/6.deabb5c6.js","../chunks/index.8c1dbec0.js","../chunks/socialmedia.caeda4dc.js","../chunks/fa.9f83fec8.js","../assets/6.83f5b710.css"],import.meta.url),()=>m(()=>import("../nodes/7.5881b71c.js"),["../nodes/7.5881b71c.js","../chunks/index.8c1dbec0.js","../chunks/PostCardList.ba348fd5.js","../assets/PostCardList.fbe0a418.css"],import.meta.url),()=>m(()=>import("../nodes/8.a55c2225.js"),["../nodes/8.a55c2225.js","../chunks/index.8c1dbec0.js","../chunks/MarkdownSite.7d9e3e05.js","../chunks/paths.c95190e0.js","../chunks/mf2.47734ed4.js","../chunks/fa.9f83fec8.js","../assets/MarkdownSite.52351ed7.css"],import.meta.url),()=>m(()=>import("../nodes/9.ae3d0625.js"),["../nodes/9.ae3d0625.js","../chunks/index.8c1dbec0.js","../chunks/MarkdownSite.7d9e3e05.js","../chunks/paths.c95190e0.js","../chunks/mf2.47734ed4.js","../chunks/fa.9f83fec8.js","../assets/MarkdownSite.52351ed7.css"],import.meta.url),()=>m(()=>import("../nodes/10.22593ef1.js"),["../nodes/10.22593ef1.js","../chunks/index.8c1dbec0.js","../chunks/PostCardList.ba348fd5.js","../assets/PostCardList.fbe0a418.css"],import.meta.url),()=>m(()=>import("../nodes/11.0166947d.js"),["../nodes/11.0166947d.js","../chunks/index.8c1dbec0.js","../chunks/MarkdownSite.7d9e3e05.js","../chunks/paths.c95190e0.js","../chunks/mf2.47734ed4.js","../chunks/fa.9f83fec8.js","../assets/MarkdownSite.52351ed7.css"],import.meta.url),()=>m(()=>import("../nodes/12.df862236.js"),["../nodes/12.df862236.js","../chunks/index.8c1dbec0.js","../assets/12.3bfdc038.css"],import.meta.url),()=>m(()=>import("../nodes/13.d68a3a4e.js"),["../nodes/13.d68a3a4e.js","../chunks/index.8c1dbec0.js","../chunks/PostCardList.ba348fd5.js","../assets/PostCardList.fbe0a418.css","../assets/13.a9b58653.css"],import.meta.url)],ae=[0],le={"/":[-3],"/blog":[-4],"/blog/[slug]":[-5],"/contact":[5],"/follow":[6],"/mf2":[-8],"/mf2/[...slug]":[-9],"/pages/[page_slug]":[-10],"/projects":[-11],"/projects/[slug]":[-12],"/tags":[-13],"/tags/[slug]":[-14]},_e={handleError:({error:a})=>{console.error(a)}};export{le as dictionary,_e as hooks,re as matchers,oe as nodes,se as root,ae as server_loads}; diff --git a/_app/immutable/entry/start.38d53a0a.js b/_app/immutable/entry/start.38d53a0a.js new file mode 100644 index 00000000..eb378468 --- /dev/null +++ b/_app/immutable/entry/start.38d53a0a.js @@ -0,0 +1,3 @@ +import{o as De,t as ye}from"../chunks/index.8c1dbec0.js";import{S as He,a as Je,I as V,g as Ce,f as Ve,b as we,c as le,s as ee,i as _e,d as M,P as qe,e as We}from"../chunks/singletons.0b9e2925.js";import{b as K}from"../chunks/paths.c95190e0.js";function Xe(n,o){return n==="/"||o==="ignore"?n:o==="never"?n.endsWith("/")?n.slice(0,-1):n:o==="always"&&!n.endsWith("/")?n+"/":n}function Ze(n){return n.split("%25").map(decodeURI).join("%25")}function Qe(n){for(const o in n)n[o]=decodeURIComponent(n[o]);return n}const et=["href","pathname","search","searchParams","toString","toJSON"];function tt(n,o){const u=new URL(n);for(const s of et)Object.defineProperty(u,s,{get(){return o(),n[s]},enumerable:!0,configurable:!0});return nt(u),u}function nt(n){Object.defineProperty(n,"hash",{get(){throw new Error("Cannot access event.url.hash. Consider using `$page.url.hash` inside a component instead")}})}const at="/__data.json";function rt(n){return n.replace(/\/$/,"")+at}function ot(...n){let o=5381;for(const u of n)if(typeof u=="string"){let s=u.length;for(;s;)o=o*33^u.charCodeAt(--s)}else if(ArrayBuffer.isView(u)){const s=new Uint8Array(u.buffer,u.byteOffset,u.byteLength);let d=s.length;for(;d;)o=o*33^s[--d]}else throw new TypeError("value must be a string or TypedArray");return(o>>>0).toString(36)}const fe=window.fetch;window.fetch=(n,o)=>((n instanceof Request?n.method:(o==null?void 0:o.method)||"GET")!=="GET"&&ne.delete(Se(n)),fe(n,o));const ne=new Map;function it(n,o){const u=Se(n,o),s=document.querySelector(u);if(s!=null&&s.textContent){const{body:d,...f}=JSON.parse(s.textContent),S=s.getAttribute("data-ttl");return S&&ne.set(u,{body:d,init:f,ttl:1e3*Number(S)}),Promise.resolve(new Response(d,f))}return fe(n,o)}function st(n,o,u){if(ne.size>0){const s=Se(n,u),d=ne.get(s);if(d){if(performance.now(){const d=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(s);if(d)return o.push({name:d[1],matcher:d[2],optional:!1,rest:!0,chained:!0}),"(?:/(.*))?";const f=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(s);if(f)return o.push({name:f[1],matcher:f[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!s)return;const S=s.split(/\[(.+?)\](?!\])/);return"/"+S.map((y,w)=>{if(w%2){if(y.startsWith("x+"))return be(String.fromCharCode(parseInt(y.slice(2),16)));if(y.startsWith("u+"))return be(String.fromCharCode(...y.slice(2).split("-").map(U=>parseInt(U,16))));const h=ct.exec(y);if(!h)throw new Error(`Invalid param: ${y}. Params and matcher names can only have underscores and alphanumeric characters.`);const[,D,x,k,N]=h;return o.push({name:k,matcher:N,optional:!!D,rest:!!x,chained:x?w===1&&S[0]==="":!1}),x?"(.*?)":D?"([^/]*)?":"([^/]+?)"}return be(y)}).join("")}).join("")}/?$`),params:o}}function ft(n){return!/^\([^)]+\)$/.test(n)}function ut(n){return n.slice(1).split("/").filter(ft)}function dt(n,o,u){const s={},d=n.slice(1);let f=0;for(let S=0;Sw).join("/"),f=0),y===void 0){l.rest&&(s[l.name]="");continue}if(!l.matcher||u[l.matcher](y)){s[l.name]=y;const w=o[S+1],h=d[S+1];w&&!w.rest&&w.optional&&h&&l.chained&&(f=0);continue}if(l.optional&&l.chained){f++;continue}return}if(!f)return s}function be(n){return n.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function pt({nodes:n,server_loads:o,dictionary:u,matchers:s}){const d=new Set(o);return Object.entries(u).map(([l,[y,w,h]])=>{const{pattern:D,params:x}=lt(l),k={id:l,exec:N=>{const U=D.exec(N);if(U)return dt(U,x,s)},errors:[1,...h||[]].map(N=>n[N]),layouts:[0,...w||[]].map(S),leaf:f(y)};return k.errors.length=k.layouts.length=Math.max(k.errors.length,k.layouts.length),k});function f(l){const y=l<0;return y&&(l=~l),[y,n[l]]}function S(l){return l===void 0?l:[d.has(l),n[l]]}}function Ke(n){try{return JSON.parse(sessionStorage[n])}catch{}}function Fe(n,o){const u=JSON.stringify(o);try{sessionStorage[n]=u}catch{}}const ht=-1,gt=-2,mt=-3,yt=-4,wt=-5,_t=-6;function bt(n,o){if(typeof n=="number")return d(n,!0);if(!Array.isArray(n)||n.length===0)throw new Error("Invalid input");const u=n,s=Array(u.length);function d(f,S=!1){if(f===ht)return;if(f===mt)return NaN;if(f===yt)return 1/0;if(f===wt)return-1/0;if(f===_t)return-0;if(S)throw new Error("Invalid input");if(f in s)return s[f];const l=u[f];if(!l||typeof l!="object")s[f]=l;else if(Array.isArray(l))if(typeof l[0]=="string"){const y=l[0],w=o==null?void 0:o[y];if(w)return s[f]=w(d(l[1]));switch(y){case"Date":s[f]=new Date(l[1]);break;case"Set":const h=new Set;s[f]=h;for(let k=1;ko!=null)}const ze=new Set(["load","prerender","csr","ssr","trailingSlash","config"]);[...ze];const Et=new Set([...ze]);[...Et];async function St(n){var o;for(const u in n)if(typeof((o=n[u])==null?void 0:o.then)=="function")return Object.fromEntries(await Promise.all(Object.entries(n).map(async([s,d])=>[s,await d])));return n}class te{constructor(o,u){this.status=o,typeof u=="string"?this.body={message:u}:u?this.body=u:this.body={message:`Error: ${o}`}}toString(){return JSON.stringify(this.body)}}class Me{constructor(o,u){this.status=o,this.location=u}}const kt="x-sveltekit-invalidated",z=Ke(He)??{},Q=Ke(Je)??{};function ve(n){z[n]=ee()}function Rt(n,o){var $e;const u=pt(n),s=n.nodes[0],d=n.nodes[1];s(),d();const f=document.documentElement,S=[],l=[];let y=null;const w={before_navigate:[],after_navigate:[]};let h={branch:[],error:null,url:null},D=!1,x=!1,k=!0,N=!1,U=!1,B=!1,H=!1,q,j=($e=history.state)==null?void 0:$e[V];j||(j=Date.now(),history.replaceState({...history.state,[V]:j},"",location.href));const ue=z[j];ue&&(history.scrollRestoration="manual",scrollTo(ue.x,ue.y));let F,ae,Y;async function ke(){if(Y=Y||Promise.resolve(),await Y,!Y)return;Y=null;const e=new URL(location.href),i=X(e,!0);y=null;const t=ae={},r=i&&await he(i);if(t===ae&&r){if(r.type==="redirect")return re(new URL(r.location,e).href,{},[e.pathname],t);r.props.page!==void 0&&(F=r.props.page),q.$set(r.props)}}function Re(e){l.some(i=>i==null?void 0:i.snapshot)&&(Q[e]=l.map(i=>{var t;return(t=i==null?void 0:i.snapshot)==null?void 0:t.capture()}))}function Ae(e){var i;(i=Q[e])==null||i.forEach((t,r)=>{var a,c;(c=(a=l[r])==null?void 0:a.snapshot)==null||c.restore(t)})}function Ie(){ve(j),Fe(He,z),Re(j),Fe(Je,Q)}async function re(e,{noScroll:i=!1,replaceState:t=!1,keepFocus:r=!1,state:a={},invalidateAll:c=!1},p,v){return typeof e=="string"&&(e=new URL(e,Ce(document))),ce({url:e,scroll:i?ee():null,keepfocus:r,redirect_chain:p,details:{state:a,replaceState:t},nav_token:v,accepted:()=>{c&&(H=!0)},blocked:()=>{},type:"goto"})}async function Le(e){return y={id:e.id,promise:he(e).then(i=>(i.type==="loaded"&&i.state.error&&(y=null),i))},y.promise}async function oe(...e){const t=u.filter(r=>e.some(a=>r.exec(a))).map(r=>Promise.all([...r.layouts,r.leaf].map(a=>a==null?void 0:a[1]())));await Promise.all(t)}function Oe(e){var r;h=e.state;const i=document.querySelector("style[data-sveltekit]");i&&i.remove(),F=e.props.page,q=new n.root({target:o,props:{...e.props,stores:M,components:l},hydrate:!0}),Ae(j);const t={from:null,to:{params:h.params,route:{id:((r=h.route)==null?void 0:r.id)??null},url:new URL(location.href)},willUnload:!1,type:"enter"};w.after_navigate.forEach(a=>a(t)),x=!0}async function W({url:e,params:i,branch:t,status:r,error:a,route:c,form:p}){let v="never";for(const g of t)(g==null?void 0:g.slash)!==void 0&&(v=g.slash);e.pathname=Xe(e.pathname,v),e.search=e.search;const b={type:"loaded",state:{url:e,params:i,branch:t,error:a,route:c},props:{constructors:vt(t).map(g=>g.node.component)}};p!==void 0&&(b.props.form=p);let _={},R=!F,A=0;for(let g=0;g(v.params.add(P),m[P])}),data:(c==null?void 0:c.data)??null,url:tt(t,()=>{v.url=!0}),async fetch(m,P){let $;m instanceof Request?($=m.url,P={body:m.method==="GET"||m.method==="HEAD"?void 0:await m.blob(),cache:m.cache,credentials:m.credentials,headers:m.headers,integrity:m.integrity,keepalive:m.keepalive,method:m.method,mode:m.mode,redirect:m.redirect,referrer:m.referrer,referrerPolicy:m.referrerPolicy,signal:m.signal,...P}):$=m;const C=new URL($,t);return I(C.href),C.origin===t.origin&&($=C.href.slice(t.origin.length)),x?st($,C.href,P):it($,P)},setHeaders:()=>{},depends:I,parent(){return v.parent=!0,i()}};p=await b.universal.load.call(null,g)??null,p=p?await St(p):null}return{node:b,loader:e,server:c,universal:(R=b.universal)!=null&&R.load?{type:"data",data:p,uses:v}:null,data:p??(c==null?void 0:c.data)??null,slash:((A=b.universal)==null?void 0:A.trailingSlash)??(c==null?void 0:c.slash)}}function Ue(e,i,t,r,a){if(H)return!0;if(!r)return!1;if(r.parent&&e||r.route&&i||r.url&&t)return!0;for(const c of r.params)if(a[c]!==h.params[c])return!0;for(const c of r.dependencies)if(S.some(p=>p(new URL(c))))return!0;return!1}function pe(e,i){return(e==null?void 0:e.type)==="data"?e:(e==null?void 0:e.type)==="skip"?i??null:null}async function he({id:e,invalidating:i,url:t,params:r,route:a}){if((y==null?void 0:y.id)===e)return y.promise;const{errors:c,layouts:p,leaf:v}=a,b=[...p,v];c.forEach(E=>E==null?void 0:E().catch(()=>{})),b.forEach(E=>E==null?void 0:E[1]().catch(()=>{}));let _=null;const R=h.url?e!==h.url.pathname+h.url.search:!1,A=h.route?a.id!==h.route.id:!1;let I=!1;const g=b.map((E,O)=>{var J;const L=h.branch[O],T=!!(E!=null&&E[0])&&((L==null?void 0:L.loader)!==E[1]||Ue(I,A,R,(J=L.server)==null?void 0:J.uses,r));return T&&(I=!0),T});if(g.some(Boolean)){try{_=await Be(t,g)}catch(E){return ie({status:E instanceof te?E.status:500,error:await Z(E,{url:t,params:r,route:{id:a.id}}),url:t,route:a})}if(_.type==="redirect")return _}const m=_==null?void 0:_.nodes;let P=!1;const $=b.map(async(E,O)=>{var ge;if(!E)return;const L=h.branch[O],T=m==null?void 0:m[O];if((!T||T.type==="skip")&&E[1]===(L==null?void 0:L.loader)&&!Ue(P,A,R,(ge=L.universal)==null?void 0:ge.uses,r))return L;if(P=!0,(T==null?void 0:T.type)==="error")throw T;return de({loader:E[1],url:t,params:r,route:a,parent:async()=>{var Te;const je={};for(let me=0;me{});const C=[];for(let E=0;EPromise.resolve({}),server_data_node:pe(c)}),b={node:await d(),loader:d,universal:null,server:null,data:null};return await W({url:t,params:a,branch:[v,b],status:e,error:i,route:null})}function X(e,i){if(_e(e,K))return;const t=se(e);for(const r of u){const a=r.exec(t);if(a)return{id:e.pathname+e.search,invalidating:i,route:r,params:Qe(a),url:e}}}function se(e){return Ze(e.pathname.slice(K.length)||"/")}function xe({url:e,type:i,intent:t,delta:r}){var v,b;let a=!1;const c={from:{params:h.params,route:{id:((v=h.route)==null?void 0:v.id)??null},url:h.url},to:{params:(t==null?void 0:t.params)??null,route:{id:((b=t==null?void 0:t.route)==null?void 0:b.id)??null},url:e},willUnload:!t,type:i};r!==void 0&&(c.delta=r);const p={...c,cancel:()=>{a=!0}};return U||w.before_navigate.forEach(_=>_(p)),a?null:c}async function ce({url:e,scroll:i,keepfocus:t,redirect_chain:r,details:a,type:c,delta:p,nav_token:v={},accepted:b,blocked:_}){var $,C,E;const R=X(e,!1),A=xe({url:e,type:c,delta:p,intent:R});if(!A){_();return}const I=j;b(),U=!0,x&&M.navigating.set(A),ae=v;let g=R&&await he(R);if(!g){if(_e(e,K))return await G(e);g=await Ne(e,{id:null},await Z(new Error(`Not found: ${e.pathname}`),{url:e,params:{},route:{id:null}}),404)}if(e=(R==null?void 0:R.url)||e,ae!==v)return!1;if(g.type==="redirect")if(r.length>10||r.includes(e.pathname))g=await ie({status:500,error:await Z(new Error("Redirect loop"),{url:e,params:{},route:{id:null}}),url:e,route:{id:null}});else return re(new URL(g.location,e).href,{},[...r,e.pathname],v),!1;else(($=g.props.page)==null?void 0:$.status)>=400&&await M.updated.check()&&await G(e);if(S.length=0,H=!1,N=!0,ve(I),Re(I),(C=g.props.page)!=null&&C.url&&g.props.page.url.pathname!==e.pathname&&(e.pathname=(E=g.props.page)==null?void 0:E.url.pathname),a){const O=a.replaceState?0:1;if(a.state[V]=j+=O,history[a.replaceState?"replaceState":"pushState"](a.state,"",e),!a.replaceState){let L=j+1;for(;Q[L]||z[L];)delete Q[L],delete z[L],L+=1}}y=null,x?(h=g.state,g.props.page&&(g.props.page.url=e),q.$set(g.props)):Oe(g);const{activeElement:m}=document;if(await ye(),k){const O=e.hash&&document.getElementById(decodeURIComponent(e.hash.slice(1)));i?scrollTo(i.x,i.y):O?O.scrollIntoView():scrollTo(0,0)}const P=document.activeElement!==m&&document.activeElement!==document.body;!t&&!P&&Ee(),k=!0,g.props.page&&(F=g.props.page),U=!1,c==="popstate"&&Ae(j),w.after_navigate.forEach(O=>O(A)),M.navigating.set(null),N=!1}async function Ne(e,i,t,r){return e.origin===location.origin&&e.pathname===location.pathname&&!D?await ie({status:r,error:t,url:e,route:i}):await G(e)}function G(e){return location.href=e.href,new Promise(()=>{})}function Ye(){let e;f.addEventListener("mousemove",c=>{const p=c.target;clearTimeout(e),e=setTimeout(()=>{r(p,2)},20)});function i(c){r(c.composedPath()[0],1)}f.addEventListener("mousedown",i),f.addEventListener("touchstart",i,{passive:!0});const t=new IntersectionObserver(c=>{for(const p of c)p.isIntersecting&&(oe(se(new URL(p.target.href))),t.unobserve(p.target))},{threshold:0});function r(c,p){const v=Ve(c,f);if(!v)return;const{url:b,external:_,download:R}=we(v,K);if(_||R)return;const A=le(v);if(!A.reload)if(p<=A.preload_data){const I=X(b,!1);I&&Le(I)}else p<=A.preload_code&&oe(se(b))}function a(){t.disconnect();for(const c of f.querySelectorAll("a")){const{url:p,external:v,download:b}=we(c,K);if(v||b)continue;const _=le(c);_.reload||(_.preload_code===qe.viewport&&t.observe(c),_.preload_code===qe.eager&&oe(se(p)))}}w.after_navigate.push(a),a()}function Z(e,i){return e instanceof te?e.body:n.hooks.handleError({error:e,event:i})??{message:i.route.id!=null?"Internal Error":"Not Found"}}return{after_navigate:e=>{De(()=>(w.after_navigate.push(e),()=>{const i=w.after_navigate.indexOf(e);w.after_navigate.splice(i,1)}))},before_navigate:e=>{De(()=>(w.before_navigate.push(e),()=>{const i=w.before_navigate.indexOf(e);w.before_navigate.splice(i,1)}))},disable_scroll_handling:()=>{(N||!x)&&(k=!1)},goto:(e,i={})=>re(e,i,[]),invalidate:e=>{if(typeof e=="function")S.push(e);else{const{href:i}=new URL(e,location.href);S.push(t=>t.href===i)}return ke()},invalidate_all:()=>(H=!0,ke()),preload_data:async e=>{const i=new URL(e,Ce(document)),t=X(i,!1);if(!t)throw new Error(`Attempted to preload a URL that does not belong to this app: ${i}`);await Le(t)},preload_code:oe,apply_action:async e=>{if(e.type==="error"){const i=new URL(location.href),{branch:t,route:r}=h;if(!r)return;const a=await Pe(h.branch.length,t,r.errors);if(a){const c=await W({url:i,params:h.params,branch:t.slice(0,a.idx).concat(a.node),status:e.status??500,error:e.error,route:r});h=c.state,q.$set(c.props),ye().then(Ee)}}else e.type==="redirect"?re(e.location,{invalidateAll:!0},[]):(q.$set({form:null,page:{...F,form:e.data,status:e.status}}),await ye(),q.$set({form:e.data}),e.type==="success"&&Ee())},_start_router:()=>{var i;history.scrollRestoration="manual",addEventListener("beforeunload",t=>{var a;let r=!1;if(Ie(),!U){const c={from:{params:h.params,route:{id:((a=h.route)==null?void 0:a.id)??null},url:h.url},to:null,willUnload:!0,type:"leave",cancel:()=>r=!0};w.before_navigate.forEach(p=>p(c))}r?(t.preventDefault(),t.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&Ie()}),(i=navigator.connection)!=null&&i.saveData||Ye(),f.addEventListener("click",t=>{var I;if(t.button||t.which!==1||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||t.defaultPrevented)return;const r=Ve(t.composedPath()[0],f);if(!r)return;const{url:a,external:c,target:p,download:v}=we(r,K);if(!a)return;if(p==="_parent"||p==="_top"){if(window.parent!==window)return}else if(p&&p!=="_self")return;const b=le(r);if(!(r instanceof SVGAElement)&&a.protocol!==location.protocol&&!(a.protocol==="https:"||a.protocol==="http:")||v)return;if(c||b.reload){xe({url:a,type:"link"})?U=!0:t.preventDefault();return}const[R,A]=a.href.split("#");if(A!==void 0&&R===location.href.split("#")[0]){if(h.url.hash===a.hash){t.preventDefault(),(I=r.ownerDocument.getElementById(A))==null||I.scrollIntoView();return}if(B=!0,ve(j),e(a),!b.replace_state)return;B=!1,t.preventDefault()}ce({url:a,scroll:b.noscroll?ee():null,keepfocus:b.keep_focus??!1,redirect_chain:[],details:{state:{},replaceState:b.replace_state??a.href===location.href},accepted:()=>t.preventDefault(),blocked:()=>t.preventDefault(),type:"link"})}),f.addEventListener("submit",t=>{if(t.defaultPrevented)return;const r=HTMLFormElement.prototype.cloneNode.call(t.target),a=t.submitter;if(((a==null?void 0:a.formMethod)||r.method)!=="get")return;const p=new URL((a==null?void 0:a.hasAttribute("formaction"))&&(a==null?void 0:a.formAction)||r.action);if(_e(p,K))return;const v=t.target,{keep_focus:b,noscroll:_,reload:R,replace_state:A}=le(v);if(R)return;t.preventDefault(),t.stopPropagation();const I=new FormData(v),g=a==null?void 0:a.getAttribute("name");g&&I.append(g,(a==null?void 0:a.getAttribute("value"))??""),p.search=new URLSearchParams(I).toString(),ce({url:p,scroll:_?ee():null,keepfocus:b??!1,redirect_chain:[],details:{state:{},replaceState:A??p.href===location.href},nav_token:{},accepted:()=>{},blocked:()=>{},type:"form"})}),addEventListener("popstate",async t=>{var r;if((r=t.state)!=null&&r[V]){if(t.state[V]===j)return;const a=z[t.state[V]];if(h.url.href.split("#")[0]===location.href.split("#")[0]){z[j]=ee(),j=t.state[V],scrollTo(a.x,a.y);return}const c=t.state[V]-j;await ce({url:new URL(location.href),scroll:a,keepfocus:!1,redirect_chain:[],details:null,accepted:()=>{j=t.state[V]},blocked:()=>{history.go(-c)},type:"popstate",delta:c})}else if(!B){const a=new URL(location.href);e(a)}}),addEventListener("hashchange",()=>{B&&(B=!1,history.replaceState({...history.state,[V]:++j},"",location.href))});for(const t of document.querySelectorAll("link"))t.rel==="icon"&&(t.href=t.href);addEventListener("pageshow",t=>{t.persisted&&M.navigating.set(null)});function e(t){h.url=t,M.page.set({...F,url:t}),M.page.notify()}},_hydrate:async({status:e=200,error:i,node_ids:t,params:r,route:a,data:c,form:p})=>{D=!0;const v=new URL(location.href);({params:r={},route:a={id:null}}=X(v,!1)||{});let b;try{const _=t.map(async(I,g)=>{const m=c[g];return m!=null&&m.uses&&(m.uses=Ge(m.uses)),de({loader:n.nodes[I],url:v,params:r,route:a,parent:async()=>{const P={};for(let $=0;$I===a.id);if(A){const I=A.layouts;for(let g=0;gd?"1":"0").join(""));const s=await fe(u.href);if(!s.ok)throw new te(s.status,await s.json());return new Promise(async d=>{var h;const f=new Map,S=s.body.getReader(),l=new TextDecoder;function y(D){return bt(D,{Promise:x=>new Promise((k,N)=>{f.set(x,{fulfil:k,reject:N})})})}let w="";for(;;){const{done:D,value:x}=await S.read();if(D&&!w)break;for(w+=!x&&w?` +`:l.decode(x);;){const k=w.indexOf(` +`);if(k===-1)break;const N=JSON.parse(w.slice(0,k));if(w=w.slice(k+1),N.type==="redirect")return d(N);if(N.type==="data")(h=N.nodes)==null||h.forEach(U=>{(U==null?void 0:U.type)==="data"&&(U.uses=Ge(U.uses),U.data=y(U.data))}),d(N);else if(N.type==="chunk"){const{id:U,data:B,error:H}=N,q=f.get(U);f.delete(U),H?q.reject(y(H)):q.fulfil(y(B))}}}})}function Ge(n){return{dependencies:new Set((n==null?void 0:n.dependencies)??[]),params:new Set((n==null?void 0:n.params)??[]),parent:!!(n!=null&&n.parent),route:!!(n!=null&&n.route),url:!!(n!=null&&n.url)}}function Ee(){const n=document.querySelector("[autofocus]");if(n)n.focus();else{const o=document.body,u=o.getAttribute("tabindex");o.tabIndex=-1,o.focus({preventScroll:!0,focusVisible:!1}),u!==null?o.setAttribute("tabindex",u):o.removeAttribute("tabindex");const s=getSelection();if(s&&s.type!=="None"){const d=[];for(let f=0;f{if(s.rangeCount===d.length){for(let f=0;f{"data"in _&&l(0,o=_.data)},[o]}class Be extends ne{constructor(e){super(),oe(this,e,je,Pe,ce,{data:0})}}function Ce(d){let e,l,o,_,v,p,n,h;l=new He({});const k=d[2].default,c=$e(k,d,d[1],null);return n=new Be({props:{data:d[0]}}),{c(){e=i("div"),ke(l.$$.fragment),o=w(),_=i("div"),c&&c.c(),v=w(),p=i("div"),ke(n.$$.fragment),this.h()},l(s){e=f(s,"DIV",{id:!0});var m=u(e);ge(l.$$.fragment,m),o=b(m),_=f(m,"DIV",{class:!0});var I=u(_);c&&c.l(I),I.forEach(r),v=b(m),p=f(m,"DIV",{});var g=u(p);ge(n.$$.fragment,g),g.forEach(r),m.forEach(r),this.h()},h(){a(_,"class","section"),a(e,"id","app")},m(s,m){Z(s,e,m),we(l,e,null),t(e,o),t(e,_),c&&c.m(_,null),t(e,v),t(e,p),we(n,p,null),h=!0},p(s,[m]){c&&c.p&&(!h||m&2)&&Ae(c,k,s,s[1],h?Ie(k,s[1],m,null):De(s[1]),null);const I={};m&1&&(I.data=s[0]),n.$set(I)},i(s){h||(le(l.$$.fragment,s),le(c,s),le(n.$$.fragment,s),h=!0)},o(s){re(l.$$.fragment,s),re(c,s),re(n.$$.fragment,s),h=!1},d(s){s&&r(e),be(l),c&&c.d(s),be(n)}}}function Te(d,e,l){let{$$slots:o={},$$scope:_}=e,{data:v}=e;return d.$$set=p=>{"data"in p&&l(0,v=p.data),"$$scope"in p&&l(1,_=p.$$scope)},[v,_,o]}class Se extends ne{constructor(e){super(),oe(this,e,Te,Ce,ce,{data:0})}}export{Se as component}; diff --git a/_app/immutable/nodes/1.1caf0f88.js b/_app/immutable/nodes/1.1caf0f88.js new file mode 100644 index 00000000..0233a251 --- /dev/null +++ b/_app/immutable/nodes/1.1caf0f88.js @@ -0,0 +1 @@ +import{S,i as q,s as x,k as _,q as f,a as C,l as d,m as g,r as h,h as u,c as k,b as m,C as v,u as E,E as $,K as y}from"../chunks/index.8c1dbec0.js";import{d as H}from"../chunks/singletons.0b9e2925.js";const K=()=>{const s=H;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},P={subscribe(s){return K().page.subscribe(s)}};function j(s){var b;let t,r=s[0].status+"",o,n,i,c=((b=s[0].error)==null?void 0:b.message)+"",l;return{c(){t=_("h1"),o=f(r),n=C(),i=_("p"),l=f(c)},l(e){t=d(e,"H1",{});var a=g(t);o=h(a,r),a.forEach(u),n=k(e),i=d(e,"P",{});var p=g(i);l=h(p,c),p.forEach(u)},m(e,a){m(e,t,a),v(t,o),m(e,n,a),m(e,i,a),v(i,l)},p(e,[a]){var p;a&1&&r!==(r=e[0].status+"")&&E(o,r),a&1&&c!==(c=((p=e[0].error)==null?void 0:p.message)+"")&&E(l,c)},i:$,o:$,d(e){e&&u(t),e&&u(n),e&&u(i)}}}function w(s,t,r){let o;return y(s,P,n=>r(0,o=n)),[o]}let B=class extends S{constructor(t){super(),q(this,t,w,j,x,{})}};export{B as component}; diff --git a/_app/immutable/nodes/10.22593ef1.js b/_app/immutable/nodes/10.22593ef1.js new file mode 100644 index 00000000..f95f4e30 --- /dev/null +++ b/_app/immutable/nodes/10.22593ef1.js @@ -0,0 +1,3 @@ +import{S as H,i as L,s as R,a as P,k as $,q as x,M as S,h as m,c as k,l as y,m as E,r as A,n as b,b as w,C as f,g as j,f as T,d as C,L as U,y as z,z as M,A as F,u as J,B as K,v as N}from"../chunks/index.8c1dbec0.js";import{P as O}from"../chunks/PostCardList.ba348fd5.js";function q(d,o,a){const l=d.slice();return l[1]=o[a],l}function B(d){let o,a,l=d[1].section+"",i,v,u,p,_;return u=new O({props:{posts:d[1].values}}),{c(){o=$("div"),a=$("h2"),i=x(l),v=P(),z(u.$$.fragment),p=P(),this.h()},l(n){o=y(n,"DIV",{class:!0});var r=E(o);a=y(r,"H2",{});var h=E(a);i=A(h,l),h.forEach(m),v=k(r),M(u.$$.fragment,r),p=k(r),r.forEach(m),this.h()},h(){b(o,"class","section content")},m(n,r){w(n,o,r),f(o,a),f(a,i),f(o,v),F(u,o,null),f(o,p),_=!0},p(n,r){(!_||r&1)&&l!==(l=n[1].section+"")&&J(i,l);const h={};r&1&&(h.posts=n[1].values),u.$set(h)},i(n){_||(j(u.$$.fragment,n),_=!0)},o(n){C(u.$$.fragment,n),_=!1},d(n){n&&m(o),K(u)}}}function Q(d){let o,a,l,i,v,u,p,_,n,r,h=d[0].sections,s=[];for(let e=0;eC(s[e],1,1,()=>{s[e]=null});return{c(){o=P(),a=$("div"),l=$("div"),i=$("h1"),v=x("Projects, Utility Apps & other Resources⚙️"),u=P(),p=$("p"),_=x(`This is a list of some of my current or past projects, as well as some + useful resources that I compiled. Go check them out!`),n=P();for(let e=0;e{"data"in i&&a(0,l=i.data)},[l]}class Z extends H{constructor(o){super(),L(this,o,W,Q,R,{data:0})}}export{Z as component}; diff --git a/_app/immutable/nodes/11.0166947d.js b/_app/immutable/nodes/11.0166947d.js new file mode 100644 index 00000000..0d169d82 --- /dev/null +++ b/_app/immutable/nodes/11.0166947d.js @@ -0,0 +1 @@ +import{S as s,i,s as m,y as c,z as u,A as f,g as p,d,B as _}from"../chunks/index.8c1dbec0.js";import{M as l}from"../chunks/MarkdownSite.7d9e3e05.js";function $(o){let e,a;return e=new l({props:{site:o[0].project,about:o[0].about}}),{c(){c(e.$$.fragment)},l(t){u(e.$$.fragment,t)},m(t,n){f(e,t,n),a=!0},p(t,[n]){const r={};n&1&&(r.site=t[0].project),n&1&&(r.about=t[0].about),e.$set(r)},i(t){a||(p(e.$$.fragment,t),a=!0)},o(t){d(e.$$.fragment,t),a=!1},d(t){_(e,t)}}}function g(o,e,a){let{data:t}=e;return o.$$set=n=>{"data"in n&&a(0,t=n.data)},[t]}class k extends s{constructor(e){super(),i(this,e,g,$,m,{data:0})}}export{k as component}; diff --git a/_app/immutable/nodes/12.df862236.js b/_app/immutable/nodes/12.df862236.js new file mode 100644 index 00000000..51107d4a --- /dev/null +++ b/_app/immutable/nodes/12.df862236.js @@ -0,0 +1 @@ +import{S as $,i as j,s as z,k as g,q as C,a as k,l as m,m as v,r as S,h as d,c as y,n as p,b as I,C as u,E as D,L as A,u as P,R,e as H,T as U}from"../chunks/index.8c1dbec0.js";function L(c,e,l){const s=c.slice();return s[1]=e[l],s}function V(c){let e,l=c[1].abstract+"",s;return{c(){e=new R(!1),s=H(),this.h()},l(n){e=U(n,!1),s=H(),this.h()},h(){e.a=s},m(n,h){e.m(l,n,h),I(n,s,h)},p(n,h){h&1&&l!==(l=n[1].abstract+"")&&e.p(l)},d(n){n&&d(s),n&&e.d()}}}function q(c){let e,l,s=c[1].tag+"",n,h,_,f,i=c[1].abstract&&V(c);return{c(){e=g("li"),l=g("a"),n=C(s),_=k(),i&&i.c(),f=k(),this.h()},l(t){e=m(t,"LI",{class:!0});var a=v(e);l=m(a,"A",{href:!0});var o=v(l);n=S(o,s),o.forEach(d),_=y(a),i&&i.l(a),f=y(a),a.forEach(d),this.h()},h(){p(l,"href",h=c[1].slug||`/tags/${c[1].tag}`),p(e,"class","tag-item svelte-zwjyck")},m(t,a){I(t,e,a),u(e,l),u(l,n),u(e,_),i&&i.m(e,null),u(e,f)},p(t,a){a&1&&s!==(s=t[1].tag+"")&&P(n,s),a&1&&h!==(h=t[1].slug||`/tags/${t[1].tag}`)&&p(l,"href",h),t[1].abstract?i?i.p(t,a):(i=V(t),i.c(),i.m(e,f)):i&&(i.d(1),i=null)},d(t){t&&d(e),i&&i.d()}}}function B(c){let e,l,s,n,h,_,f,i=c[0].tags,t=[];for(let a=0;a{"data"in n&&l(0,s=n.data)},[s]}class J extends ${constructor(e){super(),j(this,e,F,B,z,{data:0})}}export{J as component}; diff --git a/_app/immutable/nodes/13.d68a3a4e.js b/_app/immutable/nodes/13.d68a3a4e.js new file mode 100644 index 00000000..7574e0e9 --- /dev/null +++ b/_app/immutable/nodes/13.d68a3a4e.js @@ -0,0 +1 @@ +import{S as P,i as q,s as w,a as I,k as p,q as H,y as S,M as j,h as r,c as V,l as v,m as g,r as L,z,n as T,b as k,C as m,A,u as F,g as G,d as J,B as K}from"../chunks/index.8c1dbec0.js";import{P as N}from"../chunks/PostCardList.ba348fd5.js";function M(l){let a,i=l[0].details.html+"";return{c(){a=p("div"),this.h()},l(t){a=v(t,"DIV",{class:!0});var n=g(a);n.forEach(r),this.h()},h(){T(a,"class","tag-content section svelte-2aohp6")},m(t,n){k(t,a,n),a.innerHTML=i},p(t,n){n&1&&i!==(i=t[0].details.html+"")&&(a.innerHTML=i)},d(t){t&&r(a)}}}function O(l){let a,i,t,n,f,b,u=l[0].details.tag+"",$,y,E,h,c,_;document.title=a="🏷️"+l[0].details.tag+" - Tim Bachmann";let s=l[0].details.html&&M(l);return c=new N({props:{posts:l[0].posts}}),{c(){i=I(),t=p("div"),n=p("div"),f=p("h1"),b=H("🏷️ Tag: "),$=H(u),y=I(),s&&s.c(),E=I(),h=p("div"),S(c.$$.fragment),this.h()},l(e){j("svelte-1yrcbj8",document.head).forEach(r),i=V(e),t=v(e,"DIV",{class:!0});var o=g(t);n=v(o,"DIV",{class:!0});var B=g(n);f=v(B,"H1",{});var D=g(f);b=L(D,"🏷️ Tag: "),$=L(D,u),D.forEach(r),B.forEach(r),y=V(o),s&&s.l(o),E=V(o),h=v(o,"DIV",{class:!0});var C=g(h);z(c.$$.fragment,C),C.forEach(r),o.forEach(r),this.h()},h(){T(n,"class","section"),T(h,"class","section"),T(t,"class","container has-text-centered")},m(e,d){k(e,i,d),k(e,t,d),m(t,n),m(n,f),m(f,b),m(f,$),m(t,y),s&&s.m(t,null),m(t,E),m(t,h),A(c,h,null),_=!0},p(e,[d]){(!_||d&1)&&a!==(a="🏷️"+e[0].details.tag+" - Tim Bachmann")&&(document.title=a),(!_||d&1)&&u!==(u=e[0].details.tag+"")&&F($,u),e[0].details.html?s?s.p(e,d):(s=M(e),s.c(),s.m(t,E)):s&&(s.d(1),s=null);const o={};d&1&&(o.posts=e[0].posts),c.$set(o)},i(e){_||(G(c.$$.fragment,e),_=!0)},o(e){J(c.$$.fragment,e),_=!1},d(e){e&&r(i),e&&r(t),s&&s.d(),K(c)}}}function Q(l,a,i){let{data:t}=a;return l.$$set=n=>{"data"in n&&i(0,t=n.data)},[t]}class W extends P{constructor(a){super(),q(this,a,Q,O,w,{data:0})}}export{W as component}; diff --git a/_app/immutable/nodes/2.5e216872.js b/_app/immutable/nodes/2.5e216872.js new file mode 100644 index 00000000..69d99e51 --- /dev/null +++ b/_app/immutable/nodes/2.5e216872.js @@ -0,0 +1,9 @@ +import{S as et,i as tt,s as at,k as c,q as P,a as V,l as f,m as u,r as D,h as l,c as I,n as s,b as ve,C as e,g as Q,v as st,f as rt,d as se,L as nt,y as je,z as Ne,A as He,E as it,B as Le,M as ot,N as ct,u as T}from"../chunks/index.8c1dbec0.js";import{s as Ye}from"../chunks/socialmedia.caeda4dc.js";import{F as ft}from"../chunks/fa.9f83fec8.js";import{P as ut}from"../chunks/PostCardList.ba348fd5.js";function Ze(d,r,o){const n=d.slice();return n[0]=r[o],n}function xe(d){let r,o,n,_,g=d[0].site+"",E,$,a;return n=new ft({props:{icon:d[0].icon,class:"icon is-small"}}),{c(){r=c("div"),o=c("a"),je(n.$$.fragment),_=V(),E=P(g),$=V(),this.h()},l(p){r=f(p,"DIV",{class:!0});var t=u(r);o=f(t,"A",{target:!0,rel:!0,href:!0});var i=u(o);Ne(n.$$.fragment,i),_=I(i),E=D(i,g),i.forEach(l),$=I(t),t.forEach(l),this.h()},h(){s(o,"target","_blank"),s(o,"rel","me"),s(o,"href",d[0].link),s(r,"class","column svelte-l4ofsj")},m(p,t){ve(p,r,t),e(r,o),He(n,o,null),e(o,_),e(o,E),e(r,$),a=!0},p:it,i(p){a||(Q(n.$$.fragment,p),a=!0)},o(p){se(n.$$.fragment,p),a=!1},d(p){p&&l(r),Le(n)}}}function mt(d){let r,o,n,_,g,E,$=Ye,a=[];for(let t=0;t<$.length;t+=1)a[t]=xe(Ze(d,$,t));const p=t=>se(a[t],1,1,()=>{a[t]=null});return{c(){r=c("div"),o=c("h2"),n=P("Social Media"),_=V(),g=c("div");for(let t=0;t{"data"in _&&o(0,n=_.data)},[n]}class At extends et{constructor(r){super(),tt(this,r,gt,pt,at,{data:0})}}export{At as component}; diff --git a/_app/immutable/nodes/3.035a3a74.js b/_app/immutable/nodes/3.035a3a74.js new file mode 100644 index 00000000..8c6ec534 --- /dev/null +++ b/_app/immutable/nodes/3.035a3a74.js @@ -0,0 +1,3 @@ +import{S as T,i as j,s as G,a as A,k as h,q as C,y as w,M as J,h as n,c as D,l as p,m as u,r as I,z,n as v,b as F,C as s,A as R,g as H,d as L,B as M}from"../chunks/index.8c1dbec0.js";import{F as K}from"../chunks/fa.9f83fec8.js";import{P as N}from"../chunks/PostCardList.ba348fd5.js";function O(g){let r,e,a,o,S,E,d,x,i,c,y,B,f,l,$;return c=new K({props:{icon:["fa","rss"],class:"icon is-small"}}),l=new N({props:{posts:g[0].posts}}),{c(){r=A(),e=h("div"),a=h("div"),o=h("h1"),S=C("Blog 📖"),E=A(),d=h("p"),x=C(`Subscribe to my blog via + `),i=h("a"),w(c.$$.fragment),y=C(" RSS"),B=A(),f=h("div"),w(l.$$.fragment),this.h()},l(t){J("svelte-1fqlmzu",document.head).forEach(n),r=D(t),e=p(t,"DIV",{class:!0});var m=u(e);a=p(m,"DIV",{class:!0});var b=u(a);o=p(b,"H1",{});var V=u(o);S=I(V,"Blog 📖"),V.forEach(n),E=D(b),d=p(b,"P",{});var P=u(d);x=I(P,`Subscribe to my blog via + `),i=p(P,"A",{href:!0,target:!0});var q=u(i);z(c.$$.fragment,q),y=I(q," RSS"),q.forEach(n),P.forEach(n),b.forEach(n),B=D(m),f=p(m,"DIV",{class:!0});var k=u(f);z(l.$$.fragment,k),k.forEach(n),m.forEach(n),this.h()},h(){document.title="Blog - Tim Bachmann",v(i,"href","blog/rss.xml"),v(i,"target","_blank"),v(a,"class","section"),v(f,"class","section"),v(e,"class","container has-text-centered")},m(t,_){F(t,r,_),F(t,e,_),s(e,a),s(a,o),s(o,S),s(a,E),s(a,d),s(d,x),s(d,i),R(c,i,null),s(i,y),s(e,B),s(e,f),R(l,f,null),$=!0},p(t,[_]){const m={};_&1&&(m.posts=t[0].posts),l.$set(m)},i(t){$||(H(c.$$.fragment,t),H(l.$$.fragment,t),$=!0)},o(t){L(c.$$.fragment,t),L(l.$$.fragment,t),$=!1},d(t){t&&n(r),t&&n(e),M(c),M(l)}}}function Q(g,r,e){let{data:a}=r;return g.$$set=o=>{"data"in o&&e(0,a=o.data)},[a]}class Y extends T{constructor(r){super(),j(this,r,Q,O,G,{data:0})}}export{Y as component}; diff --git a/_app/immutable/nodes/4.07141e4d.js b/_app/immutable/nodes/4.07141e4d.js new file mode 100644 index 00000000..ffdecfba --- /dev/null +++ b/_app/immutable/nodes/4.07141e4d.js @@ -0,0 +1,3 @@ +import{S as v,i as b,s as E,y as k,z as q,A as w,g as y,d as M,B as S,k as p,q as f,l as _,m as h,r as d,h as m,n as $,b as A,C as l,E as C}from"../chunks/index.8c1dbec0.js";import{M as O}from"../chunks/MarkdownSite.7d9e3e05.js";function T(n){let e,s,t,o,a,r;return{c(){e=p("div"),s=p("em"),t=f(`You found an error in this post? Open a + `),o=p("a"),a=f("pull request"),r=f("."),this.h()},l(i){e=_(i,"DIV",{class:!0,slot:!0});var u=h(e);s=_(u,"EM",{});var c=h(s);t=d(c,`You found an error in this post? Open a + `),o=_(c,"A",{href:!0});var g=h(o);a=d(g,"pull request"),g.forEach(m),r=d(c,"."),c.forEach(m),u.forEach(m),this.h()},h(){$(o,"href",n[1]),$(e,"class","outro svelte-1ysgxol"),$(e,"slot","outro")},m(i,u){A(i,e,u),l(e,s),l(s,t),l(s,o),l(o,a),l(s,r)},p:C,d(i){i&&m(e)}}}function Y(n){let e,s;return e=new O({props:{site:n[0].post,about:n[0].about,$$slots:{outro:[T]},$$scope:{ctx:n}}}),{c(){k(e.$$.fragment)},l(t){q(e.$$.fragment,t)},m(t,o){w(e,t,o),s=!0},p(t,[o]){const a={};o&1&&(a.site=t[0].post),o&1&&(a.about=t[0].about),o&4&&(a.$$scope={dirty:o,ctx:t}),e.$set(a)},i(t){s||(y(e.$$.fragment,t),s=!0)},o(t){M(e.$$.fragment,t),s=!1},d(t){S(e,t)}}}function x(n,e,s){var a;let{data:t}=e;const o=`https://github.com/Tiim/Tiim.github.io/tree/source/content/${(a=t==null?void 0:t.post)==null?void 0:a.slug}.md`;return n.$$set=r=>{"data"in r&&s(0,t=r.data)},[t,o]}class D extends v{constructor(e){super(),b(this,e,x,Y,E,{data:0})}}export{D as component}; diff --git a/_app/immutable/nodes/5.a52c5d6a.js b/_app/immutable/nodes/5.a52c5d6a.js new file mode 100644 index 00000000..3ea797b2 --- /dev/null +++ b/_app/immutable/nodes/5.a52c5d6a.js @@ -0,0 +1 @@ +import{S as F,i as T,s as z,k as $,q as I,a as S,l as k,m as E,r as P,h as v,c as C,n as d,b as V,C as p,g as D,f as H,d as N,L,y as M,z as j,A as G,u as B,B as J,v as K,E as O}from"../chunks/index.8c1dbec0.js";import{s as Q}from"../chunks/socialmedia.caeda4dc.js";import{F as R}from"../chunks/fa.9f83fec8.js";function q(u,e,r){const n=u.slice();return n[1]=e[r],n}function U(u){let e,r=".",n;return{c(){e=$("span"),n=I(r),this.h()},l(o){e=k(o,"SPAN",{class:!0});var s=E(e);n=P(s,r),s.forEach(v),this.h()},h(){d(e,"class","handle svelte-1s7r06i")},m(o,s){V(o,e,s),p(e,n)},p:O,d(o){o&&v(e)}}}function W(u){let e,r=u[1].handle+"",n;return{c(){e=$("span"),n=I(r),this.h()},l(o){e=k(o,"SPAN",{class:!0});var s=E(e);n=P(s,r),s.forEach(v),this.h()},h(){d(e,"class","handle svelte-1s7r06i")},m(o,s){V(o,e,s),p(e,n)},p(o,s){s&1&&r!==(r=o[1].handle+"")&&B(n,r)},d(o){o&&v(e)}}}function w(u){let e,r,n,o,s,_=u[1].site+"",g,m,l,b,t;n=new R({props:{icon:u[1].icon}});function c(i,f){return i[1].handle?W:U}let a=c(u),h=a(u);return{c(){e=$("a"),r=$("span"),M(n.$$.fragment),o=S(),s=$("div"),g=I(_),m=S(),h.c(),l=S(),this.h()},l(i){e=k(i,"A",{target:!0,class:!0,rel:!0,href:!0});var f=E(e);r=k(f,"SPAN",{class:!0});var y=E(r);j(n.$$.fragment,y),y.forEach(v),o=C(f),s=k(f,"DIV",{class:!0});var A=E(s);g=P(A,_),m=C(A),h.l(A),A.forEach(v),l=C(f),f.forEach(v),this.h()},h(){d(r,"class","ico svelte-1s7r06i"),d(s,"class","text svelte-1s7r06i"),d(e,"target","_blank"),d(e,"class","item svelte-1s7r06i"),d(e,"rel","me"),d(e,"href",b=u[1].link)},m(i,f){V(i,e,f),p(e,r),G(n,r,null),p(e,o),p(e,s),p(s,g),p(s,m),h.m(s,null),p(e,l),t=!0},p(i,f){const y={};f&1&&(y.icon=i[1].icon),n.$set(y),(!t||f&1)&&_!==(_=i[1].site+"")&&B(g,_),a===(a=c(i))&&h?h.p(i,f):(h.d(1),h=a(i),h&&(h.c(),h.m(s,null))),(!t||f&1&&b!==(b=i[1].link))&&d(e,"href",b)},i(i){t||(D(n.$$.fragment,i),t=!0)},o(i){N(n.$$.fragment,i),t=!1},d(i){i&&v(e),J(n),h.d()}}}function X(u){let e,r,n,o,s,_,g,m=u[0],l=[];for(let t=0;tN(l[t],1,1,()=>{l[t]=null});return{c(){e=$("div"),r=$("h1"),n=I("Contact Tiim"),o=S(),s=$("div"),_=$("div");for(let t=0;to.type.includes("contact"))),[n]}class te extends F{constructor(e){super(),T(this,e,Y,X,z,{})}}export{te as component}; diff --git a/_app/immutable/nodes/6.deabb5c6.js b/_app/immutable/nodes/6.deabb5c6.js new file mode 100644 index 00000000..402df581 --- /dev/null +++ b/_app/immutable/nodes/6.deabb5c6.js @@ -0,0 +1 @@ +import{S as B,i as C,s as T,k,q as D,a as A,l as $,m as E,r as F,h as d,c as w,n as p,b as I,C as v,g as S,f as z,d as P,L as H,y as L,z as M,A as j,u as q,B as G,v as J,E as K}from"../chunks/index.8c1dbec0.js";import{s as O}from"../chunks/socialmedia.caeda4dc.js";import{F as Q}from"../chunks/fa.9f83fec8.js";function V(u,e,r){const n=u.slice();return n[1]=e[r],n}function R(u){let e,r=".",n;return{c(){e=k("span"),n=D(r),this.h()},l(o){e=$(o,"SPAN",{class:!0});var l=E(e);n=F(l,r),l.forEach(d),this.h()},h(){p(e,"class","handle svelte-1kpxhvr")},m(o,l){I(o,e,l),v(e,n)},p:K,d(o){o&&d(e)}}}function U(u){let e,r=u[1].handle+"",n;return{c(){e=k("span"),n=D(r),this.h()},l(o){e=$(o,"SPAN",{class:!0});var l=E(e);n=F(l,r),l.forEach(d),this.h()},h(){p(e,"class","handle svelte-1kpxhvr")},m(o,l){I(o,e,l),v(e,n)},p(o,l){l&1&&r!==(r=o[1].handle+"")&&q(n,r)},d(o){o&&d(e)}}}function N(u){let e,r,n,o,l,_=u[1].site+"",g,m,s,b,t;n=new Q({props:{icon:u[1].icon}});function i(c,f){return c[1].handle?U:R}let a=i(u),h=a(u);return{c(){e=k("a"),r=k("span"),L(n.$$.fragment),o=A(),l=k("div"),g=D(_),m=A(),h.c(),s=A(),this.h()},l(c){e=$(c,"A",{target:!0,class:!0,rel:!0,href:!0});var f=E(e);r=$(f,"SPAN",{class:!0});var y=E(r);M(n.$$.fragment,y),y.forEach(d),o=w(f),l=$(f,"DIV",{class:!0});var x=E(l);g=F(x,_),m=w(x),h.l(x),x.forEach(d),s=w(f),f.forEach(d),this.h()},h(){p(r,"class","ico svelte-1kpxhvr"),p(l,"class","text svelte-1kpxhvr"),p(e,"target","_blank"),p(e,"class","item svelte-1kpxhvr"),p(e,"rel","me"),p(e,"href",b=u[1].link)},m(c,f){I(c,e,f),v(e,r),j(n,r,null),v(e,o),v(e,l),v(l,g),v(l,m),h.m(l,null),v(e,s),t=!0},p(c,f){const y={};f&1&&(y.icon=c[1].icon),n.$set(y),(!t||f&1)&&_!==(_=c[1].site+"")&&q(g,_),a===(a=i(c))&&h?h.p(c,f):(h.d(1),h=a(c),h&&(h.c(),h.m(l,null))),(!t||f&1&&b!==(b=c[1].link))&&p(e,"href",b)},i(c){t||(S(n.$$.fragment,c),t=!0)},o(c){P(n.$$.fragment,c),t=!1},d(c){c&&d(e),G(n),h.d()}}}function W(u){let e,r,n,o,l,_,g,m=u[0],s=[];for(let t=0;tP(s[t],1,1,()=>{s[t]=null});return{c(){e=k("div"),r=k("h1"),n=D("Follow Tiim"),o=A(),l=k("div"),_=k("div");for(let t=0;to.type.includes("follow"))),[n]}class te extends B{constructor(e){super(),C(this,e,X,W,T,{})}}export{te as component}; diff --git a/_app/immutable/nodes/7.5881b71c.js b/_app/immutable/nodes/7.5881b71c.js new file mode 100644 index 00000000..1276b454 --- /dev/null +++ b/_app/immutable/nodes/7.5881b71c.js @@ -0,0 +1 @@ +import{S as x,i as y,s as E,k as f,q as C,a as D,y as I,l as p,m as _,r as P,h as l,c as V,z as q,n as v,b as N,C as d,A as S,g as b,d as k,B as w}from"../chunks/index.8c1dbec0.js";import{P as z}from"../chunks/PostCardList.ba348fd5.js";function A(m){let t,a,s,r,u,i,n,h;return n=new z({props:{posts:m[0].notes}}),{c(){t=f("div"),a=f("div"),s=f("h1"),r=C("Notes 📒"),u=D(),i=f("div"),I(n.$$.fragment),this.h()},l(e){t=p(e,"DIV",{class:!0});var o=_(t);a=p(o,"DIV",{class:!0});var c=_(a);s=p(c,"H1",{});var $=_(s);r=P($,"Notes 📒"),$.forEach(l),c.forEach(l),u=V(o),i=p(o,"DIV",{class:!0});var g=_(i);q(n.$$.fragment,g),g.forEach(l),o.forEach(l),this.h()},h(){v(a,"class","section"),v(i,"class","section"),v(t,"class","container has-text-centered")},m(e,o){N(e,t,o),d(t,a),d(a,s),d(s,r),d(t,u),d(t,i),S(n,i,null),h=!0},p(e,[o]){const c={};o&1&&(c.posts=e[0].notes),n.$set(c)},i(e){h||(b(n.$$.fragment,e),h=!0)},o(e){k(n.$$.fragment,e),h=!1},d(e){e&&l(t),w(n)}}}function B(m,t,a){let{data:s}=t;return m.$$set=r=>{"data"in r&&a(0,s=r.data)},[s]}class j extends x{constructor(t){super(),y(this,t,B,A,E,{data:0})}}export{j as component}; diff --git a/_app/immutable/nodes/8.a55c2225.js b/_app/immutable/nodes/8.a55c2225.js new file mode 100644 index 00000000..e320f04e --- /dev/null +++ b/_app/immutable/nodes/8.a55c2225.js @@ -0,0 +1 @@ +import{S as r,i,s as m,y as u,z as c,A as f,g as p,d,B as _}from"../chunks/index.8c1dbec0.js";import{M as l}from"../chunks/MarkdownSite.7d9e3e05.js";function $(o){let e,a;return e=new l({props:{site:o[0].post,about:o[0].about}}),{c(){u(e.$$.fragment)},l(t){c(e.$$.fragment,t)},m(t,n){f(e,t,n),a=!0},p(t,[n]){const s={};n&1&&(s.site=t[0].post),n&1&&(s.about=t[0].about),e.$set(s)},i(t){a||(p(e.$$.fragment,t),a=!0)},o(t){d(e.$$.fragment,t),a=!1},d(t){_(e,t)}}}function g(o,e,a){let{data:t}=e;return o.$$set=n=>{"data"in n&&a(0,t=n.data)},[t]}class k extends r{constructor(e){super(),i(this,e,g,$,m,{data:0})}}export{k as component}; diff --git a/_app/immutable/nodes/9.ae3d0625.js b/_app/immutable/nodes/9.ae3d0625.js new file mode 100644 index 00000000..d018ff47 --- /dev/null +++ b/_app/immutable/nodes/9.ae3d0625.js @@ -0,0 +1 @@ +import{S as r,i,s as m,y as u,z as c,A as f,g as p,d,B as g}from"../chunks/index.8c1dbec0.js";import{M as _}from"../chunks/MarkdownSite.7d9e3e05.js";function l(o){let e,n;return e=new _({props:{site:o[0].page,about:o[0].about}}),{c(){u(e.$$.fragment)},l(t){c(e.$$.fragment,t)},m(t,a){f(e,t,a),n=!0},p(t,[a]){const s={};a&1&&(s.site=t[0].page),a&1&&(s.about=t[0].about),e.$set(s)},i(t){n||(p(e.$$.fragment,t),n=!0)},o(t){d(e.$$.fragment,t),n=!1},d(t){g(e,t)}}}function $(o,e,n){let{data:t}=e;return o.$$set=a=>{"data"in a&&n(0,t=a.data)},[t]}class k extends r{constructor(e){super(),i(this,e,$,l,m,{data:0})}}export{k as component}; diff --git a/_app/version.json b/_app/version.json new file mode 100644 index 00000000..f151bc18 --- /dev/null +++ b/_app/version.json @@ -0,0 +1 @@ +{"version":"1695246146286"} \ No newline at end of file diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/assets/2019-05-vue-on-github-pages.png b/assets/2019-05-vue-on-github-pages.png new file mode 100644 index 00000000..e831b4af Binary files /dev/null and b/assets/2019-05-vue-on-github-pages.png differ diff --git a/assets/2019-07-sql-optional-filters-coalesce.png b/assets/2019-07-sql-optional-filters-coalesce.png new file mode 100644 index 00000000..b15dd8da Binary files /dev/null and b/assets/2019-07-sql-optional-filters-coalesce.png differ diff --git a/assets/2021-01-20-Thesis.pdf b/assets/2021-01-20-Thesis.pdf new file mode 100644 index 00000000..14eac4be Binary files /dev/null and b/assets/2021-01-20-Thesis.pdf differ diff --git a/assets/2021-01-git-operations-as-planning-problems.png b/assets/2021-01-git-operations-as-planning-problems.png new file mode 100644 index 00000000..31d29b30 Binary files /dev/null and b/assets/2021-01-git-operations-as-planning-problems.png differ diff --git a/assets/2021-02-hasura-array.png b/assets/2021-02-hasura-array.png new file mode 100644 index 00000000..7d48b30c Binary files /dev/null and b/assets/2021-02-hasura-array.png differ diff --git a/assets/2022-02-phone-audio-to-pc.jpg b/assets/2022-02-phone-audio-to-pc.jpg new file mode 100644 index 00000000..d7c847a2 Binary files /dev/null and b/assets/2022-02-phone-audio-to-pc.jpg differ diff --git a/assets/2022-03-ssh-windows-wsl.png b/assets/2022-03-ssh-windows-wsl.png new file mode 100644 index 00000000..532d83ab Binary files /dev/null and b/assets/2022-03-ssh-windows-wsl.png differ diff --git a/assets/2022-07-first-go-project-commenting-api.png b/assets/2022-07-first-go-project-commenting-api.png new file mode 100644 index 00000000..e0cc5f56 Binary files /dev/null and b/assets/2022-07-first-go-project-commenting-api.png differ diff --git a/assets/aqualetics-coach-screenshot.png b/assets/aqualetics-coach-screenshot.png new file mode 100644 index 00000000..48137922 Binary files /dev/null and b/assets/aqualetics-coach-screenshot.png differ diff --git a/assets/lenex-splits-sheet-creator.png b/assets/lenex-splits-sheet-creator.png new file mode 100644 index 00000000..ff18be20 Binary files /dev/null and b/assets/lenex-splits-sheet-creator.png differ diff --git a/blog.html b/blog.html new file mode 100644 index 00000000..4f02bed6 --- /dev/null +++ b/blog.html @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Blog - Tim Bachmann + + +
+ + +
+
+ +

Blog 📖

+

Subscribe to my blog via + + RSS +

+

Getting the Absolute Path of a Remote Directory in Ansible

+ 9/20/2023 +
Getting the Absolute Path of a Remote Directory in Ansible +

There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.

+
+
+ +

Automated Planning using Property-Directed Reachability with Seed Heuristics

+ 5/6/2023 +
Automated Planning using Property-Directed Reachability with Seed Heuristics +

Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.

+
+
+ +

Weechat Notifications with ntfy.sh

+ 3/28/2023 +
Weechat Notifications with ntfy.sh +

Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.

+
+
+ +

Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN

+ 3/15/2023 +
Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN +

I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.

+
+
+ +

"no such file or directory" after enabling CGO in Docker

+ 1/24/2023 +
+

Quick fix for the "no such file or directory" error after enabling CGO, when running in a scratch docker image.

+
+
+ +

SvelteKit Server-Side Rendering (SSR) with @urql/svelte

+ 9/26/2022 +
SvelteKit Server-Side Rendering (SSR) with @urql/svelte +

Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.

+
+
+ +

First Go Project: A Jam-stack Commenting API

+ 7/12/2022 +
First Go Project: A Jam-stack Commenting API +

I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!

+
+
+ +

You should be using RSS

+ 6/5/2022 +
You should be using RSS +

Decide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read.

+
+
+ +

How to set up an SSH Server on Windows with WSL

+ 3/2/2022 +
How to set up an SSH Server on Windows with WSL +

It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.

+
+
+ +

Modelling Git Operations as Planning Problems

+ 1/20/2021 +
Modelling Git Operations as Planning Problems +

Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.

+
+
+ +
+ +
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2019-05-vue-on-github-pages.html b/blog/2019-05-vue-on-github-pages.html new file mode 100644 index 00000000..051312d8 --- /dev/null +++ b/blog/2019-05-vue-on-github-pages.html @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + How I use Vue.js on GitHub Pages - Tim Bachmann + + +
+ + +
+
+ +
How I use Vue.js on GitHub Pages +
+ +

How I use Vue.js on GitHub Pages

+ + +

dev + github-pages + javascript + vue.js +

+ +
by
+ published on +
+ + +

I recently read the Article Serving Vue.js apps on GitHub Pages and it inspired me to write about what I'm doing differently.

+

If you want to see an example of this method in action, go check out my personal website on GitHub

+

I won't be explaining how to setup a Vue project. If you're looking for a Tutorial on that go check out the awesome Vue.js Guide.

+

So you have setup your awesome Vue project and want to host it on GitHub Pages. The way Muhammad explained it you would build the project using npm run build, commit the dist/ folder along with your source files and point GitHub to the dist folder. This might get quite messy because you either have commit messages with the sole purpose of uploading the dist folder or you commit the code changes at the same time which makes it hard to find the relevant changes if you ever want to look at your commits again.

+

So what can you do about this?

+

Git to the rescue, let's use a branch that contains all the build files.

+

Step 1 - keeping our working branch clean 🛀

+

To make sure that the branch we are working from stays clean of any build files we are gonna add a .gitignore file to the root.

+
# .gitignore
+dist/
+
+

Step 2 - adding a second branch 🌳

+

We are not goint to branch off master like how we would do it if we were to modify our code with the intention to merge it back to the main branch. Instead we are gonna create a squeaky clean new branch that will only ever hold the dist files. After all we will not ever need to merge these two branches together.

+

We do this by creating a new git repository inside the dist folder:

+
cd dist/
+git init
+git add .
+git commit -m 'Deploying my awesome vue app'
+
+

Step 3 - deploying 🚚

+

We are gonna force push our new git repository to a branch on GitHub. This might go against git best practices but since we won't ever checkout this branch we don't have to worry about that.

+
git push -f git@github.com:<username>/<repo>.git <branch>
+
+

⚠️ Make sure you double or tripple check your destination branch! You don't want to accidentally overwrite your working branch. Using the branch gh-pages will most likely be a good idea.

+

Step 4 - pointing GitHub to the right place 👈

+

Now we are almost done. The only thing left is telling GitHub where our assets live.

+

Go to your repo, on the top right navigate to Settings and scroll down to GitHub pages. Enable it and set your source branch to the branch you force pushed to, for example gh-pages.

+

Step 5 - automating everything 😴

+

If you don't mind doing this whole process (Step 2 and 3) every time you want to deploy you can stop now. If you're as lazy as me, here is the script I use to deploy with one command:

+
# deploy.sh
+
+#!/usr/bin/env sh
+
+# abort on errors
+set -e
+
+# build
+echo Linting..
+npm run lint
+echo Building. this may take a minute...
+npm run build
+
+# navigate into the build output directory
+cd dist
+
+# if you are deploying to a custom domain
+# echo 'example.com' > CNAME
+
+echo Deploying..
+git init
+git add -A
+git commit -m 'deploy'
+
+# deploy
+git push -f git@github.com:<username>/<repo>.git <branch>
+
+cd -
+
+
+

If your on windows look into the Windows Subsystem for Linus (WSL) it will be worth it.

+

If you are still reading, thank you very much. This is actually my first article and I'm really happy to hear about any opinions and criticisms. +Happy Coding ♥

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2019-05-vue-on-github-pages/__data.json b/blog/2019-05-vue-on-github-pages/__data.json new file mode 100644 index 00000000..2dc713ca --- /dev/null +++ b/blog/2019-05-vue-on-github-pages/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":24},{"html":2,"slug":3,"uuid":4,"title":5,"published":6,"description":7,"content_tags":8,"date":13,"cover_image":14,"abstract":15,"tags":16,"links":-1,"type":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\" rel=\"nofollow noopener noreferrer\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>\n\u003Cp>If you want to see an example of this method in action, go check out my \u003Ca href=\"https://tiimb.work\" rel=\"nofollow noopener noreferrer\">personal website\u003C/a> on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>\u003C/p>\n\u003Cp>I won't be explaining how to setup a Vue project. If you're looking for a Tutorial on that go check out the awesome \u003Ca href=\"https://vuejs.org/v2/guide/\" rel=\"nofollow noopener noreferrer\">Vue.js Guide\u003C/a>.\u003C/p>\n\u003Cp>So you have setup your awesome Vue project and want to host it on GitHub Pages. The way Muhammad explained it you would build the project using \u003Ccode>npm run build\u003C/code>, commit the \u003Ccode>dist/\u003C/code> folder along with your source files and point GitHub to the dist folder. This might get quite messy because you either have commit messages with the sole purpose of uploading the dist folder or you commit the code changes at the same time which makes it hard to find the relevant changes if you ever want to look at your commits again.\u003C/p>\n\u003Cp>So what can you do about this?\u003C/p>\n\u003Cp>Git to the rescue, let's use a branch that contains all the build files.\u003C/p>\n\u003Ch2>Step 1 - keeping our working branch clean 🛀\u003C/h2>\n\u003Cp>To make sure that the branch we are working from stays clean of any build files we are gonna add a \u003Ccode>.gitignore\u003C/code> file to the root.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># .gitignore\ndist/\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 2 - adding a second branch 🌳\u003C/h2>\n\u003Cp>We are not goint to branch off master like how we would do it if we were to modify our code with the intention to merge it back to the main branch. Instead we are gonna create a squeaky clean new branch that will only ever hold the dist files. After all we will not ever need to merge these two branches together.\u003C/p>\n\u003Cp>We do this by creating a new git repository inside the dist folder:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">cd dist/\ngit init\ngit add .\ngit commit -m 'Deploying my awesome vue app'\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 3 - deploying 🚚\u003C/h2>\n\u003Cp>We are gonna force push our new git repository to a branch on GitHub. This might go against git best practices but since we won't ever checkout this branch we don't have to worry about that.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">git push -f git@github.com:<username>/<repo>.git <branch>\n\u003C/code>\u003C/pre>\n\u003Cp>⚠️ Make sure you double or tripple check your destination branch! You don't want to accidentally overwrite your working branch. Using the branch \u003Ccode>gh-pages\u003C/code> will most likely be a good idea.\u003C/p>\n\u003Ch2>Step 4 - pointing GitHub to the right place 👈\u003C/h2>\n\u003Cp>Now we are almost done. The only thing left is telling GitHub where our assets live.\u003C/p>\n\u003Cp>Go to your repo, on the top right navigate to \u003Ccode>Settings\u003C/code> and scroll down to GitHub pages. Enable it and set your source branch to the branch you force pushed to, for example \u003Ccode>gh-pages\u003C/code>.\u003C/p>\n\u003Ch2>Step 5 - automating everything 😴\u003C/h2>\n\u003Cp>If you don't mind doing this whole process (Step 2 and 3) every time you want to deploy you can stop now. If you're as lazy as me, here is the script I use to deploy with one command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># deploy.sh\n\n#!/usr/bin/env sh\n\n# abort on errors\nset -e\n\n# build\necho Linting..\nnpm run lint\necho Building. this may take a minute...\nnpm run build\n\n# navigate into the build output directory\ncd dist\n\n# if you are deploying to a custom domain\n# echo 'example.com' > CNAME\n\necho Deploying..\ngit init\ngit add -A\ngit commit -m 'deploy'\n\n# deploy\ngit push -f git@github.com:<username>/<repo>.git <branch>\n\ncd -\n\n\u003C/code>\u003C/pre>\n\u003Cp>If your on windows look into the Windows Subsystem for Linus (WSL) it will be worth it.\u003C/p>\n\u003Cp>If you are still reading, thank you very much. This is actually my first article and I'm really happy to hear about any opinions and criticisms.\nHappy Coding ♥\u003C/p>","blog/2019-05-vue-on-github-pages","96054292-eb45-4d8a-9aca-bb050175ff2a","How I use Vue.js on GitHub Pages",true,"How to properly deploy a Vue.js app on GitHub Pages",[9,10,11,12],"GitHub Pages","Vue.js","Javascript","dev",["Date","2019-05-04T00:00:00.000Z"],"/assets/2019-05-vue-on-github-pages.png","\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>",[12,17,18,19],"github-pages","javascript","vue.js","article","blog",[],"2023-09-02T19:26:59Z",{"html":25,"slug":26,"uuid":27,"date":28,"created":29,"published":6,"abstract":30,"tags":31,"links":-1,"type":20,"cover_image":-1,"description":32,"folder":33},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2019-07-sql-optional-filters-coalesce.html b/blog/2019-07-sql-optional-filters-coalesce.html new file mode 100644 index 00000000..52e11c6b --- /dev/null +++ b/blog/2019-07-sql-optional-filters-coalesce.html @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + How to write optional filters in SQL - Tim Bachmann + + +
+ + +
+
+ +
How to write optional filters in SQL +
+ +

How to write optional filters in SQL

+ + +

dev + quick-tip + sql +

+ +
by
+ published on +
+ + +

The problem

+

Let's say you have a rest API with the following endpoint that returns all of the books in your database:

+
GET /book/
+
+

Your SQL query might look like something like this

+
SELECT *
+FROM books
+
+

Sometimes you want to only list books, for example, from a specific author. How do we do this in SQL?

+

Naive solution: String concatenation ✂

+

One way would be to concatenate your sql query something like this:

+
const arguments = [];
+const queryString = "SELECT * FROM books WHERE true";
+if (authorFilter != null) {
+  queryString += "AND author = ?";
+  arguments.push(authorFilter);
+}
+db.query(queryString, arguments);
+
+

I'm not much of a fan of manually concatenating strings.

+

The coalesce function 🌟

+

Most Databases have the function coalesce which accepts a variable amount of arguments and returns the first argument that is not null.

+
-- Examle
+SELECT coalesce(null, null, 'tiim.ch', null, '@TiimB') as example;
+
+-- Will return
+
+example
+---------
+tiim.ch
+
+

But how will this function help us?

+

Optional filters with the coalesce function

+
SELECT *
+FROM books
+WHERE
+  author = coalesce(?, author);
+
+

If the filter value is null the coalesce expression will resolve to author +and the comparison author = author will be true.

+

If on the other hand the value is set for example to Shakespeare then the author will be compared to Shakespeare.

+

I came across this way to implement optional filters only recently. If you have a more idiomatic way to do this let me know please ✨

+

If you liked this post please follow me on here or on Twitter under @TiimB 😎

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2019-07-sql-optional-filters-coalesce/__data.json b/blog/2019-07-sql-optional-filters-coalesce/__data.json new file mode 100644 index 00000000..0d73b810 --- /dev/null +++ b/blog/2019-07-sql-optional-filters-coalesce/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":21},{"html":2,"slug":3,"uuid":4,"title":5,"published":6,"description":7,"content_tags":8,"date":12,"cover_image":13,"abstract":14,"tags":15,"links":-1,"type":17,"folder":18,"comments":19,"latestComment":20},"\u003Ch2>The problem\u003C/h2>\n\u003Cp>Let's say you have a rest API with the following endpoint that returns all of the books in your database:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-rest\">GET /book/\n\u003C/code>\u003C/pre>\n\u003Cp>Your SQL query might look like something like this\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT *\nFROM books\n\u003C/code>\u003C/pre>\n\u003Cp>Sometimes you want to only list books, for example, from a specific author. How do we do this in SQL?\u003C/p>\n\u003Ch2>Naive solution: String concatenation ✂\u003C/h2>\n\u003Cp>One way would be to concatenate your sql query something like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const arguments = [];\nconst queryString = \"SELECT * FROM books WHERE true\";\nif (authorFilter != null) {\n queryString += \"AND author = ?\";\n arguments.push(authorFilter);\n}\ndb.query(queryString, arguments);\n\u003C/code>\u003C/pre>\n\u003Cp>I'm not much of a fan of manually concatenating strings.\u003C/p>\n\u003Ch2>The coalesce function 🌟\u003C/h2>\n\u003Cp>Most Databases have the function \u003Ccode>coalesce\u003C/code> which accepts a variable amount of arguments and returns the first argument that is not null.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- Examle\nSELECT coalesce(null, null, 'tiim.ch', null, '@TiimB') as example;\n\n-- Will return\n\nexample\n---------\ntiim.ch\n\u003C/code>\u003C/pre>\n\u003Cp>But how will this function help us?\u003C/p>\n\u003Ch2>Optional filters with the coalesce function\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT *\nFROM books\nWHERE\n author = coalesce(?, author);\n\u003C/code>\u003C/pre>\n\u003Cp>If the filter value is null the coalesce expression will resolve to \u003Ccode>author\u003C/code>\nand the comparison \u003Ccode>author = author\u003C/code> will be true.\u003C/p>\n\u003Cp>If on the other hand the value is set for example to Shakespeare then the author will be compared to Shakespeare.\u003C/p>\n\u003Cp>I came across this way to implement optional filters only recently. If you have a more idiomatic way to do this let me know please ✨\u003C/p>\n\u003Cp>If you liked this post please follow me on here or on Twitter under \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> 😎\u003C/p>","blog/2019-07-sql-optional-filters-coalesce","899fb73c-a78e-4cd9-b712-1886715b2d56","How to write optional filters in SQL",true,"A simple way to filter by optional values in SQL with the COALESCE function.",[9,10,11],"SQL","quick-tip","dev",["Date","2019-07-11T00:00:00.000Z"],"/assets/2019-07-sql-optional-filters-coalesce.png","\u003Cp>Let's say you have a rest API with the following endpoint that returns all of the books in your database:\u003C/p>",[11,10,16],"sql","article","blog",[],"2023-09-02T19:26:59Z",{"html":22,"slug":23,"uuid":24,"date":25,"created":26,"published":6,"abstract":27,"tags":28,"links":-1,"type":17,"cover_image":-1,"description":29,"folder":30},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2021-01-git-operations-as-planning-problems.html b/blog/2021-01-git-operations-as-planning-problems.html new file mode 100644 index 00000000..8bff7c30 --- /dev/null +++ b/blog/2021-01-git-operations-as-planning-problems.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + Modelling Git Operations as Planning Problems - Tim Bachmann + + +
+ + +
+
+ +
Modelling Git Operations as Planning Problems +
+ +

Modelling Git Operations as Planning Problems

+ + +

dev + git + pddl + planning-system +

+ +
by
+ published on +last updated on
+ + +

Abstract

+

Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.

+

Download Thesis

+

Cite

+
@thesis{bachmann2021,
+	title        = {Modelling Git Operations as Planning Problems},
+	author       = {Tim Bachmann},
+	year         = {2021},
+  month        = {01},
+	type         = {Bachelor's Thesis},
+	school       = {University of Basel},
+	doi          = {10.13140/RG.2.2.24784.17922}
+}
+
+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2021-01-git-operations-as-planning-problems/__data.json b/blog/2021-01-git-operations-as-planning-problems/__data.json new file mode 100644 index 00000000..444f8853 --- /dev/null +++ b/blog/2021-01-git-operations-as-planning-problems/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":25},{"html":2,"slug":3,"uuid":4,"title":5,"published":6,"description":7,"content_tags":8,"date":13,"modified":14,"cover_image":15,"abstract":16,"tags":17,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>\n\u003Cp>\u003Ca href=\"https://tiim.ch/assets/2021-01-20-Thesis.pdf\" rel=\"nofollow noopener noreferrer\">Download Thesis\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode>@thesis{bachmann2021,\n\ttitle = {Modelling Git Operations as Planning Problems},\n\tauthor = {Tim Bachmann},\n\tyear = {2021},\n month = {01},\n\ttype = {Bachelor's Thesis},\n\tschool = {University of Basel},\n\tdoi = {10.13140/RG.2.2.24784.17922}\n}\n\u003C/code>\u003C/pre>","blog/2021-01-git-operations-as-planning-problems","dc6e6d10-c460-4d3c-8fe2-4ce7535b4af1","Modelling Git Operations as Planning Problems",true,"Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.",[9,10,11,12],"Git","PDDL","Planning-System","dev",["Date","2021-01-20T00:00:00.000Z"],["Date","2023-09-18T11:41:51.000Z"],"/assets/2021-01-git-operations-as-planning-problems.png","\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>",[12,18,19,20],"git","pddl","planning-system","article","blog",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"date":29,"created":30,"published":6,"abstract":31,"tags":32,"links":-1,"type":21,"cover_image":-1,"description":33,"folder":34},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2022-02-phone-audio-to-pc.html b/blog/2022-02-phone-audio-to-pc.html new file mode 100644 index 00000000..31236bc1 --- /dev/null +++ b/blog/2022-02-phone-audio-to-pc.html @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + How to Listen to Phone Audio on PC - Tim Bachmann + + +
+ + +
+
+ +
How to Listen to Phone Audio on PC +
+ +

How to Listen to Phone Audio on PC

+ + +

audio + bluetooth + how-to + software + windows +

+ +
by
+ published on +
+ + +

Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.

+

TLDR:

+
    +
  • Either use Bluetooth Audio Receiver from the Microsoft Store to connect you phone via Bluetooth,
  • +
  • Or use an audio cable to connect the phone to the "line-in" on your PC.
  • +
+

Bluetooth (recommended)

+

Requirements: A PC with integrated Bluetooth or a Bluetooth dongle.

+

I recommend this approach more than the wired one because it is way less effort, you don't have to deal with a USB or lightning to audio dongle and in my opinion it is more reliable.

+

Pair your phone with your PC as normal, by opening the Bluetooth settings on your phone and on the PC and wait for the devices to show up. When you successfully paired the phone once you will not have to do this again. Now you need an app that will tell the phone that it can use the PC as a wireless speaker. The only app I found that will do this is the Bluetooth Audio Receiver app from the Windows Store. Install and and open it. You should see your phone on the list of Bluetooth devices on the app. Select it and click on the Open Connection button. It might take a moment but after it connected, you should hear all sounds from your phone on your PC.

+

Wired

+

Requirements:

+
    +
  • Male-to-Male audio cable (3.5mm audio jack).
  • +
  • A line-in port on your PC (usually blue audio jack on the back)
  • +
  • USB-C to audio jack adapter (Optional)
  • +
  • Lighting to audio jack adapter (Optional)
  • +
+

This approach works if your PC doesn't support Bluetooth, or if the Bluetooth connection drops for some reason. Connect the audio cable to the blue line-in jack on the back of the computer. Then, connect the phone to the other end of the audio cable. If your phone does not have an audio jack, use the adapter on the USB-C or Lightning port. If your PC detects that you connected a new line-in device, it might open the audio settings automatically. If not, right-click on the volume icon on the taskbar next to the clock and select Sounds. Navigate to the Input tab and double click on the Line-In entry (the one with a cable icon). Navigate to the Monitor tab and select the check box for "Use this device as a playback source". This will tell windows it should play all sounds received through this input directly to the speakers. Usually this is used to monitor microphones but it works for this use case too. You should now hear any sound from your phone through your PC headphones or speakers. Make sure you turn this checkbox off when you disconnect your phone. Otherwise you might hear a crackle or other sounds when the loose cable gets touched.

+

Photo by Lisa Fotios from Pexels

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2022-02-phone-audio-to-pc/__data.json b/blog/2022-02-phone-audio-to-pc/__data.json new file mode 100644 index 00000000..b81e7465 --- /dev/null +++ b/blog/2022-02-phone-audio-to-pc/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":22},{"html":2,"slug":3,"uuid":4,"title":5,"published":6,"date":7,"description":8,"cover_image":9,"content_tags":10,"abstract":16,"tags":17,"links":-1,"type":18,"folder":19,"comments":20,"latestComment":21},"\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>\n\u003Cp>\u003Cem>TLDR\u003C/em>:\u003C/p>\n\u003Cul>\n\u003Cli>Either use Bluetooth Audio Receiver from the Microsoft Store to connect you phone via Bluetooth,\u003C/li>\n\u003Cli>Or use an audio cable to connect the phone to the \"line-in\" on your PC.\u003C/li>\n\u003C/ul>\n\u003Ch2>Bluetooth (recommended)\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>: A PC with integrated Bluetooth or a Bluetooth dongle.\u003C/p>\n\u003Cp>I recommend this approach more than the wired one because it is way less effort, you don't have to deal with a USB or lightning to audio dongle and in my opinion it is more reliable.\u003C/p>\n\u003Cp>Pair your phone with your PC as normal, by opening the Bluetooth settings on your phone and on the PC and wait for the devices to show up. When you successfully paired the phone once you will not have to do this again. Now you need an app that will tell the phone that it can use the PC as a wireless speaker. The only app I found that will do this is the \u003Ca href=\"https://www.microsoft.com/de-de/p/bluetooth-audio-receiver/9n9wclwdqs5j\" rel=\"nofollow noopener noreferrer\">Bluetooth Audio Receiver\u003C/a> app from the Windows Store. Install and and open it. You should see your phone on the list of Bluetooth devices on the app. Select it and click on the \u003Ccode>Open Connection\u003C/code> button. It might take a moment but after it connected, you should hear all sounds from your phone on your PC.\u003C/p>\n\u003Ch2>Wired\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>:\u003C/p>\n\u003Cul>\n\u003Cli>Male-to-Male audio cable (3.5mm audio jack).\u003C/li>\n\u003Cli>A line-in port on your PC (usually blue audio jack on the back)\u003C/li>\n\u003Cli>USB-C to audio jack adapter (Optional)\u003C/li>\n\u003Cli>Lighting to audio jack adapter (Optional)\u003C/li>\n\u003C/ul>\n\u003Cp>This approach works if your PC doesn't support Bluetooth, or if the Bluetooth connection drops for some reason. Connect the audio cable to the blue line-in jack on the back of the computer. Then, connect the phone to the other end of the audio cable. If your phone does not have an audio jack, use the adapter on the USB-C or Lightning port. If your PC detects that you connected a new line-in device, it might open the audio settings automatically. If not, right-click on the volume icon on the taskbar next to the clock and select \u003Ccode>Sounds\u003C/code>. Navigate to the \u003Ccode>Input\u003C/code> tab and double click on the Line-In entry (the one with a cable icon). Navigate to the Monitor tab and select the check box for \"Use this device as a playback source\". This will tell windows it should play all sounds received through this input directly to the speakers. Usually this is used to monitor microphones but it works for this use case too. You should now hear any sound from your phone through your PC headphones or speakers. Make sure you turn this checkbox off when you disconnect your phone. Otherwise you might hear a crackle or other sounds when the loose cable gets touched.\u003C/p>\n\u003Cp>\u003Cem>Photo by Lisa Fotios from Pexels\u003C/em>\u003C/p>","blog/2022-02-phone-audio-to-pc","be57f2df-d58f-4b79-8a51-e20d482f46cf","How to Listen to Phone Audio on PC",true,["Date","2022-02-12T00:00:00.000Z"],"Learn how to connect your phone audio to your PC over wire or Bluetooth.","/assets/2022-02-phone-audio-to-pc.jpg",[11,12,13,14,15],"how-to","audio","windows","bluetooth","software","\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>",[12,14,11,15,13],"article","blog",[],"2023-09-02T19:26:59Z",{"html":23,"slug":24,"uuid":25,"date":26,"created":27,"published":6,"abstract":28,"tags":29,"links":-1,"type":18,"cover_image":-1,"description":30,"folder":31},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2022-03-ssh-windows-wsl.html b/blog/2022-03-ssh-windows-wsl.html new file mode 100644 index 00000000..b440a803 --- /dev/null +++ b/blog/2022-03-ssh-windows-wsl.html @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + How to set up an SSH Server on Windows with WSL - Tim Bachmann + + +
+ + +
+
+ +
How to set up an SSH Server on Windows with WSL +
+ +

How to set up an SSH Server on Windows with WSL

+ + +

dev + ssh + windows + wsl +

+ +
by
+ published on +
+ + +

There are many guides on the internet showing how to set up an SSH server inside WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).

+

Installing the OpenSSH Server

+

Windows has been shipping with an OpenSSH client and server for a long time. They are not installed by default but can be activated either in the settings as described in the official docs or with the following PowerShell commands.

+

You will need to start PowerShell as Administrator

+

First, install the OpenSSH client and server.

+
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
+Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
+
+

Enable the SSH service and make sure the firewall rule is configured:

+
# Enable the service
+Start-Service sshd
+Set-Service -Name sshd -StartupType 'Automatic'
+
+# Confirm the firewall rule is configured. It should be created automatically by setup. Run the following to verify
+if (!(Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {
+    Write-Output "Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it..."
+    New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
+} else {
+    Write-Output "Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists."
+}
+
+

Congratulations, you have installed the SSH server on your Windows machine. And all without manually setting up a background service or modifying config files.

+

Setting WSL as Default Shell

+

To directly boot into WSL when connecting, we need to change the default shell from cmd.exe or PowerShell.exe to bash.exe, which in turn runs the default WSL distribution. This can be done with the PowerShell command:

+
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\WINDOWS\System32\bash.exe" -PropertyType String -Force
+
+

Note: even though the shell is running on the Linux side, the SSH server is still on windows. This means you have to use to windows username to log in, and the SCP command copies files relative to the user directory on windows.

+

Enable Key-based Authentication (non-Admin User)

+

Note: If the user account has Admin permissions, read the next chapter, otherwise continue reading.

+

Create the folder .ssh in the users home directory on windows: (e.g. C:\Users\<username>\.ssh). Run the following commands in PowerShell (not as administrator).

+
New-Item -Path ~\.ssh -ItmeType "directory"
+New-Item -Path ~\.ssh\authorized_keys
+
+

The file .ssh\autzorized_keys will contain a list of all public keys that shall be allowed to connect to the SSH server.

+

Copy the contents of your public key file (usually stored in ~/.ssh/id_rsa.pub) to the authorized_keys file. If a key is already present, paste your key on a new line.

+

Enable Key-based Authentication (Admin User)

+

If the user is in the Administrators group, it is not possible to have the authorized_keys file in the user directory for security purposes. +Instead, it needs to be located on the following path %ProgramData%\ssh\administrators_authorized_keys. A second requirement is that it is only accessible to Administrator users, to prevent a normal user from gaining admin permissions.

+

To create the file start PowerShell as administrator and run the following command.

+
New-Item -Path $env:programdata\ssh\administrators_authorized_keys
+
+

This will create the file with the correct permissions. Now open the file and paste your public key into it. The public key should be located at ~/.ssh/id_rsa.pub. If a key is already present, paste your key on a new line.

+

Verifying everything works

+

Verify that you can SSH into your machine by running the following inside WSL:

+
IP=$(cat /etc/resolv.conf | grep nameserver | cut -d " " -f2) # get the windows host ip address
+ssh <user>@$IP
+
+

Or from PowerShell and cmd:

+
ssh <user>@localhost
+
+

Drawbacks

+

There are some drawbacks to this approach. If you rely on some programs or scripts to work over SSH, this might not be the method for you. Most scripts expect a unix machine on the other end, or if they expect a windows machine they will most likely not be configured to deal with WSL.

+

If you however just want to connect to your pc to copy some files or change some settings this approach is perfectly fine.

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

3 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
  • + +
    + In reply to + undefined + +

    Hi Tim, no problem. I had this error in "Event Viewer > Applications and Services Logs > OpenSSH > Admin" and figure it out that sshd seems to search the Administrators groups to operate, literal name and not properly localized by region. + +erroid:2 user:SYSTEM details:"sshd: error: unable to resolve group administrators" + +Maybe is not all the non-english windows with this problem, but I have it but after created the group works like a charm.

    +
  • + +

    Tim

    + 2022-09-20 12:58:29 PM + + + +
    + In reply to + undefined + +

    Hi FinderX + +Thanks for the heads up. I don't remember having to create a new user group, even though my system language is German. Maybe I just forgot about that though.

    +
  • + +

    FinderX

    + 2022-09-20 7:50:46 AM + + + +
    + +

    Hi! +I add some roundabouts about admin-users, if your windows ssh server system language is NOT english, you must create 'Administrators' group (without quotes) in your language equivalent of "Users and Local Groups > Groups", if your server is a DC (Domain Controller) create it in your language equivalent of "Active Directories Users and Computers". + +Create the user group with name Administrators, description whatever, ex. "Dummy group for sshd to work correctly.", and in Members add your language equivalent of the user Administrator. + +This is optional but I suggest you change these settings in "%programdata%\ssh\sshd_config" after you successfully copy your public key to the ssh server : + +StrictModes yes +PubkeyAuthentication yes +PasswordAuthentication no + +You can see the log activity in your language equivalent of "Applications and Services Logs > OpenSSH > Admin or Operational" + +Best Regards.

    +
  • +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2022-03-ssh-windows-wsl/__data.json b/blog/2022-03-ssh-windows-wsl/__data.json new file mode 100644 index 00000000..1ce08acb --- /dev/null +++ b/blog/2022-03-ssh-windows-wsl/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":43},{"html":2,"slug":3,"uuid":4,"title":5,"published":6,"date":7,"description":8,"cover_image":9,"content_tags":10,"abstract":15,"tags":16,"links":-1,"type":20,"folder":21,"comments":22,"latestComment":42},"\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\" rel=\"nofollow noopener noreferrer\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\" rel=\"nofollow noopener noreferrer\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\" rel=\"nofollow noopener noreferrer\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\" rel=\"nofollow noopener noreferrer\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>\n\u003Ch2>Installing the OpenSSH Server\u003C/h2>\n\u003Cp>Windows has been shipping with an OpenSSH client and server for a long time. They are not installed by default but can be activated either in the settings as described \u003Ca href=\"https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse\" rel=\"nofollow noopener noreferrer\">in the official docs\u003C/a> or with the following PowerShell commands.\u003C/p>\n\u003Cp>\u003Cstrong>You will need to start PowerShell as Administrator\u003C/strong>\u003C/p>\n\u003Cp>First, install the OpenSSH client and server.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0\nAdd-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0\n\u003C/code>\u003C/pre>\n\u003Cp>Enable the SSH service and make sure the firewall rule is configured:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\"># Enable the service\nStart-Service sshd\nSet-Service -Name sshd -StartupType 'Automatic'\n\n# Confirm the firewall rule is configured. It should be created automatically by setup. Run the following to verify\nif (!(Get-NetFirewallRule -Name \"OpenSSH-Server-In-TCP\" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {\n Write-Output \"Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it...\"\n New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22\n} else {\n Write-Output \"Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists.\"\n}\n\u003C/code>\u003C/pre>\n\u003Cp>Congratulations, you have installed the SSH server on your Windows machine. And all without manually setting up a background service or modifying config files.\u003C/p>\n\u003Ch2>Setting WSL as Default Shell\u003C/h2>\n\u003Cp>To directly boot into WSL when connecting, we need to change the default shell from \u003Ccode>cmd.exe\u003C/code> or \u003Ccode>PowerShell.exe\u003C/code> to \u003Ccode>bash.exe\u003C/code>, which in turn runs the default WSL distribution. This can be done with the PowerShell command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-ItemProperty -Path \"HKLM:\\SOFTWARE\\OpenSSH\" -Name DefaultShell -Value \"C:\\WINDOWS\\System32\\bash.exe\" -PropertyType String -Force\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: even though the shell is running on the Linux side, the SSH server is still on windows. This means you have to use to windows username to log in, and the SCP command copies files relative to the user directory on windows.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (non-Admin User)\u003C/h2>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: If the user account has Admin permissions, read the next chapter, otherwise continue reading.\u003C/p>\n\u003Cp>Create the folder \u003Ccode>.ssh\u003C/code> in the users home directory on windows: (e.g. \u003Ccode>C:\\Users\\<username>\\.ssh\u003C/code>). Run the following commands in PowerShell (not as administrator).\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path ~\\.ssh -ItmeType \"directory\"\nNew-Item -Path ~\\.ssh\\authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>The file \u003Ccode>.ssh\\autzorized_keys\u003C/code> will contain a list of all public keys that shall be allowed to connect to the SSH server.\u003C/p>\n\u003Cp>Copy the contents of your public key file (usually stored in \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>) to the \u003Ccode>authorized_keys\u003C/code> file. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (Admin User)\u003C/h2>\n\u003Cp>If the user is in the Administrators group, it is not possible to have the \u003Ccode>authorized_keys\u003C/code> file in the user directory for security purposes.\nInstead, it needs to be located on the following path \u003Ccode>%ProgramData%\\ssh\\administrators_authorized_keys\u003C/code>. A second requirement is that it is only accessible to Administrator users, to prevent a normal user from gaining admin permissions.\u003C/p>\n\u003Cp>To create the file start PowerShell as administrator and run the following command.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path $env:programdata\\ssh\\administrators_authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>This will create the file with the correct permissions. Now open the file and paste your public key into it. The public key should be located at \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Verifying everything works\u003C/h2>\n\u003Cp>Verify that you can SSH into your machine by running the following inside WSL:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">IP=$(cat /etc/resolv.conf | grep nameserver | cut -d \" \" -f2) # get the windows host ip address\nssh <user>@$IP\n\u003C/code>\u003C/pre>\n\u003Cp>Or from PowerShell and cmd:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ssh <user>@localhost\n\u003C/code>\u003C/pre>\n\u003Ch2>Drawbacks\u003C/h2>\n\u003Cp>There are some drawbacks to this approach. If you rely on some programs or scripts to work over SSH, this might not be the method for you. Most scripts expect a unix machine on the other end, or if they expect a windows machine they will most likely not be configured to deal with WSL.\u003C/p>\n\u003Cp>If you however just want to connect to your pc to copy some files or change some settings this approach is perfectly fine.\u003C/p>","blog/2022-03-ssh-windows-wsl","03b5a86c-5f4d-4086-9f5f-e1e46b4bcf58","How to set up an SSH Server on Windows with WSL",true,["Date","2022-03-02T00:00:00.000Z"],"It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.","/assets/2022-03-ssh-windows-wsl.png",[11,12,13,14],"SSH","WSL","Windows","dev","\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>",[14,17,18,19],"ssh","windows","wsl","article","blog",[23,31,37],{"id":24,"type":25,"replyTo":26,"timestamp":27,"page":3,"url":28,"content":29,"name":30},"20f2c526-7466-4c21-83ac-51750f278328","comment","f7c1891b-e97b-4030-863e-19344ed84d32","2022-09-20T14:59:17Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#20f2c526-7466-4c21-83ac-51750f278328","Hi Tim, no problem. I had this error in \"Event Viewer > Applications and Services Logs > OpenSSH > Admin\" and figure it out that sshd seems to search the Administrators groups to operate, literal name and not properly localized by region.\n\nerroid:2 user:SYSTEM details:\"sshd: error: unable to resolve group administrators\"\n\nMaybe is not all the non-english windows with this problem, but I have it but after created the group works like a charm.","",{"id":26,"type":25,"replyTo":32,"timestamp":33,"page":3,"url":34,"content":35,"name":36},"5de01bc5-b7c7-4522-9dfe-c67f103d4c03","2022-09-20T12:58:29Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#f7c1891b-e97b-4030-863e-19344ed84d32","Hi FinderX\n\nThanks for the heads up. I don't remember having to create a new user group, even though my system language is German. Maybe I just forgot about that though.","Tim",{"id":32,"type":25,"replyTo":30,"timestamp":38,"page":3,"url":39,"content":40,"name":41},"2022-09-20T07:50:46Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#5de01bc5-b7c7-4522-9dfe-c67f103d4c03","Hi!\nI add some roundabouts about admin-users, if your windows ssh server system language is NOT english, you must create 'Administrators' group (without quotes) in your language equivalent of \"Users and Local Groups > Groups\", if your server is a DC (Domain Controller) create it in your language equivalent of \"Active Directories Users and Computers\".\n\nCreate the user group with name Administrators, description whatever, ex. \"Dummy group for sshd to work correctly.\", and in Members add your language equivalent of the user Administrator.\n\nThis is optional but I suggest you change these settings in \"%programdata%\\ssh\\sshd_config\" after you successfully copy your public key to the ssh server :\n\nStrictModes yes\nPubkeyAuthentication yes\nPasswordAuthentication no\n\nYou can see the log activity in your language equivalent of \"Applications and Services Logs > OpenSSH > Admin or Operational\"\n\nBest Regards.","FinderX","2023-09-02T19:26:59Z",{"html":44,"slug":45,"uuid":46,"date":47,"created":48,"published":6,"abstract":49,"tags":50,"links":-1,"type":20,"cover_image":-1,"description":30,"folder":51},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2022-06-use-rss.html b/blog/2022-06-use-rss.html new file mode 100644 index 00000000..b487586f --- /dev/null +++ b/blog/2022-06-use-rss.html @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + You should be using RSS - Tim Bachmann + + +
+ + +
+
+ +
You should be using RSS +
+ +

You should be using RSS

+ + +

dev + rss + software +

+ +
by
+ published on +
+ + +

I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.

+

But there is another way! By subscribing to RSS feeds you are in control of what you are shown. Most websites, blogs, news sites and even social media sites provide RSS feeds to subscribe to. You get only the articles, videos or audio content you are subscribed to, without any algorithm messing with your attention.

+

But what exactly is an RSS feed?

+

RSS stands for "Really Simple Syndication", and it is a protocol for a website to provide a list of content. It is an old protocol, the first version was introduced in 1999, but it might be more useful nowadays than ever. +If you listen to podcasts, you are already familiar with RSS feeds: a podcast is an RSS feed which links to audio files instead of online articles. +An RSS feed is just an XML document which contains information about the feed and a list of content. +When you use an app to subscribe to an RSS feed, this app will just save the URL to the XML document and load it regularly to check if new content is available. You are completely in control of how often the feed is refreshed and what feeds you want to subscribe to. Some RSS reader apps also allow you to specify some rules for example about if you should be notified, based on the feed, the content or the tags.

+

How to subscribe to a feed?

+

Since an RSS feed is just an XML document, you don't technically have to subscribe to a feed to read it, you could just open the document and read the XML. But that would be painful. Luckily there are several plugins, apps and services that allow you to easily subscribe to and read RSS feeds.

+

If you want to start using RSS and are not sure if you will take the time to open a dedicated app, I would recommend using an RSS plugin for another software that you are using regularly. For example, the Thunderbird email client already has built-in RSS support. If you want to read to the feeds directly inside of your browser, you can use the feedbro extension for Chrome, Firefox, and other Chromium-based browsers. I use the Vivaldi browser which comes with an integrated RSS feed reader.

+

What if there is no RSS feed?

+

Unfortunately not every website offers an RSS feed. Although it might be worth it to hunt for them. Some websites offer an RSS feed but do not link to it anywhere. +If there is no feed, but a newsletter is offered, the service "Kill The Newsletter" will provide you with email addresses and a corresponding RSS URL to convert any newsletter to a feed. Another service to consider is FetchRSS. It turns any website into an RSS feed.

+

RSS Apps

+

If you want to have a dedicated app for your reading, you're in luck! There is a plethora of apps to choose from, all with different features and user interfaces. +There are three main types of apps: standalone apps, service-based apps, and self-hosted apps. Most apps are standalone, meaning they fetch the RSS feeds only when open, and don't sync to your other devices. The service-based apps rely on a cloud service which will fetch the feeds around the clock, even when all your devices are off. They can also send you a summary mail if you forget to check for some time and they can sync your subscriptions across all your devices. Unfortunately, most service-based apps only offer a limited experience for free. The last category is self-hosted apps. They are similar to the service based apps but instead of some company running the service, you have to provide a server for the service to run yourself.

+

I use a standalone app, because I do not want to rely on a service, but I also don't want to go through the hassle of setting up a self-hosted solution.

+

If you are still unsure what RSS app you could try out, I provided a list below. Make sure to add the RSS feed for my blog (https://tiim.ch/blog/rss.xml) to test it out 😉

+

Standalone Apps

+ +

Service-Based Apps

+ +

Self-hosted Apps

+
+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

1 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
  • + +

    Jamie Tanna

    + 2022-11-13 8:34:12 AM + +

    Mentioned this post

    +
    + +

    Liked +You should be using RSS +Post detailsDecide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read. https://i.imgur.com/t3mebu7.png

    +
  • +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2022-06-use-rss/__data.json b/blog/2022-06-use-rss/__data.json new file mode 100644 index 00000000..5a11cbfb --- /dev/null +++ b/blog/2022-06-use-rss/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":31},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"aliases":7,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"content_tags":13,"abstract":17,"tags":18,"links":-1,"type":19,"folder":20,"comments":21,"latestComment":30},"\u003Cp>I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.\u003C/p>\n\u003Cp>But there is another way! By subscribing to RSS feeds you are in control of what you are shown. Most websites, blogs, news sites and even social media sites provide RSS feeds to subscribe to. You get only the articles, videos or audio content you are subscribed to, without any algorithm messing with your attention.\u003C/p>\n\u003Ch2>But what exactly is an RSS feed?\u003C/h2>\n\u003Cp>RSS stands for \"Really Simple Syndication\", and it is a protocol for a website to provide a list of content. It is an old protocol, the first version was introduced in 1999, but it might be more useful nowadays than ever.\nIf you listen to podcasts, you are already familiar with RSS feeds: a podcast is an RSS feed which links to audio files instead of online articles.\nAn RSS feed is just an XML document which contains information about the feed and a list of content.\nWhen you use an app to subscribe to an RSS feed, this app will just save the URL to the XML document and load it regularly to check if new content is available. You are completely in control of how often the feed is refreshed and what feeds you want to subscribe to. Some RSS reader apps also allow you to specify some rules for example about if you should be notified, based on the feed, the content or the tags.\u003C/p>\n\u003Ch2>How to subscribe to a feed?\u003C/h2>\n\u003Cp>Since an RSS feed is just an XML document, you don't \u003Cem>technically\u003C/em> have to subscribe to a feed to read it, you \u003Cem>could\u003C/em> just open the document and read the XML. But that would be painful. Luckily there are several plugins, apps and services that allow you to easily subscribe to and read RSS feeds.\u003C/p>\n\u003Cp>If you want to start using RSS and are not sure if you will take the time to open a dedicated app, I would recommend using an RSS plugin for another software that you are using regularly. For example, the \u003Ca href=\"https://thunderbird.net/\" rel=\"nofollow noopener noreferrer\">Thunderbird\u003C/a> email client already has built-in RSS support. If you want to read to the feeds directly inside of your browser, you can use the \u003Ca href=\"https://nodetics.com/feedbro/\" rel=\"nofollow noopener noreferrer\">feedbro\u003C/a> extension for Chrome, Firefox, and other Chromium-based browsers. I use the \u003Ca href=\"https://vivaldi.com\" rel=\"nofollow noopener noreferrer\">Vivaldi\u003C/a> browser which comes with an integrated RSS feed reader.\u003C/p>\n\u003Ch2>What if there is no RSS feed?\u003C/h2>\n\u003Cp>Unfortunately not every website offers an RSS feed. Although it might be worth it to hunt for them. Some websites offer an RSS feed but do not link to it anywhere.\nIf there is no feed, but a newsletter is offered, the service \"\u003Ca href=\"https://kill-the-newsletter.com\" rel=\"nofollow noopener noreferrer\">Kill The Newsletter\u003C/a>\" will provide you with email addresses and a corresponding RSS URL to convert any newsletter to a feed. Another service to consider is \u003Ca href=\"http://fetchrss.com\" rel=\"nofollow noopener noreferrer\">FetchRSS\u003C/a>. It turns any website into an RSS feed.\u003C/p>\n\u003Ch2>RSS Apps\u003C/h2>\n\u003Cp>If you want to have a dedicated app for your reading, you're in luck! There is a plethora of apps to choose from, all with different features and user interfaces.\nThere are three main types of apps: standalone apps, service-based apps, and self-hosted apps. Most apps are standalone, meaning they fetch the RSS feeds only when open, and don't sync to your other devices. The service-based apps rely on a cloud service which will fetch the feeds around the clock, even when all your devices are off. They can also send you a summary mail if you forget to check for some time and they can sync your subscriptions across all your devices. Unfortunately, most service-based apps only offer a limited experience for free. The last category is self-hosted apps. They are similar to the service based apps but instead of some company running the service, you have to provide a server for the service to run yourself.\u003C/p>\n\u003Cp>I use a standalone app, because I do not want to rely on a service, but I also don't want to go through the hassle of setting up a self-hosted solution.\u003C/p>\n\u003Cp>If you are still unsure what RSS app you could try out, I provided a list below. Make sure to add the \u003Ca href=\"https://tiim.ch/blog/rss.xml\" rel=\"nofollow noopener noreferrer\">RSS feed for my blog\u003C/a> (\u003Ccode>https://tiim.ch/blog/rss.xml\u003C/code>) to test it out 😉\u003C/p>\n\u003Ch3>Standalone Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://thunderbird.net\" rel=\"nofollow noopener noreferrer\">Thunderbird\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://ravenreader.app\" rel=\"nofollow noopener noreferrer\">RavenReader\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://netnewswire.com\" rel=\"nofollow noopener noreferrer\">NetNewsWire\u003C/a> (Free, Integration with Services possible)\u003C/li>\n\u003Cli>\u003Ca href=\"https://vivaldi.com\" rel=\"nofollow noopener noreferrer\">Vivaldi Browser\u003C/a> (Free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://nodetics.com/feedbro/\" rel=\"nofollow noopener noreferrer\">feedbro browser extension\u003C/a> (Free)\u003C/li>\n\u003C/ul>\n\u003Ch3>Service-Based Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://feedreader.com\" rel=\"nofollow noopener noreferrer\">FeedReader\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://feeder.co\" rel=\"nofollow noopener noreferrer\">Feeder\u003C/a> (Freemium, 10 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.inoreader.com/pricing\" rel=\"nofollow noopener noreferrer\">Inoreader\u003C/a> (Freemium, Ads and 150 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://newsblur.com\" rel=\"nofollow noopener noreferrer\">NewsBlur\u003C/a> (Freemium, 64 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.feedspot.com\" rel=\"nofollow noopener noreferrer\">Feedspot\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://feedly.com\" rel=\"nofollow noopener noreferrer\">Feedly\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://feedbin.com\" rel=\"nofollow noopener noreferrer\">Feedbin\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://theoldreader.com\" rel=\"nofollow noopener noreferrer\">TheOldReader\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://bazqux.com\" rel=\"nofollow noopener noreferrer\">BazQux\u003C/a>\u003C/li>\n\u003C/ul>\n\u003Ch3>Self-hosted Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://www.commafeed.com/\" rel=\"nofollow noopener noreferrer\">CommaFeed\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://freshrss.org\" rel=\"nofollow noopener noreferrer\">FreshRSS\u003C/a> (Free, OSS)\u003C/li>\n\u003C/ul>","blog/2022-06-use-rss","ee633c0d-d668-48e5-af57-aaae9d243099",["Date","2022-06-05T00:00:00.000Z"],["Date","2022-06-04T22:01:15.123Z"],[8],null,"You should be using RSS",true,"Decide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read.","https://i.imgur.com/t3mebu7.png",[14,15,16],"rss","dev","software","\u003Cp>I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.\u003C/p>",[15,14,16],"article","blog",[22],{"id":23,"type":24,"replyTo":25,"timestamp":26,"page":3,"url":27,"content":28,"name":29},"31ea6d40-e8c2-4ada-ac16-028a3036d2de","webmention","","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/oatm9/","Liked\nYou should be using RSS\nPost detailsDecide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read. https://i.imgur.com/t3mebu7.png","Jamie Tanna","2023-09-02T19:26:59Z",{"html":32,"slug":33,"uuid":34,"date":35,"created":36,"published":10,"abstract":37,"tags":38,"links":-1,"type":19,"cover_image":-1,"description":25,"folder":39},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2022-07-12-first-go-project-commenting-api.html b/blog/2022-07-12-first-go-project-commenting-api.html new file mode 100644 index 00000000..6eda7028 --- /dev/null +++ b/blog/2022-07-12-first-go-project-commenting-api.html @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + First Go Project: A Jam-stack Commenting API - Tim Bachmann + + +
+ + +
+
+ +
First Go Project: A Jam-stack Commenting API +
+ +

First Go Project: A Jam-stack Commenting API

+ + +

go + indiego + project + tiim.ch + web-api +

+ +
by
+ published on +last updated on
+ + +

I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on Github Pages, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.

+

I kept my eyes open for a service that I liked, but I did not want to just integrate any old service into my website, I did have some requirements:

+
    +
  • The service should not cost anything. I would rather host something myself than sign up for another subscription (because I'm already paying for a VPS anyway).
  • +
  • I want to control how the comments on my website are displayed. I quite like my website design and I don't want a generic comment box below my posts.
  • +
  • The service should respect the privacy of the people using my website.
  • +
  • There should be an option to comment without setting up an account with the service.
  • +
+

While looking around for how other people integrated comments into their static websites, I found a nice blog post from Average Linux User which compares a few popular commenting systems. +Unfortunately, most systems either are not very privacy-friendly, cost money or store the comments as comments on Github issues..? +After looking through the options I decided to use this opportunity to write my own commenting system and dabble with the Go programming language.

+

Writing a commenting API in Go

+

First thing first, if you want to take a look at the code, check out the Github repo.

+

I decided to write the commenting system in Go because I have been looking for an excuse to practice Go for a while, and this seemed like the perfect fit. It is a small CRUD app, consisting of a storage component, an API component and a small event component in the middle to easily compose the functionality I want.

+

Currently, it supports the following functionality:

+
    +
  • Listing all comments (optionally since a specified timestamp)
  • +
  • Listing all comments for a specified page (optionally since a specified timestamp)
  • +
  • Posting comments through the API
  • +
  • A simple admin dashboard that lists all comments and allows the admin to delete them
  • +
  • Email notifications when someone comments
  • +
  • Email notifications when someone replies to your comment
  • +
  • SQLite storage for comments
  • +
+

The code is built in a way to make it easy to customise the features. +For example to disable features like the email reply notifications you can just comment out the line in the main.go file that registers that hook.

+

To write custom hooks that get executed when a new comment gets submitted or one gets deleted, just implement the Handler interface and register it in the main method.

+

You can also easily add other storage options like databases or file storage by implementing the Store and SubscribtionStore interfaces.

+

Can it be used in production? 🚗💨

+

I currently use it on this website! Go test it out (I might delete the comments if they are rude though 🤔).

+

In all seriousness, I would not use it for a website where the comments are critical. But for a personal blog or similar, I don't see why not.

+

If you want to host your own version, there is a Dockerfile available. If you decide to integrate this into your website, please comment below, ping me @TiimB or shoot me an email hey@tiim.ch, I would love to check it out.

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

11 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
  • + +

    Tim Bachmann

    + 2023-08-02 9:10:04 AM + +

    Mentioned this post

    +
    + +

    I blogged about creating a comment system for my website a while ago, +and later how I implemented webmentions into that same project. +Since then this little go program has grown quite a bit, and it has turned into a modular platform +that supports quite a few technologies:...

    +
  • + +

    Anonymous

    + 2023-07-09 7:25:02 PM + + + +
    + +

    Heya just saw your post on Reddit about this comment feature, didn't want to leave without using it ^^. Nicely done!

    +
  • + +

    Tiim

    + 2022-12-07 11:01:37 PM + + + +
    + In reply to + undefined + +

    You are right, there is not any documentation in the readme yet. Although hopefully, I will work on that soon. I'm in the middle of refactoring the project. + +To query the comments there are two rest endpoints ["/comment"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L41-L71) and ["/comment/:page"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L73-L107) which return all comments or the comments for a specific page. The comments are loaded from this API endpoint when the site is generated. + +To display the comments without rebuilding the site, new comments are fetched in the browser with the "?since=<time-of-last-build>" query parameter.

    +
  • + +

    Poorchop

    + 2022-12-07 10:33:44 PM + + + +
    + +

    Good stuff. Always nice to see a site supporting comments and/or Webmentions. Maybe I missed it in the readme but I am curious as to how one queries the API for comments. Do you pull in the comments from the database when you generate the site?

    +
  • + +

    Golang Bot

    + 2022-11-27 10:31:58 PM + +

    Mentioned this post

    +
    + +

    I published a new blog post: +First Go Project: A jam-stack Commenting API +tiim.ch/blog/2022-07-1… +#golang #jamstack #API

    +
  • + +

    Tim Bachmann

    + 2022-11-21 10:19:23 PM + +

    Mentioned this post

    +
    + +

    This site now supports sending and receiving webmentions and surfacing structured data using microformats2.

    +
  • + +

    hola

    + 2022-07-18 8:44:11 AM + + + +
    + +

    hola

    +
  • + +

    polite

    + 2022-07-13 9:06:11 PM + + + +
    + In reply to + undefined + +

    And a polite reply

    +
  • + +

    somGuy

    + 2022-07-13 6:31:31 PM + + + +
    + +

    Pretty cool dudez

    +
  • + +

    rude

    + 2022-07-12 9:40:39 PM + + + +
    + +

    This is a rude comment ;)

    +
  • + +

    wdup

    + 2022-07-12 1:30:14 PM + + + +
    + +

    Good job dude!

    +
  • +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2022-07-12-first-go-project-commenting-api/__data.json b/blog/2022-07-12-first-go-project-commenting-api/__data.json new file mode 100644 index 00000000..82aa325d --- /dev/null +++ b/blog/2022-07-12-first-go-project-commenting-api/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":93},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"aliases":7,"title":9,"published":10,"modified":11,"description":12,"cover_image":13,"content_tags":14,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":92},"\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\" rel=\"nofollow noopener noreferrer\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>\n\u003Cp>I kept my eyes open for a service that I liked, but I did not want to just integrate any old service into my website, I did have some requirements:\u003C/p>\n\u003Cul>\n\u003Cli>The service should not cost anything. I would rather host something myself than sign up for another subscription (because I'm already paying for a VPS anyway).\u003C/li>\n\u003Cli>I want to control how the comments on my website are displayed. I quite like my website design and I don't want a generic comment box below my posts.\u003C/li>\n\u003Cli>The service should respect the privacy of the people using my website.\u003C/li>\n\u003Cli>There should be an option to comment without setting up an account with the service.\u003C/li>\n\u003C/ul>\n\u003Cp>While looking around for how other people integrated comments into their static websites, I found a nice \u003Ca href=\"https://averagelinuxuser.com/static-website-commenting/\" rel=\"nofollow noopener noreferrer\">blog post from Average Linux User\u003C/a> which compares a few popular commenting systems.\nUnfortunately, most systems either are not very privacy-friendly, cost money or store the comments as comments on Github issues..?\nAfter looking through the options I decided to use this opportunity to write my own commenting system and dabble with the Go programming language.\u003C/p>\n\u003Ch2>Writing a commenting API in Go\u003C/h2>\n\u003Cp>First thing first, if you want to take a look at the code, check out the \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">Github repo\u003C/a>.\u003C/p>\n\u003Cp>I decided to write the commenting system in Go because I have been looking for an excuse to practice Go for a while, and this seemed like the perfect fit. It is a small CRUD app, consisting of a storage component, an API component and a small event component in the middle to easily compose the functionality I want.\u003C/p>\n\u003Cp>Currently, it supports the following functionality:\u003C/p>\n\u003Cul>\n\u003Cli>Listing all comments (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Listing all comments for a specified page (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Posting comments through the API\u003C/li>\n\u003Cli>A simple admin dashboard that lists all comments and allows the admin to delete them\u003C/li>\n\u003Cli>Email notifications when someone comments\u003C/li>\n\u003Cli>Email notifications when someone replies to your comment\u003C/li>\n\u003Cli>SQLite storage for comments\u003C/li>\n\u003C/ul>\n\u003Cp>The code is built in a way to make it easy to customise the features.\nFor example to disable features like the email reply notifications you can just \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/main.go#L52\" rel=\"nofollow noopener noreferrer\">comment out the line in the main.go\u003C/a> file that registers that hook.\u003C/p>\n\u003Cp>To write custom hooks that get executed when a new comment gets submitted or one gets deleted, just implement the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/event/handler.go\" rel=\"nofollow noopener noreferrer\">Handler\u003C/a> interface and register it in the main method.\u003C/p>\n\u003Cp>You can also easily add other storage options like databases or file storage by implementing the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/model/store.go\" rel=\"nofollow noopener noreferrer\">Store and SubscribtionStore\u003C/a> interfaces.\u003C/p>\n\u003Ch2>Can it be used in production? 🚗💨\u003C/h2>\n\u003Cp>I currently use it on this website! Go test it out (I might delete the comments if they are rude though 🤔).\u003C/p>\n\u003Cp>In all seriousness, I would not use it for a website where the comments are critical. But for a personal blog or similar, I don't see why not.\u003C/p>\n\u003Cp>If you want to host your own version, there is a Dockerfile available. If you decide to integrate this into your website, please comment below, ping me \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> or shoot me an email \u003Ca href=\"mailto:hey@tiim.ch\">hey@tiim.ch\u003C/a>, I would love to check it out.\u003C/p>","blog/2022-07-12-first-go-project-commenting-api","bff14052-4f3f-4dcb-bcee-155ae1c6b09e",["Date","2022-07-12T00:00:00.000Z"],["Date","2022-07-08T16:24:37.766Z"],[8],null,"First Go Project: A Jam-stack Commenting API",true,["Date","2022-11-23T21:42:29.000Z"],"I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!","/assets/2022-07-first-go-project-commenting-api.png",[15,16,17,18,19],"go","web-api","project","tiim.ch","indiego","\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>",[15,19,17,18,16],"article","blog",[25,33,40,47,52,58,63,68,75,81,86],{"id":26,"type":27,"replyTo":28,"timestamp":29,"page":3,"url":30,"content":31,"name":32},"31ec5f44-15b2-498a-890d-350e38b9a83e","webmention","","2023-08-02T09:10:04Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...","Tim Bachmann",{"id":34,"type":35,"replyTo":28,"timestamp":36,"page":3,"url":37,"content":38,"name":39},"6d792d24-ba58-4408-83a4-3583667ff4ad","comment","2023-07-09T19:25:02Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#6d792d24-ba58-4408-83a4-3583667ff4ad","Heya just saw your post on Reddit about this comment feature, didn't want to leave without using it ^^. Nicely done!","Anonymous",{"id":41,"type":35,"replyTo":42,"timestamp":43,"page":3,"url":44,"content":45,"name":46},"99dd9ccf-5349-4f41-9553-67986e1a1074","1c8ba0da-10df-4a7a-b067-55875441de2d","2022-12-07T23:01:37Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#99dd9ccf-5349-4f41-9553-67986e1a1074","You are right, there is not any documentation in the readme yet. Although hopefully, I will work on that soon. I'm in the middle of refactoring the project.\n\nTo query the comments there are two rest endpoints [\"/comment\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L41-L71) and [\"/comment/:page\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L73-L107) which return all comments or the comments for a specific page. The comments are loaded from this API endpoint when the site is generated.\n\nTo display the comments without rebuilding the site, new comments are fetched in the browser with the \"?since=\u003Ctime-of-last-build>\" query parameter.","Tiim",{"id":42,"type":35,"replyTo":28,"timestamp":48,"page":3,"url":49,"content":50,"name":51},"2022-12-07T22:33:44Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#1c8ba0da-10df-4a7a-b067-55875441de2d","Good stuff. Always nice to see a site supporting comments and/or Webmentions. Maybe I missed it in the readme but I am curious as to how one queries the API for comments. Do you pull in the comments from the database when you generate the site?","Poorchop",{"id":53,"type":27,"replyTo":28,"timestamp":54,"page":3,"url":55,"content":56,"name":57},"e42756e4-d5a5-4727-8c58-434d285b7ab3","2022-11-27T22:31:58Z","https://brid.gy/repost/twitter/tiimb/1546801590593638400/1546801615264415745","I published a new blog post:\nFirst Go Project: A jam-stack Commenting API\ntiim.ch/blog/2022-07-1…\n#golang #jamstack #API","Golang Bot",{"id":59,"type":27,"replyTo":28,"timestamp":60,"page":3,"url":61,"content":62,"name":32},"b8d3ae8b-0059-4379-8c35-30c97269908f","2022-11-21T22:19:23Z","https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","This site now supports sending and receiving webmentions and surfacing structured data using microformats2.",{"id":64,"type":35,"replyTo":28,"timestamp":65,"page":3,"url":66,"content":67,"name":67},"171e3444-f1d0-492d-8bc7-c0a133a41783","2022-07-18T08:44:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#171e3444-f1d0-492d-8bc7-c0a133a41783","hola",{"id":69,"type":35,"replyTo":70,"timestamp":71,"page":3,"url":72,"content":73,"name":74},"5875bba1-e69b-467a-bab5-23ef4160d257","621574fd-eea2-48d6-87c8-aebd0f05f1aa","2022-07-13T21:06:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#5875bba1-e69b-467a-bab5-23ef4160d257","And a polite reply","polite",{"id":76,"type":35,"replyTo":28,"timestamp":77,"page":3,"url":78,"content":79,"name":80},"8494c653-ef37-47a2-ae1b-00d00e4815a9","2022-07-13T18:31:31Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#8494c653-ef37-47a2-ae1b-00d00e4815a9","Pretty cool dudez","somGuy",{"id":70,"type":35,"replyTo":28,"timestamp":82,"page":3,"url":83,"content":84,"name":85},"2022-07-12T21:40:39Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#621574fd-eea2-48d6-87c8-aebd0f05f1aa","This is a rude comment ;)","rude",{"id":87,"type":35,"replyTo":28,"timestamp":88,"page":3,"url":89,"content":90,"name":91},"fb6278ae-e48c-4397-ba29-bec4e5cb3a57","2022-07-12T13:30:14Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#fb6278ae-e48c-4397-ba29-bec4e5cb3a57","Good job dude!","wdup","2023-09-02T19:26:59Z",{"html":94,"slug":95,"uuid":96,"date":97,"created":98,"published":10,"abstract":99,"tags":100,"links":-1,"type":22,"cover_image":-1,"description":28,"folder":101},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2022-09-27-sveltekit-ssr-with-urql.html b/blog/2022-09-27-sveltekit-ssr-with-urql.html new file mode 100644 index 00000000..5563978d --- /dev/null +++ b/blog/2022-09-27-sveltekit-ssr-with-urql.html @@ -0,0 +1,428 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + SvelteKit Server-Side Rendering (SSR) with @urql/svelte - Tim Bachmann + + +
+ + +
+
+ +
SvelteKit Server-Side Rendering (SSR) with @urql/svelte +
+ +

SvelteKit Server-Side Rendering (SSR) with @urql/svelte

+ + +

graphql + ssr + sveltekit + urql +

+ +
by
+ published on +
+ + +

In this blog post, I will explain why server-side rendering with the urql GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.

+

Server-side rendering (SSR) is one of the great features of SvelteKit. I will try to keep this blog post short and will therefore not explain what server-side rendering is and why you should take advantage of it (you really should!). If you want to know more about SSR you can take a look at this article: A Deep Dive into Server-Side Rendering (SSR) in JavaScript.

+

Background - SSR in SvelteKit

+

SvelteKit implements SSR by providing a load function for every layout and page component. If a page or layout needs to perform some asynchronous operation, this should be done inside of this load function. SvelteKit executes this function asynchronously on the server side as well as on the client side and the return value of this function is assigned to the data prop of the associated component. Usually, this asynchronous operation is loading data from an external service, like in the case of this blog post a GraphQL server. +You can of course load data directly in the component, but SvelteKit will not wait for this to complete when doing SSR, and the resulting HTML will not include the loaded data.

+

Background - @urql/svelte

+

The urql library allows us to easily issue GraphQL queries and mutations. Some of the functionality it has to make our lives easier include:

+
    +
  • Reloading a query when a query variable changes
  • +
  • Reloading a query after a mutation that touches the same data as the query
  • +
+

We want to keep these features, even when using urql when doing SSR.

+

The Problem

+

When implementing SSR in my project, I ran into two problems. I couldn't find any documentation or any articles solving them, so I decided to write down my solutions to those problems in this blog post.

+

Problem 1 - Svelte and urql Reactivity

+

Let's say we have the following load function, which executes a GraphQL query to load a list of red cars:

+
// src/routes/car/+page.js
+
+/** @type {import('./$types').PageLoad} */
+export function load(event) {
+  const client = createClient({
+    url: config.url,
+    fetch: event.fetch,
+  });
+
+  const carColor = "red";
+
+  const cars = client
+    .query(carsQuery, {
+      color: carColor,
+    })
+    .toPromise()
+    .then((c) => c.data?.car);
+
+  return {
+    cars,
+  };
+}
+
+

This example uses the urql method client.query to start a query to get us a list of cars with a red colour (The GraphQL query is not shown but the exact query is not important for this example). +The client gets a special fetch function from the event which has a few nice properties, like preventing a second network request on the client side if that same request was just issued on the server-side.

+

Since the query code is now located in the load function and not in a svelte component, there is no way to easily change the carColor and have urql automatically reload the query. The only way to change the variable is to set the value as a query parameter and read that from the event argument. This however means that we have to refresh the whole page just to reload this query.

+

The other thing urql does for us, reloading the query when we do a mutation on the same data, will not work with the above code either.

+

The solution: A query in the load function and a query in the component

+

To fix those two drawbacks we have to add the same query as in the load function to our component code as well. Unfortunately, this means when a user loads the page, it sends a request from the client side, even though the same request got sent from the server side already.

+

I created a small wrapper function queryStoreInitialData that creates the query inside of the component and intelligently switches from the (possibly stale) data from the load function to the new data. Using this wrapper, the page or layout might look as follows:

+
<script>
+  import { queryStoreInitialData } from "@/lib/gql-client"; // The helper function mentioned above
+  import { getContextClient } from "@urql/svelte";
+  import { carsQuery } from "./query"; // The query
+
+  export let data;
+
+  $: gqlStore = queryStoreInitialData(
+    {
+      client: getContextClient(),
+      query: carsQuery,
+    },
+    data.cars
+  );
+  $: cars = $gqlStore?.data?.car;
+</script>
+
+<div>
+  <pre>
+    {JSON.stringify(cars, null, 2)}
+  </pre>
+</div>
+
+
    +
  1. The native queryStore function gets replaced with the wrapper function.
  2. +
  3. The initial value of the query is supplied to the wrapper
  4. +
+

Unfortunately, we can not return the query result from the load function directly like this:

+
const result = await client.query(cars, {}).toPromise();
+
+return {
+  cars: toInitialValue(result),
+};
+
+

This results in the following error:

+
Cannot stringify a function (data.events.operation.context.fetch)
+Error: Cannot stringify a function (data.events.operation.context.fetch)
+    at render_response (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/render.js:181:20)
+    at runMicrotasks (<anonymous>)
+    at processTicksAndRejections (node:internal/process/task_queues:96:5)
+    at async render_page (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/index.js:276:10)
+    at async resolve (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:232:17)
+    at async respond (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:284:20)
+    at async file:///app/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:406:22
+
+

This is because the query result contains data that is not serializable. +To fix this I created the toInitialValue function, which deletes all non-serializable elements from the result. The load function now looks like follows;

+
// src/routes/car/+page.js
+import { createServerClient, toInitialValue } from "@/lib/gql-client";
+import { parse } from "cookie";
+import { carsQuery } from "./query";
+
+/** @type {import('./$types').PageServerLoad} */
+export const load = async (event) => {
+  const client = createClient({
+    url: config.url,
+    fetch: event.fetch,
+  });
+
+  const result = await client.query(cars, {}).toPromise();
+
+  return {
+    cars: toInitialValue(result),
+  };
+};
+
+

Problem 2 - Authentication

+

We will look at the same load function as #Problem 1 - Svelte and urql Reactivity: the function creates a urql client with the fetch function from the event object and uses this client to send a query.

+

Sometimes however the GraphQL API requires authentication in the form of a cookie to allow access.

+

Unfortunately, the fetch function that we get from the load event will only pass the cookies on if the requested domain is the same as the base domain or a more specific subdomain of it. This means if your SvelteKit site runs on example.com and your GraphQL server runs on gql.example.com then the cookies will get forwarded and everything is fine. This however is, in my experience, often not the case. Either you might use an external service for your GraphQL API or you host it yourself and want to use its internal domain.

+

The only way to pass the cookies on to the GraphQL server, in this case, is by manually setting the cookie header when creating the urql client. This however forces us to use the server-only load function, as we do not have access to the cookie header in the normal load function.

+

The new code now looks like this:

+
// /src/routes/car/+page.server.js
+
+/** @type {import('./$types').PageServerLoad} */
+export function load(event) {
+  const client = createClient({
+    url: config.url,
+    fetch,
+    fetchOptions: {
+      credentials: "include",
+      headers: {
+        // inject the cookie header
+        // FIXME: change the cookie name
+        Cookie: `gql-session=${event.cookies.get("gql-session")}`,
+      },
+    },
+  });
+
+  const cars = client.query(carsQuery, {}).toPromise();
+
+  return {
+    cars: toInitialValue(result),
+  };
+}
+
+

To keep the size of the load functions across my codebase smaller I created a small wrapper function createServerClient:

+
// /src/routes/car/+page.server.js
+
+/** @type {import('./$types').PageServerLoad} */
+export function load(event) {
+  const client = createServerClient(event.cookies);
+
+  const cars = client.query(carsQuery, {}).toPromise();
+
+  return {
+    cars: toInitialValue(result),
+  };
+}
+
+

The Code

+

Below you can find the three functions createServerClient, queryStoreInitialData and toInitialValue that we used above:

+
// /src/lib/gql-client.js
+
+import { browser } from "$app/environment";
+import { urls } from "@/config";
+import { createClient, queryStore } from "@urql/svelte";
+import { derived, readable } from "svelte/store";
+
+/**
+ * Helper function to create an urql client for a server-side-only load function
+ *
+ *
+ * @param {import('@sveltejs/kit').Cookies} cookies
+ * @returns
+ */
+export function createServerClient(cookies) {
+  return createClient({
+    // FIXME: adjust your graphql url
+    url: urls.gql,
+    fetch,
+    // FIXME: if you don't need to authenticate, delete the following object:
+    fetchOptions: {
+      credentials: "include",
+      headers: {
+        // FIXME: if you want to set a cookie adjust the cookie name
+        Cookie: `gql-session=${cookies.get("gql-session")}`,
+      },
+    },
+  });
+}
+
+/**
+ * Helper method to send a GraphQL query but use the data from the SvelteKit load function initially.
+ *
+ *
+ * @param {any} queryArgs
+ * @param {any} initialValue
+ * @returns
+ */
+export function queryStoreInitialData(queryArgs, initialValue) {
+  if (!initialValue || (!initialValue.error && !initialValue.data)) {
+    throw new Error("No initial value from server");
+  }
+
+  let query = readable({ fetching: true });
+  if (browser) {
+    query = queryStore(queryArgs);
+  }
+
+  return derived(query, (value, set) => {
+    if (value.fetching) {
+      set({ ...initialValue, source: "server", fetching: true });
+    } else {
+      set({ ...value, source: "client" });
+    }
+  });
+}
+
+/**
+ * Make the result object of a urql query serialisable.
+ *
+ *
+ * @template T
+ * @param {Promise<import('@urql/svelte').OperationResult<T, any >>|import('@urql/svelte').OperationResult<T, any >} result
+ * @returns {Promise<{fetching:false, error: undefined | {name?: string, message?: string; graphQLErrors?: any[]; networkError?: Error; response?: any;}, data: T|undefined}>}
+ */
+export async function toInitialValue(result) {
+  const { error, data } = await result;
+
+  // required to turn class array into array of javascript objects
+  const errorObject = error ? {} : undefined;
+  if (errorObject) {
+    console.warn(error);
+    errorObject.graphQLErrors = error?.graphQLErrors?.map((e) => ({ ...e }));
+    errorObject.networkError = { ...error?.networkError };
+    errorObject.response = { value: "response omitted" };
+  }
+
+  return {
+    fetching: false,
+    error: { ...error, ...errorObject },
+    data,
+  };
+}
+
+

Link to the Gist

+

End remarks

+

Even though I think this solution is not too bad, I wish @urql/svelte would implement a better way to handle SSR with sveltekit. I posted a question on the urql GitHub discussions board, but I have not gotten any response yet.

+
+Info

This article was written with @svelte/kit version 1.0.0-next.499 and @urql/svelte version 3.0.1. +I will try to update this article as I update my codebase to newer versions.

+
+

If this post helped you, or you found a better or different way to solve SSR with urql, please let me know in the comments, write me an email or tag me on twitter @TiimB.

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

1 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
  • + +

    Farin

    + 2023-02-05 12:02:51 AM + + + +
    + +

    Hi + +inspiring article. Anyway, inspired with it I tried to find more seamless integration - get SSR rendered queries but keep original interface. + +You may be interested in my approach, it's dropped in discussion you open +https://github.com/urql-graphql/urql/discussions/2703

    +
  • +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2022-09-27-sveltekit-ssr-with-urql/__data.json b/blog/2022-09-27-sveltekit-ssr-with-urql/__data.json new file mode 100644 index 00000000..ca7e4168 --- /dev/null +++ b/blog/2022-09-27-sveltekit-ssr-with-urql/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":33},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"aliases":7,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"content_tags":13,"abstract":18,"tags":19,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":32},"\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\" rel=\"nofollow noopener noreferrer\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>\n\u003Cp>Server-side rendering (SSR) is one of the great features of SvelteKit. I will try to keep this blog post short and will therefore not explain what server-side rendering is and why you should take advantage of it \u003Cem>(you really should!)\u003C/em>. If you want to know more about SSR you can take a look at this article: \u003Ca href=\"https://towardsdev.com/server-side-rendering-srr-in-javascript-a1b7298f0d04\" rel=\"nofollow noopener noreferrer\">A Deep Dive into Server-Side Rendering (SSR) in JavaScript\u003C/a>.\u003C/p>\n\u003Ch2>Background - SSR in SvelteKit\u003C/h2>\n\u003Cp>SvelteKit implements SSR by providing a \u003Ca href=\"https://kit.svelte.dev/docs/load\" rel=\"nofollow noopener noreferrer\">\u003Ccode>load\u003C/code> function\u003C/a> for every layout and page component. If a page or layout needs to perform some asynchronous operation, this should be done inside of this load function. SvelteKit executes this function asynchronously on the server side as well as on the client side and the return value of this function is assigned to the \u003Ccode>data\u003C/code> prop of the associated component. Usually, this asynchronous operation is loading data from an external service, like in the case of this blog post a GraphQL server.\nYou can of course load data directly in the component, but SvelteKit will not wait for this to complete when doing SSR, and the resulting HTML will not include the loaded data.\u003C/p>\n\u003Ch2>Background - @urql/svelte\u003C/h2>\n\u003Cp>The urql library allows us to easily issue GraphQL queries and mutations. Some of the functionality it has to make our lives easier include:\u003C/p>\n\u003Cul>\n\u003Cli>Reloading a query when a query variable changes\u003C/li>\n\u003Cli>Reloading a query after a mutation that touches the same data as the query\u003C/li>\n\u003C/ul>\n\u003Cp>We want to keep these features, even when using urql when doing SSR.\u003C/p>\n\u003Ch2>The Problem\u003C/h2>\n\u003Cp>When implementing SSR in my project, I ran into two problems. I couldn't find any documentation or any articles solving them, so I decided to write down my solutions to those problems in this blog post.\u003C/p>\n\u003Ch3>Problem 1 - Svelte and urql Reactivity\u003C/h3>\n\u003Cp>Let's say we have the following load function, which executes a GraphQL query to load a list of red cars:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\n\n/** @type {import('./$types').PageLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const carColor = \"red\";\n\n const cars = client\n .query(carsQuery, {\n color: carColor,\n })\n .toPromise()\n .then((c) => c.data?.car);\n\n return {\n cars,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>This example uses the urql method \u003Ccode>client.query\u003C/code> to start a query to get us a list of cars with a red colour (The GraphQL query is not shown but the exact query is not important for this example).\nThe client gets a \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">special fetch function\u003C/a> from the event which has a few nice properties, like preventing a second network request on the client side if that same request was just issued on the server-side.\u003C/p>\n\u003Cp>Since the query code is now located in the load function and not in a svelte component, there is no way to easily change the \u003Ccode>carColor\u003C/code> and have urql automatically reload the query. The only way to change the variable is to set the value as a query parameter and read that from the \u003Ccode>event\u003C/code> argument. This however means that we have to refresh the whole page just to reload this query.\u003C/p>\n\u003Cp>The other thing urql does for us, reloading the query when we do a mutation on the same data, will not work with the above code either.\u003C/p>\n\u003Ch3>The solution: A query in the load function and a query in the component\u003C/h3>\n\u003Cp>To fix those two drawbacks we have to add the same query as in the load function to our component code as well. Unfortunately, this means when a user loads the page, it sends a request from the client side, even though the same request got sent from the server side already.\u003C/p>\n\u003Cp>I created a small wrapper function \u003Ccode>queryStoreInitialData\u003C/code> that creates the query inside of the component and intelligently switches from the (possibly stale) data from the load function to the new data. Using this wrapper, the page or layout might look as follows:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-svelte\"><script>\n import { queryStoreInitialData } from \"@/lib/gql-client\"; // The helper function mentioned above\n import { getContextClient } from \"@urql/svelte\";\n import { carsQuery } from \"./query\"; // The query\n\n export let data;\n\n $: gqlStore = queryStoreInitialData(\n {\n client: getContextClient(),\n query: carsQuery,\n },\n data.cars\n );\n $: cars = $gqlStore?.data?.car;\n</script>\n\n<div>\n <pre>\n {JSON.stringify(cars, null, 2)}\n </pre>\n</div>\n\u003C/code>\u003C/pre>\n\u003Col>\n\u003Cli>The native \u003Ccode>queryStore\u003C/code> function gets replaced with the wrapper function.\u003C/li>\n\u003Cli>The initial value of the query is supplied to the wrapper\u003C/li>\n\u003C/ol>\n\u003Cp>Unfortunately, we can not return the query result from the load function directly like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const result = await client.query(cars, {}).toPromise();\n\nreturn {\n cars: toInitialValue(result),\n};\n\u003C/code>\u003C/pre>\n\u003Cp>This results in the following error:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-stacktrace\">Cannot stringify a function (data.events.operation.context.fetch)\nError: Cannot stringify a function (data.events.operation.context.fetch)\n at render_response (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/render.js:181:20)\n at runMicrotasks (<anonymous>)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async render_page (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/index.js:276:10)\n at async resolve (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:232:17)\n at async respond (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:284:20)\n at async file:///app/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:406:22\n\u003C/code>\u003C/pre>\n\u003Cp>This is because the query result contains data that is not serializable.\nTo fix this I created the \u003Ccode>toInitialValue\u003C/code> function, which deletes all non-serializable elements from the result. The load function now looks like follows;\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\nimport { createServerClient, toInitialValue } from \"@/lib/gql-client\";\nimport { parse } from \"cookie\";\nimport { carsQuery } from \"./query\";\n\n/** @type {import('./$types').PageServerLoad} */\nexport const load = async (event) => {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const result = await client.query(cars, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n};\n\u003C/code>\u003C/pre>\n\u003Ch3>Problem 2 - Authentication\u003C/h3>\n\u003Cp>We will look at the same \u003Ccode>load\u003C/code> function as #Problem 1 - Svelte and urql Reactivity: the function creates a urql client with the fetch function from the event object and uses this client to send a query.\u003C/p>\n\u003Cp>Sometimes however the GraphQL API requires authentication in the form of a cookie to allow access.\u003C/p>\n\u003Cp>Unfortunately, the \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">fetch function that we get from the load event\u003C/a> will only pass the cookies on if the requested domain is the same as the base domain or a more specific subdomain of it. This means if your SvelteKit site runs on \u003Ccode>example.com\u003C/code> and your GraphQL server runs on \u003Ccode>gql.example.com\u003C/code> then the cookies will get forwarded and everything is fine. This however is, in my experience, often not the case. Either you might use an external service for your GraphQL API or you host it yourself and want to use its internal domain.\u003C/p>\n\u003Cp>The only way to pass the cookies on to the GraphQL server, in this case, is by manually setting the cookie header when creating the urql client. This however forces us to use the server-only load function, as we do not have access to the cookie header in the normal load function.\u003C/p>\n\u003Cp>The new code now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch,\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // inject the cookie header\n // FIXME: change the cookie name\n Cookie: `gql-session=${event.cookies.get(\"gql-session\")}`,\n },\n },\n });\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>To keep the size of the load functions across my codebase smaller I created a small wrapper function \u003Ccode>createServerClient\u003C/code>:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createServerClient(event.cookies);\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Ch2>The Code\u003C/h2>\n\u003Cp>Below you can find the three functions \u003Ccode>createServerClient\u003C/code>, \u003Ccode>queryStoreInitialData\u003C/code> and \u003Ccode>toInitialValue\u003C/code> that we used above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/lib/gql-client.js\n\nimport { browser } from \"$app/environment\";\nimport { urls } from \"@/config\";\nimport { createClient, queryStore } from \"@urql/svelte\";\nimport { derived, readable } from \"svelte/store\";\n\n/**\n * Helper function to create an urql client for a server-side-only load function\n *\n *\n * @param {import('@sveltejs/kit').Cookies} cookies\n * @returns\n */\nexport function createServerClient(cookies) {\n return createClient({\n // FIXME: adjust your graphql url\n url: urls.gql,\n fetch,\n // FIXME: if you don't need to authenticate, delete the following object:\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // FIXME: if you want to set a cookie adjust the cookie name\n Cookie: `gql-session=${cookies.get(\"gql-session\")}`,\n },\n },\n });\n}\n\n/**\n * Helper method to send a GraphQL query but use the data from the SvelteKit load function initially.\n *\n *\n * @param {any} queryArgs\n * @param {any} initialValue\n * @returns\n */\nexport function queryStoreInitialData(queryArgs, initialValue) {\n if (!initialValue || (!initialValue.error && !initialValue.data)) {\n throw new Error(\"No initial value from server\");\n }\n\n let query = readable({ fetching: true });\n if (browser) {\n query = queryStore(queryArgs);\n }\n\n return derived(query, (value, set) => {\n if (value.fetching) {\n set({ ...initialValue, source: \"server\", fetching: true });\n } else {\n set({ ...value, source: \"client\" });\n }\n });\n}\n\n/**\n * Make the result object of a urql query serialisable.\n *\n *\n * @template T\n * @param {Promise<import('@urql/svelte').OperationResult<T, any >>|import('@urql/svelte').OperationResult<T, any >} result\n * @returns {Promise<{fetching:false, error: undefined | {name?: string, message?: string; graphQLErrors?: any[]; networkError?: Error; response?: any;}, data: T|undefined}>}\n */\nexport async function toInitialValue(result) {\n const { error, data } = await result;\n\n // required to turn class array into array of javascript objects\n const errorObject = error ? {} : undefined;\n if (errorObject) {\n console.warn(error);\n errorObject.graphQLErrors = error?.graphQLErrors?.map((e) => ({ ...e }));\n errorObject.networkError = { ...error?.networkError };\n errorObject.response = { value: \"response omitted\" };\n }\n\n return {\n fetching: false,\n error: { ...error, ...errorObject },\n data,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Ca href=\"https://gist.github.com/Tiim/1adeb4d74ce7ae09d0d0aa4176a6195d\" rel=\"nofollow noopener noreferrer\">Link to the Gist\u003C/a>\u003C/p>\n\u003Ch2>End remarks\u003C/h2>\n\u003Cp>Even though I think this solution is not too bad, I wish @urql/svelte would implement a better way to handle SSR with sveltekit. I posted a \u003Ca href=\"https://github.com/FormidableLabs/urql/discussions/2703\" rel=\"nofollow noopener noreferrer\">question on the urql GitHub discussions board\u003C/a>, but I have not gotten any response yet.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>This article was written with \u003Ccode>@svelte/kit\u003C/code> version \u003Ccode>1.0.0-next.499\u003C/code> and \u003Ccode>@urql/svelte\u003C/code> version \u003Ccode>3.0.1\u003C/code>.\nI will try to update this article as I update my codebase to newer versions.\u003C/p>\n\u003C/blockquote>\n\u003Cp>If this post helped you, or you found a better or different way to solve SSR with urql, please let me know in the comments, write me an email or tag me on twitter \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a>.\u003C/p>","blog/2022-09-27-sveltekit-ssr-with-urql","1e223cab-bca2-4b3b-a75a-71f158c90cba",["Date","2022-09-26T00:00:00.000Z"],["Date","2022-09-26T08:55:23.886Z"],[8],null,"SvelteKit Server-Side Rendering (SSR) with @urql/svelte",true,"Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.","https://i.imgur.com/5DBIbbT.png",[14,15,16,17],"urql","sveltekit","SSR","graphql","\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>",[17,20,15,14],"ssr","article","blog",[24],{"id":25,"type":26,"replyTo":27,"timestamp":28,"page":3,"url":29,"content":30,"name":31},"a38babef-2c4c-41e6-a748-8723b6cc34ef","comment","","2023-02-05T00:02:51Z","https://tiim.ch/blog/2022-09-27-sveltekit-ssr-with-urql#a38babef-2c4c-41e6-a748-8723b6cc34ef","Hi\n\ninspiring article. Anyway, inspired with it I tried to find more seamless integration - get SSR rendered queries but keep original interface. \n\nYou may be interested in my approach, it's dropped in discussion you open \nhttps://github.com/urql-graphql/urql/discussions/2703","Farin","2023-09-02T19:26:59Z",{"html":34,"slug":35,"uuid":36,"date":37,"created":38,"published":10,"abstract":39,"tags":40,"links":-1,"type":21,"cover_image":-1,"description":27,"folder":41},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2022-12-indiewebifying-my-website-part-1.html b/blog/2022-12-indiewebifying-my-website-part-1.html new file mode 100644 index 00000000..97a93e44 --- /dev/null +++ b/blog/2022-12-indiewebifying-my-website-part-1.html @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + IndieWebifying my Website Part 1 - Microformats and Webmentions - Tim Bachmann + + +
+ + +
+
+ +
IndieWebifying my Website Part 1 - Microformats and Webmentions +
+ +

IndieWebifying my Website Part 1 - Microformats and Webmentions

+ + +

go + indiego + indieweb + mf2 + tiim.ch + webmentions +

+ +
by
+ published on +last updated on
+ + +

A few weeks ago, I stumbled on one of Jamie Tanna's blog posts about microformats2 by accident. That is when I first learned about the wonderful world of the IndieWeb. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?

+

The IndieWeb

+

I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.

+

The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.

+

The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.

+

The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.

+

Some of the standards in the IndieWeb include:

+
    +
  • Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.
  • +
  • Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.
  • +
  • IndieAuth, an OAuth2-based way to log in using only your domain name.
  • +
+

The implementation on my website

+

As explained in my earlier post First Go Project: A Jam-stack Commenting API, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.

+

Making the website machine-readable with Microformats

+

As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called h-entry. By adding the h-entry class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as p-name, p-author or dt-published.

+

While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.

+

Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.

+

Accepting comments and other interactions via Webmentions

+

The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.

+

In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.

+

Since I already have a small custom service running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions

+

It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database. +I found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.

+

I have tested my webmentions implementation using webmention.rocks, but I would appreciate it if you left me a mention as well 😃

+

Publishing short-form content such as replies, likes and bookmarks: A notes post type

+

The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called notes. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.

+

I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: full-rss.xml. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.

+

Notifying referenced websites: Sending Webmentions

+

Sending webmentions was easy compared to receiving webmentions:

+

On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.

+

Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library gocron for scheduling the regular intervals, gofeed for parsing the RSS feed and webmention for extracting links and sending webmentions.

+

In the future: IndieAuth

+

The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.

+

Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.

+
+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

8 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
  • + +

    Evgeny Kuznetsov

    + 2023-09-02 7:26:59 PM + +

    Mentioned this post

    +
    + +

    +
  • + +

    Tim Bachmann

    + 2023-08-02 9:10:03 AM + +

    Mentioned this post

    +
    + +

    I blogged about creating a comment system for my website a while ago, +and later how I implemented webmentions into that same project. +Since then this little go program has grown quite a bit, and it has turned into a modular platform +that supports quite a few technologies:...

    +
  • + +

    Tim Bachmann

    + 2022-12-09 10:49:06 AM + +

    Mentioned this post

    +
    + +

    Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.

    +
  • + +

    https://martymcgui.re/

    + 2022-12-05 8:41:07 AM + +

    Mentioned this post

    +
    + +

    ★ Liked https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1

    +
  • + +

    Jimmy Lipham

    + 2022-11-27 10:32:27 PM + +

    Mentioned this post

    +
    + +

    +
  • + +

    Golang Smart Bot

    + 2022-11-27 10:31:57 PM + +

    Mentioned this post

    +
    + +

    I published a new blog post: +IndieWebifying my Website Part 1 - Microformats and Webmentions +tiim.ch/blog/2022-12-i… +#indieweb #microformats #webmentions #golang

    +
  • + +

    Webmention Rocks!

    + 2022-11-15 12:47:35 PM + +

    Mentioned this post

    +
    + +

    This test verifies that you accept a Webmention request that contains a valid source and target URL. To pass this test, your Webmention endpoint must return either HTTP 200, 201 or 202 along with the appropriate headers. +If your endpoint returns HTTP 201, then it MUST also return a Location header. If it returns HTTP 200 or 202, then it MUST NOT include a Location header.

    +
  • + +

    Jamie Tanna

    + 2022-11-13 8:34:12 AM + +

    Mentioned this post

    +
    + +

    Liked +IndieWebifying my Website Part 1 - Microformats and Webmentions +Post detailsThis site now supports sending and receiving webmentions and surfacing structured data using microformats2. https://i.imgur.com/FpgIBxI.jpg

    +
  • +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2022-12-indiewebifying-my-website-part-1/__data.json b/blog/2022-12-indiewebifying-my-website-part-1/__data.json new file mode 100644 index 00000000..bf7e4da1 --- /dev/null +++ b/blog/2022-12-indiewebifying-my-website-part-1/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":76},{"html":2,"slug":3,"uuid":4,"date":5,"aliases":6,"title":8,"published":9,"modified":10,"description":11,"cover_image":12,"content_tags":13,"syndication":20,"abstract":22,"tags":23,"links":-1,"type":26,"folder":27,"comments":28,"latestComment":33},"\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\" rel=\"nofollow noopener noreferrer\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\" rel=\"nofollow noopener noreferrer\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>\n\u003Ch2>The IndieWeb\u003C/h2>\n\u003Cp>I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.\u003C/p>\n\u003Cp>The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.\u003C/p>\n\u003Cp>The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.\u003C/p>\n\u003Cp>The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.\u003C/p>\n\u003Cp>Some of the standards in the IndieWeb include:\u003C/p>\n\u003Cul>\n\u003Cli>Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.\u003C/li>\n\u003Cli>Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.\u003C/li>\n\u003Cli>IndieAuth, an OAuth2-based way to log in using only your domain name.\u003C/li>\n\u003C/ul>\n\u003Ch2>The implementation on my website\u003C/h2>\n\u003Cp>As explained in my earlier post \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">First Go Project: A Jam-stack Commenting API\u003C/a>, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.\u003C/p>\n\u003Ch3>Making the website machine-readable with Microformats\u003C/h3>\n\u003Cp>As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called \u003Ca href=\"http://microformats.org/wiki/h-entry\" rel=\"nofollow noopener noreferrer\">h-entry\u003C/a>. By adding the \u003Ccode>h-entry\u003C/code> class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as \u003Ccode>p-name\u003C/code>, \u003Ccode>p-author\u003C/code> or \u003Ccode>dt-published\u003C/code>.\u003C/p>\n\u003Cp>While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.\u003C/p>\n\u003Cp>Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.\u003C/p>\n\u003Ch3>Accepting comments and other interactions via Webmentions\u003C/h3>\n\u003Cp>The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.\u003C/p>\n\u003Cp>In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.\u003C/p>\n\u003Cp>Since I already have a \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">small custom service\u003C/a> running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions\u003C/p>\n\u003Cp>It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database.\nI found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.\u003C/p>\n\u003Cp>I have tested my webmentions implementation using \u003Ca href=\"https://webmention.rocks\" rel=\"nofollow noopener noreferrer\">webmention.rocks\u003C/a>, but I would appreciate it if you left me a mention as well 😃\u003C/p>\n\u003Ch3>Publishing short-form content such as replies, likes and bookmarks: A notes post type\u003C/h3>\n\u003Cp>The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">notes\u003C/a>. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.\u003C/p>\n\u003Cp>I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: \u003Ca href=\"https://tiim.ch/full-rss.xml\" rel=\"nofollow noopener noreferrer\">full-rss.xml\u003C/a>. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.\u003C/p>\n\u003Ch3>Notifying referenced websites: Sending Webmentions\u003C/h3>\n\u003Cp>Sending webmentions was easy compared to receiving webmentions:\u003C/p>\n\u003Cp>On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.\u003C/p>\n\u003Cp>Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library \u003Ca href=\"https://github.com/go-co-op/gocron\" rel=\"nofollow noopener noreferrer\">gocron\u003C/a> for scheduling the regular intervals, \u003Ca href=\"https://github.com/mmcdole/gofeed\" rel=\"nofollow noopener noreferrer\">gofeed\u003C/a> for parsing the RSS feed and \u003Ca href=\"https://willnorris.com/go/webmention\" rel=\"nofollow noopener noreferrer\">webmention\u003C/a> for extracting links and sending webmentions.\u003C/p>\n\u003Ch3>In the future: IndieAuth\u003C/h3>\n\u003Cp>The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.\u003C/p>\n\u003Cp>Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.\u003C/p>\u003Cdiv class=\"mf2\">\u003Cblockquote class=\"syndication\">This post is also on \u003Cul>\u003Cli>\u003Ca class=\"u-syndication\" href=\"https://news.indieweb.org/en\">news.indieweb.org\u003C/a>\u003C/li>\u003C/ul>\u003C/blockquote>\u003C/div>\n","blog/2022-12-indiewebifying-my-website-part-1","3b342241-c414-4670-bd22-03e13d6531b7",["Date","2022-11-12T10:55:14.000Z"],[7],null,"IndieWebifying my Website Part 1 - Microformats and Webmentions",true,["Date","2022-12-03T20:56:54.000Z"],"This site now supports sending and receiving webmentions and surfacing structured data using microformats2.","https://i.imgur.com/FpgIBxI.jpg",[14,15,16,17,18,19],"IndieWeb","Webmentions","mf2","tiim.ch","go","indiego",[21],"https://news.indieweb.org/en","\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>",[18,19,24,16,17,25],"indieweb","webmentions","article","blog",[29,36,42,47,53,58,64,70],{"id":30,"type":31,"replyTo":32,"timestamp":33,"page":3,"url":34,"content":32,"name":35},"41435fdd-5fd2-4175-b9ea-ef9ce0dec154","webmention","","2023-09-02T19:26:59Z","https://evgenykuznetsov.org/en/reactions/2022/like-337215655/","Evgeny Kuznetsov",{"id":37,"type":31,"replyTo":32,"timestamp":38,"page":3,"url":39,"content":40,"name":41},"68bd3601-4f00-42c1-b28c-1f2ef75ac851","2023-08-02T09:10:03Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...","Tim Bachmann",{"id":43,"type":31,"replyTo":32,"timestamp":44,"page":3,"url":45,"content":46,"name":41},"5f508a42-8b83-4c10-9f7e-9c1b80e23ab1","2022-12-09T10:49:06Z","https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting","Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.",{"id":48,"type":31,"replyTo":32,"timestamp":49,"page":3,"url":50,"content":51,"name":52},"dc8dbf30-ff1f-4e13-ba36-37f12666005c","2022-12-05T08:41:07Z","https://martymcgui.re/2022/12/05/033926/","★ Liked https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","https://martymcgui.re/",{"id":54,"type":31,"replyTo":32,"timestamp":55,"page":3,"url":56,"content":32,"name":57},"6f6c1f11-2ae0-41a4-b7d0-e343ef63aa52","2022-11-27T22:32:27Z","https://brid.gy/like/twitter/tiimb/1591417020557525003/48372745","Jimmy Lipham",{"id":59,"type":31,"replyTo":32,"timestamp":60,"page":3,"url":61,"content":62,"name":63},"5e1c4149-8fb9-48fb-b285-5efbd626b259","2022-11-27T22:31:57Z","https://brid.gy/repost/twitter/tiimb/1591417020557525003/1591418692008558592","I published a new blog post:\nIndieWebifying my Website Part 1 - Microformats and Webmentions\ntiim.ch/blog/2022-12-i…\n#indieweb #microformats #webmentions #golang","Golang Smart Bot",{"id":65,"type":31,"replyTo":32,"timestamp":66,"page":3,"url":67,"content":68,"name":69},"6e0bf830-1735-42a2-9aa2-ea4c40ab7a45","2022-11-15T12:47:35Z","https://webmention.rocks/receive/1/f0fa5421056e068fe902932ef98f6d71","This test verifies that you accept a Webmention request that contains a valid source and target URL. To pass this test, your Webmention endpoint must return either HTTP 200, 201 or 202 along with the appropriate headers.\nIf your endpoint returns HTTP 201, then it MUST also return a Location header. If it returns HTTP 200 or 202, then it MUST NOT include a Location header.","Webmention Rocks!",{"id":71,"type":31,"replyTo":32,"timestamp":72,"page":3,"url":73,"content":74,"name":75},"4e237d08-9f22-480e-a46e-8f40adf06c5e","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/rm8as/","Liked\nIndieWebifying my Website Part 1 - Microformats and Webmentions\nPost detailsThis site now supports sending and receiving webmentions and surfacing structured data using microformats2. https://i.imgur.com/FpgIBxI.jpg","Jamie Tanna",{"html":77,"slug":78,"uuid":79,"date":80,"created":81,"published":9,"abstract":82,"tags":83,"links":-1,"type":26,"cover_image":-1,"description":32,"folder":84},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2022-12-storj-cloudflare-image-hosting.html b/blog/2022-12-storj-cloudflare-image-hosting.html new file mode 100644 index 00000000..a92f3957 --- /dev/null +++ b/blog/2022-12-storj-cloudflare-image-hosting.html @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + Hosting Images with Storj and Cloudflare - Tim Bachmann + + +
+ + +
+
+ +
Hosting Images with Storj and Cloudflare +
+ +

Hosting Images with Storj and Cloudflare

+ + +

cdn + cloudflare + indieweb + storj +

+ +
by
+ published on +
+ + +

For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs TOS:

+
+

[...] Also, don't use Imgur to host image libraries you link to from elsewhere, content for your website, advertising, avatars, or anything else that turns us into your content delivery network.

+
+

Finally when I started indie-webifying my website, and was implementing the micropub protocol (which I will blog about at a later time), I decided that it was at the time to host the images on a platform that was meant to do that. I looked at a few storage providers such as cloudinary and S3 based object storage and landed on Storj.io, mostly because of the generous free tier, which should suffice for this little blog for quite a while.

+

One thing that bothered me slightly was that all storage providers I looked at charge for traffic. It's not the fact that it's an additional expense (if your not in the free tier anymore) that bothers me, but the fact that I don't have any control over how much this will cost me. In all likelihood this will never cost me anything since this blog has not much traffic, but if a post were to go viral (one can dream...), this could result in a surprise bill at the end of the month.

+

To help with the traffic costs I decided to try to use the free CDN functionality of Cloudflare to reduce the traffic to Storj. In this blog post I will describe how I did that.

+

Is this the right solution for you?

+

If you are in a similar situation as me, and just want to have somewhere to host your images for a personal website or to share images or screenshots as links while still having control over all your data, this could be a good solution.

+

If you want to build a robust image pipeline with resizing and image optimization, or you are building an enterprise website this is probably not the right way. You should take a look at cloudinary or one of the big cloud providers.

+

Prerequisites

+

To use Cloudflare as a CDN, you need to have Cloudflare setup as your DNS host for the domain you want to serve the images from. Even if you just want to use a subdomain like media.example.com, the whole example.com domain needs to be on cloudflare. For me this was not much of an issue, I followed the instructions from cloudflare and pointed the nameserver of my domain to cloudflare. Although I did have an issue during the migration, which resulted in my website being down for two hours. But I'm pretty sure this was caused by my previous nameserver provider.

+

Setting up Storj & Cloudflare

+

I assume you already have an account at storj.io. The next step is creating a bucket for your images. A bucket is just a place for your files and folders to live in storj, just like in any other S3 compatible storage provider. (Actually there are no folders in storj and other S3 services, the folders are just prefixes of the filenames). When creating a bucket, make sure you save the passphrase securely, such as in your password manager. Whenever storj asks you for the passphrase, make sure you don't let storj generate a new one! Every new passphrase will create access to a new bucket.

+

The next step is installing the uplink cli. Follow the quick start tutorial to get an access grant. Remember to use the same passphrase from above. Now follow the next quickstart tutorial to add the bucket to the uplink cli. The file accessgrant.txt in the tutorial only contains the access-grant string that you got from the last step.

+

Finally we want to share the bucket so the images can be accessed from the web. For this you can run the following command:

+
uplink share --dns <domain> sj://<bucket>/<prefix> --not-after=none
+
+

Replace <domain> with the domain you want to serve the images from. In my case I use media.tiim.ch. Then replace <bucket> with the name of your bucket and <prefix> with the prefix.

+

As mentioned above, you can think of a prefix as a folder. If you use for example media-site1 as a prefix, then every file in the "folder" media-site1 will be shared. This means you can use multiple prefixes to serve files for multiple websites in the same bucket.

+

You will get the following output:

+
[...]
+=========== DNS INFO =====================================================================
+Remember to update the $ORIGIN with your domain name. You may also change the $TTL.
+$ORIGIN example.com.
+$TTL    3600
+media.example.com           IN      CNAME   link.storjshare.io.
+txt-media.example.com       IN      TXT     storj-root:mybucket/myprefix
+txt-media.example.com       IN      TXT     storj-access:totallyrandomstringofnonsens
+
+

Create the DNS entries in Cloudflare with the values printed in the last three lines. Make sure you enable the proxy setting when entering the CNAME entry to enable Cloudflares CDN service.

+

And that's it. All files you put in the bucket with the correct prefix are now available under your domain! :)

+

If this blog post helped you, or you have some issues or thoughts on this, leave a comment via the comment box below or via webmention.

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

2 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2022-12-storj-cloudflare-image-hosting/__data.json b/blog/2022-12-storj-cloudflare-image-hosting/__data.json new file mode 100644 index 00000000..061fe84f --- /dev/null +++ b/blog/2022-12-storj-cloudflare-image-hosting/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":40},{"html":2,"slug":3,"uuid":4,"date":5,"aliases":6,"title":8,"published":9,"modified":7,"description":10,"cover_image":11,"cover_caption":12,"content_tags":13,"abstract":18,"tags":19,"links":-1,"type":24,"folder":25,"comments":26,"latestComment":39},"\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\" rel=\"nofollow noopener noreferrer\">TOS\u003C/a>:\u003C/p>\n\u003Cblockquote>\n\u003Cp>[...] Also, don't use Imgur to host image libraries you link to from elsewhere, content for your website, advertising, avatars, or anything else that turns us into your content delivery network.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Finally when I started \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">indie-webifying my website\u003C/a>, and was implementing the micropub protocol (which I will blog about at a later time), I decided that it was at the time to host the images on a platform that was meant to do that. I looked at a few storage providers such as cloudinary and S3 based object storage and landed on \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">Storj.io\u003C/a>, mostly because of the generous free tier, which should suffice for this little blog for quite a while.\u003C/p>\n\u003Cp>One thing that bothered me slightly was that all storage providers I looked at charge for traffic. It's not the fact that it's an additional expense (if your not in the free tier anymore) that bothers me, but the fact that I don't have any control over how much this will cost me. In all likelihood this will never cost me anything since this blog has not much traffic, but if a post were to go viral (one can dream...), this could result in a surprise bill at the end of the month.\u003C/p>\n\u003Cp>To help with the traffic costs I decided to try to use the free CDN functionality of Cloudflare to reduce the traffic to Storj. In this blog post I will describe how I did that.\u003C/p>\n\u003Ch2>Is this the right solution for you?\u003C/h2>\n\u003Cp>If you are in a similar situation as me, and just want to have somewhere to host your images for a personal website or to share images or screenshots as links while still having control over all your data, this could be a good solution.\u003C/p>\n\u003Cp>If you want to build a robust image pipeline with resizing and image optimization, or you are building an enterprise website this is probably not the right way. You should take a look at cloudinary or one of the big cloud providers.\u003C/p>\n\u003Ch2>Prerequisites\u003C/h2>\n\u003Cp>To use Cloudflare as a CDN, you need to have Cloudflare setup as your DNS host for the domain you want to serve the images from. Even if you just want to use a subdomain like \u003Ccode>media.example.com\u003C/code>, the whole \u003Ccode>example.com\u003C/code> domain needs to be on cloudflare. For me this was not much of an issue, I followed the instructions from cloudflare and pointed the nameserver of my domain to cloudflare. Although I did have an issue during the migration, which resulted in my website being down for two hours. But I'm pretty sure this was caused by my previous nameserver provider.\u003C/p>\n\u003Ch2>Setting up Storj & Cloudflare\u003C/h2>\n\u003Cp>I assume you already have an account at \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">storj.io\u003C/a>. The next step is creating a bucket for your images. A bucket is just a place for your files and folders to live in storj, just like in any other S3 compatible storage provider. (Actually there are no folders in storj and other S3 services, the folders are just prefixes of the filenames). When creating a bucket, make sure you save the passphrase securely, such as in your password manager. Whenever storj asks you for the passphrase, make sure you don't let storj generate a new one! Every new passphrase will create access to a new bucket.\u003C/p>\n\u003Cp>The next step is \u003Ca href=\"https://docs.storj.io/dcs/downloads/download-uplink-cli\" rel=\"nofollow noopener noreferrer\">installing the uplink cli\u003C/a>. Follow the quick start tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object\" rel=\"nofollow noopener noreferrer\">get an access grant\u003C/a>. Remember to use the same passphrase from above. Now follow the next quickstart tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object/set-up-uplink-cli\" rel=\"nofollow noopener noreferrer\">add the bucket to the uplink cli\u003C/a>. The file \u003Ccode>accessgrant.txt\u003C/code> in the tutorial only contains the access-grant string that you got from the last step.\u003C/p>\n\u003Cp>Finally we want to share the bucket so the images can be accessed from the web. For this you can run the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">uplink share --dns <domain> sj://<bucket>/<prefix> --not-after=none\n\u003C/code>\u003C/pre>\n\u003Cp>Replace \u003Ccode><domain>\u003C/code> with the domain you want to serve the images from. In my case I use \u003Ccode>media.tiim.ch\u003C/code>. Then replace \u003Ccode><bucket>\u003C/code> with the name of your bucket and \u003Ccode><prefix>\u003C/code> with the prefix.\u003C/p>\n\u003Cp>As mentioned above, you can think of a prefix as a folder. If you use for example \u003Ccode>media-site1\u003C/code> as a prefix, then every file in the \"folder\" \u003Ccode>media-site1\u003C/code> will be shared. This means you can use multiple prefixes to serve files for multiple websites in the same bucket.\u003C/p>\n\u003Cp>You will get the following output:\u003C/p>\n\u003Cpre>\u003Ccode>[...]\n=========== DNS INFO =====================================================================\nRemember to update the $ORIGIN with your domain name. You may also change the $TTL.\n$ORIGIN example.com.\n$TTL 3600\nmedia.example.com IN CNAME link.storjshare.io.\ntxt-media.example.com IN TXT storj-root:mybucket/myprefix\ntxt-media.example.com IN TXT storj-access:totallyrandomstringofnonsens\n\u003C/code>\u003C/pre>\n\u003Cp>Create the DNS entries in Cloudflare with the values printed in the last three lines. Make sure you enable the proxy setting when entering the CNAME entry to enable Cloudflares CDN service.\u003C/p>\n\u003Cp>And that's it. All files you put in the bucket with the correct prefix are now available under your domain! :)\u003C/p>\n\u003Cp>If this blog post helped you, or you have some issues or thoughts on this, leave a comment via the comment box below or via webmention.\u003C/p>","blog/2022-12-storj-cloudflare-image-hosting","6d5a964d-328e-43d7-9189-40280b012074",["Date","2022-12-03T13:37:33.000Z"],[7],null,"Hosting Images with Storj and Cloudflare",true,"Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.","https://media.tiim.ch/d280fad4-632a-4b5a-b6b2-6a5c0026b61c.jpg","Image generated by Dall-E: travel postcards scattered on grass, top down view, photoreal",[14,15,16,17],"CDN","IndieWeb","Cloudflare","Storj","\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\">TOS\u003C/a>:\u003C/p>",[20,21,22,23],"cdn","cloudflare","indieweb","storj","article","blog",[27,34],{"id":28,"type":29,"replyTo":30,"timestamp":31,"page":3,"url":32,"content":30,"name":33},"edeb5ddb-357a-41cf-b2b2-704df571d70c","webmention","","2023-01-11T06:03:38Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/1570290779419017216","Laura Forster",{"id":35,"type":29,"replyTo":30,"timestamp":36,"page":3,"url":37,"content":30,"name":38},"2331980c-acee-4549-a260-61b4119c63a9","2022-12-05T06:12:20Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/275384865","kevin","2023-09-02T19:26:59Z",{"html":41,"slug":42,"uuid":43,"date":44,"created":45,"published":9,"abstract":46,"tags":47,"links":-1,"type":24,"cover_image":-1,"description":30,"folder":48},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2023-01-15-weechat-docker.html b/blog/2023-01-15-weechat-docker.html new file mode 100644 index 00000000..7606335b --- /dev/null +++ b/blog/2023-01-15-weechat-docker.html @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + Running the WeeChat IRC Client on a VPS in Docker - Tim Bachmann + + +
+ + +
+
+ +
Running the WeeChat IRC Client on a VPS in Docker +
Stable Diffusion - anime landscape, pastel colors, thick outlines, forest, mountains, golden light
+ +

Running the WeeChat IRC Client on a VPS in Docker

+ + +

docker + irc + weechat +

+ +
by
+ published on +last updated on
+ + +

I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used HexChat in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.

+

The one client I have seen mentioned a lot is WeeChat (not to be confused with WeChat, the Chinese instant messenger). WeeChat runs in the terminal as a TUI and after a while of getting used to (and after enabling 'mouse mode') it seems intuitive enough.

+

The nice thing about WeeChat running not as a graphical application, is that it makes it possible to run on a server and access it from anywhere over ssh.

+
+INFO

Except on mobile devices, but weechat has mobile apps that can connect to it directly.

+
+

Since I pretty much host all my selfhosted software in docker on a VPS, I was looking if someone already published a docker image for WeeChat. There is a bunch of them, but only weechat/weechat (the official image) is still updated regularly. The docker hub page does not have any documentation, but I managed to find it in the weechat/weechat-container github repo.

+

As it says in the readme on github, you can start the container with

+
docker run -it weechat/weechat
+
+

which will run weechat directly in the foreground.

+
+Info

Don't skip the -it command line flags. The -i or --interactive keeps stdin open, which is required to send input to weechat. Weechat also closes immediately if the stdin gets closed, which took me a while to figure out. +The -t or --tty flag is required to provide a fake tty to the container. I don't really understand what that means but without this you won't see the user interface of weechat.

+
+

Running in the foreground is not really that helpful if we want to run weechat on a server, so we need to detach (let it run in the background) from the container with the -d or --detach flag. It also helps to specify a name for the container with the --name <name> argument, so we can quickly find the container again later. The docker command now looks like this:

+
docker run -it -d --name weechat weechat/weechat
+
+

When we run this command, we will notice that weechat is running in the background. To access it we can run docker attach weechat. To detach from weechat without exiting the container, we can press CTRL-p CTRL-q as described in the docker attach reference

+

I noticed that there are two versions of the weechat image: a debian version and an alpine linux version. Generally the Alpine Linux versions of containers are smaller than the Debian versions, so I decided to use the alpine version: weechat/weechat:latest-alpine.

+

With this we are practically done, but if we ever remove and restart the container, all of the chat logs and customisations to weechat will be gone. To prevent this we need to add the config and log files to a volume.

+

I generally use the folder ~/docker/(service) to point my docker volumes to, so I have a convenient place to inspect, modify and back up the data.

+

Let's create the folder and add the volume to the docker container. I also added the --restart unless-stopped flag to make sure the container gets restarted if it either exits for some reason of if docker restarts.

+
mkdir -p ~/docker/weechat/data
+mkdir -p ~/docker/weechat/config
+
+docker run -it -d --restart unless-stopped \
+    -v "~/docker/weechat/data:/home/user/.weechat" \
+    -v "~/docker/weechat/config:/home/user/.config/weechat" \
+    --name weechat weechat/weechat:latest-alpine`
+
+

Running this command on the server is all we need to have weechat running in docker.

+
+

But how do I quickly connect to weechat? Do I always have to first ssh into the server and then run docker attach?

+
+

Yes but, as almost always, we can simplify this with a bash script:

+
#!/usr/bin/env bash
+
+HOST=<ssh host>
+ssh -t "${HOST}" docker attach weechat
+
+

This bash script starts ssh with the -t flag which tells ssh that the command is interactive. +Copy this script into your ~/.local/bin folder and make it executable.

+
nano ~/.local/bin/weechat.sh
+chmod +x weechat.sh
+
+

And that's it! Running weechat.sh will open an ssh session to your server and attach to the weechat container. Happy Chatting!

+

If you liked this post, consider subscribing to my blog via RSS, or on social media. If you have any questions, feel free to contact me. I also usually hang out in ##tiim on irc.libera.chat. My name on IRC is tiim.

+
+Update 2022-01-18

I have found that at the beginning of a session, the input to weechat doesn't seem to work. Sometimes weechat refuses to let me type anything and/or doesn't recognize mouse events. +After a while of spamming keys and Alt-m (toggle mouse mode), it seems to fix itself most of the time. +I have no idea if thats a problem with weechat, with docker or with ssh, and so far have not found a solution for this. If you have the same problem or even know how to fix it, feel free to reach out.

+
+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

4 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
  • + +

    Tim Bachmann

    + 2023-03-28 10:10:56 AM + +

    Mentioned this post

    +
    + +

    Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.

    +
  • + +

    DM Cyber Security

    + 2023-03-02 12:05:49 AM + +

    Mentioned this post

    +
    + +

    +
  • + +

    Christopher Scott

    + 2023-01-25 6:20:43 PM + +

    Mentioned this post

    +
    + +

    +
  • + +

    WeeChat

    + 2023-01-25 9:22:51 AM + +

    Mentioned this post

    +
    + +

    New blog post: A walkthrough on how to set up the WeeChat IRC client in docker. #irc #docker @WeeChatClient +tiim.ch/blog/2023-01-1…

    +
  • +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2023-01-15-weechat-docker/__data.json b/blog/2023-01-15-weechat-docker/__data.json new file mode 100644 index 00000000..4a3b003e --- /dev/null +++ b/blog/2023-01-15-weechat-docker/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":49},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"aliases":7,"title":9,"published":10,"modified":11,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":48},"\u003Cp>I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used \u003Ca href=\"https://hexchat.github.io/\" rel=\"nofollow noopener noreferrer\">HexChat\u003C/a> in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.\u003C/p>\n\u003Cp>The one client I have seen mentioned a lot is \u003Ca href=\"https://weechat.org/\" rel=\"nofollow noopener noreferrer\">WeeChat\u003C/a> (not to be confused with WeChat, the Chinese instant messenger). WeeChat runs in the terminal as a \u003Ca href=\"https://en.wikipedia.org/wiki/Text-based_user_interface\" rel=\"nofollow noopener noreferrer\">TUI\u003C/a> and after a while of getting used to (and after enabling 'mouse mode') it seems intuitive enough.\u003C/p>\n\u003Cp>The nice thing about WeeChat running not as a graphical application, is that it makes it possible to run on a server and access it from anywhere over ssh.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>INFO\u003C/span>\u003Cp>Except on mobile devices, but weechat has mobile apps that can connect to it directly.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Since I pretty much host all my selfhosted software in docker on a VPS, I was looking if someone already published a docker image for WeeChat. There is a bunch of them, but only \u003Ca href=\"https://hub.docker.com/r/weechat/weechat\" rel=\"nofollow noopener noreferrer\">weechat/weechat\u003C/a> (the official image) is still updated regularly. The docker hub page does not have any documentation, but I managed to find it in the \u003Ca href=\"https://github.com/weechat/weechat-container\" rel=\"nofollow noopener noreferrer\">weechat/weechat-container\u003C/a> github repo.\u003C/p>\n\u003Cp>As it says in the readme on github, you can start the container with\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">docker run -it weechat/weechat\n\u003C/code>\u003C/pre>\n\u003Cp>which will run weechat directly in the foreground.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>Don't skip the \u003Ccode>-it\u003C/code> command line flags. The \u003Ccode>-i\u003C/code> or \u003Ccode>--interactive\u003C/code> keeps stdin open, which is required to send input to weechat. Weechat also closes immediately if the stdin gets closed, which took me a while to figure out.\nThe \u003Ccode>-t\u003C/code> or \u003Ccode>--tty\u003C/code> flag is required to provide a fake tty to the container. I don't really understand what that means but without this you won't see the user interface of weechat.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Running in the foreground is not really that helpful if we want to run weechat on a server, so we need to detach (let it run in the background) from the container with the \u003Ccode>-d\u003C/code> or \u003Ccode>--detach\u003C/code> flag. It also helps to specify a name for the container with the \u003Ccode>--name <name>\u003C/code> argument, so we can quickly find the container again later. The docker command now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">docker run -it -d --name weechat weechat/weechat\n\u003C/code>\u003C/pre>\n\u003Cp>When we run this command, we will notice that weechat is running in the background. To access it we can run \u003Ccode>docker attach weechat\u003C/code>. To detach from weechat without exiting the container, we can press \u003Ccode>CTRL-p CTRL-q\u003C/code> as described in the \u003Ca href=\"https://docs.docker.com/engine/reference/commandline/attach/#description\" rel=\"nofollow noopener noreferrer\">docker attach reference\u003C/a>\u003C/p>\n\u003Cp>I noticed that there are two versions of the weechat image: a debian version and an alpine linux version. Generally the Alpine Linux versions of containers are smaller than the Debian versions, so I decided to use the alpine version: \u003Ccode>weechat/weechat:latest-alpine\u003C/code>.\u003C/p>\n\u003Cp>With this we are practically done, but if we ever remove and restart the container, all of the chat logs and customisations to weechat will be gone. To prevent this we need to add the config and log files to a volume.\u003C/p>\n\u003Cp>I generally use the folder \u003Ccode>~/docker/(service)\u003C/code> to point my docker volumes to, so I have a convenient place to inspect, modify and back up the data.\u003C/p>\n\u003Cp>Let's create the folder and add the volume to the docker container. I also added the \u003Ccode>--restart unless-stopped\u003C/code> flag to make sure the container gets restarted if it either exits for some reason of if docker restarts.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">mkdir -p ~/docker/weechat/data\nmkdir -p ~/docker/weechat/config\n\ndocker run -it -d --restart unless-stopped \\\n -v \"~/docker/weechat/data:/home/user/.weechat\" \\\n -v \"~/docker/weechat/config:/home/user/.config/weechat\" \\\n --name weechat weechat/weechat:latest-alpine`\n\u003C/code>\u003C/pre>\n\u003Cp>Running this command on the server is all we need to have weechat running in docker.\u003C/p>\n\u003Cblockquote>\n\u003Cp>But how do I quickly connect to weechat? Do I always have to first ssh into the server and then run docker attach?\u003C/p>\n\u003C/blockquote>\n\u003Cp>Yes but, as almost always, we can simplify this with a bash script:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-bash\">#!/usr/bin/env bash\n\nHOST=<ssh host>\nssh -t \"${HOST}\" docker attach weechat\n\u003C/code>\u003C/pre>\n\u003Cp>This bash script starts ssh with the \u003Ccode>-t\u003C/code> flag which tells ssh that the command is interactive.\nCopy this script into your \u003Ccode>~/.local/bin\u003C/code> folder and make it executable.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">nano ~/.local/bin/weechat.sh\nchmod +x weechat.sh\n\u003C/code>\u003C/pre>\n\u003Cp>And that's it! Running \u003Ccode>weechat.sh\u003C/code> will open an ssh session to your server and attach to the weechat container. Happy Chatting!\u003C/p>\n\u003Cp>If you liked this post, consider subscribing to my blog via \u003Ca href=\"https://tiim.ch/blog/rss.xml\" rel=\"nofollow noopener noreferrer\">RSS\u003C/a>, or on \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">social media\u003C/a>. If you have any questions, feel free to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>. I also usually hang out in \u003Ca href=\"irc://irc.libera.chat/##tiim\">\u003Ccode>##tiim\u003C/code> on irc.libera.chat\u003C/a>. My name on IRC is \u003Ccode>tiim\u003C/code>.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Update 2022-01-18\u003C/span>\u003Cp>I have found that at the beginning of a session, the input to weechat doesn't seem to work. Sometimes weechat refuses to let me type anything and/or doesn't recognize mouse events.\nAfter a while of spamming keys and \u003Ccode>Alt-m\u003C/code> (toggle mouse mode), it seems to fix itself most of the time.\nI have no idea if thats a problem with weechat, with docker or with ssh, and so far have not found a solution for this. If you have the same problem or even know how to fix it, feel free to reach out.\u003C/p>\n\u003C/blockquote>","blog/2023-01-15-weechat-docker","889ff4db-3ccb-4ab1-9676-a2b0ea8f19eb",["Date","2023-01-15T00:00:00.000Z"],["Date","2023-01-15T00:17:07.000Z"],[8],null,"Running the WeeChat IRC Client on a VPS in Docker",true,["Date","2023-01-18T11:34:27.000Z"],"Walkthrough on how to setup the WeeChat IRC client in docker.","https://media.tiim.ch/a28c65a1-ed95-43d3-af87-a2ad222bee7f.jpg","Stable Diffusion - anime landscape, pastel colors, thick outlines, forest, mountains, golden light",[16,17,18],"irc","weechat","docker","\u003Cp>I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used \u003Ca href=\"https://hexchat.github.io/\">HexChat\u003C/a> in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.\u003C/p>",[18,16,17],"article","blog",[24,32,37,42],{"id":25,"type":26,"replyTo":27,"timestamp":28,"page":3,"url":29,"content":30,"name":31},"52b7b3e6-e233-4379-8f0c-3332aed562a6","webmention","","2023-03-28T10:10:56Z","https://tiim.ch/blog/2023-03-28-weechat-notification-ntfy","Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.","Tim Bachmann",{"id":33,"type":26,"replyTo":27,"timestamp":34,"page":3,"url":35,"content":27,"name":36},"f6f58ebf-a68f-415e-baed-cb8bf38189fd","2023-03-02T00:05:49Z","https://brid.gy/like/twitter/tiimb/1614403118258601987/1557313575072546816","DM Cyber Security",{"id":38,"type":26,"replyTo":27,"timestamp":39,"page":3,"url":40,"content":27,"name":41},"45d40e9f-6498-4432-bdb0-01210e55d092","2023-01-25T18:20:43Z","https://brid.gy/like/twitter/tiimb/1614403118258601987/8717982","Christopher Scott",{"id":43,"type":26,"replyTo":27,"timestamp":44,"page":3,"url":45,"content":46,"name":47},"979c42d0-a8fc-4a52-a85d-bedb672fb144","2023-01-25T09:22:51Z","https://brid.gy/repost/twitter/tiimb/1614403118258601987/1618133427139792898","New blog post: A walkthrough on how to set up the WeeChat IRC client in docker. #irc #docker @WeeChatClient\ntiim.ch/blog/2023-01-1…","WeeChat","2023-09-02T19:26:59Z",{"html":50,"slug":51,"uuid":52,"date":53,"created":54,"published":10,"abstract":55,"tags":56,"links":-1,"type":21,"cover_image":-1,"description":27,"folder":57},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2023-01-24-no-such-file-or-directory-cgo.html b/blog/2023-01-24-no-such-file-or-directory-cgo.html new file mode 100644 index 00000000..89e1c3f5 --- /dev/null +++ b/blog/2023-01-24-no-such-file-or-directory-cgo.html @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + "no such file or directory" after enabling CGO in Docker - Tim Bachmann + + +
+ + +
+
+ +
+ +

"no such file or directory" after enabling CGO in Docker

+ + +

cgo + docker + go +

+ +
by
+ published on +
+ + +

Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message exec /app/indiego: no such file or directory. I had removed the CGO_ENABLE=0 variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the scratch image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.

+

To include libc into the container, I simply changed the base image from scratch to alpine, which includes libc. This makes the image slightly larger but this seemed way easier than trying to include libc directly.

+

As a bonus I got to delete the /usr/share/zoneinfo and ca-certificates.crt files, and rely on those provided by alpine.

+

You can see the commit to IndieGo here.

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2023-01-24-no-such-file-or-directory-cgo/__data.json b/blog/2023-01-24-no-such-file-or-directory-cgo/__data.json new file mode 100644 index 00000000..564b1d82 --- /dev/null +++ b/blog/2023-01-24-no-such-file-or-directory-cgo/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":23},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"aliases":7,"title":9,"published":10,"modified":8,"description":11,"cover_image":-1,"cover_image_txt":12,"content_tags":13,"abstract":17,"tags":18,"links":-1,"type":19,"folder":20,"comments":21,"latestComment":22},"\u003Cp>Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message \u003Ccode>exec /app/indiego: no such file or directory\u003C/code>. I had removed the \u003Ccode>CGO_ENABLE=0\u003C/code> variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the \u003Ccode>scratch\u003C/code> image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.\u003C/p>\n\u003Cp>To include libc into the container, I simply changed the base image from \u003Ccode>scratch\u003C/code> to \u003Ccode>alpine\u003C/code>, which includes libc. This makes the image slightly larger but this seemed way easier than trying to include libc directly.\u003C/p>\n\u003Cp>As a bonus I got to delete the \u003Ccode>/usr/share/zoneinfo\u003C/code> and \u003Ccode>ca-certificates.crt\u003C/code> files, and rely on those provided by alpine.\u003C/p>\n\u003Cp>You can see the commit to IndieGo \u003Ca href=\"https://github.com/Tiim/IndieGo/commit/63968814de7e39f295386bf398b583aa8bf0411c\" rel=\"nofollow noopener noreferrer\">here\u003C/a>.\u003C/p>","blog/2023-01-24-no-such-file-or-directory-cgo","dd580343-9e0f-4754-93dd-25667e6b5859",["Date","2023-01-24T00:00:00.000Z"],["Date","2023-01-24T20:54:11.330Z"],[8],null,"\"no such file or directory\" after enabling CGO in Docker",true,"Quick fix for the \"no such file or directory\" error after enabling CGO, when running in a scratch docker image.","",[14,15,16],"go","cgo","docker","\u003Cp>Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message \u003Ccode>exec /app/indiego: no such file or directory\u003C/code>. I had removed the \u003Ccode>CGO_ENABLE=0\u003C/code> variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the \u003Ccode>scratch\u003C/code> image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.\u003C/p>",[15,16,14],"article","blog",[],"2023-09-02T19:26:59Z",{"html":24,"slug":25,"uuid":26,"date":27,"created":28,"published":10,"abstract":29,"tags":30,"links":-1,"type":19,"cover_image":-1,"description":12,"folder":31},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2023-03-21-anyconnect-wsl2.html b/blog/2023-03-21-anyconnect-wsl2.html new file mode 100644 index 00000000..ccec593e --- /dev/null +++ b/blog/2023-03-21-anyconnect-wsl2.html @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN - Tim Bachmann + + +
+ + +
+
+ +
Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN +
Stable Diffusion - Anything V3.0 - 1boy, hacker, in front of computer, back of head visible, vintage neon color scheme, terminal, big monitor
+ +

Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN

+ + +

dns + networking + vpn + wsl +

+ +
by
+ published on +
+ + +

I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.

+

I will show you how I fixed this problem for me and explain what the commands I used do. This post is mostly for my reference, but I hope it helps anyone else as well.

+

Finding out what your problem is

+

Let's check first if we have internet access inside WSL2. For this run the ping command with an IP address as a destination:

+
ping 8.8.8.8
+
+

If you get something like this as the output, your internet connection is fine, and it's just the DNS nameserver addresses that are misconfigured, you can jump forward to Solution 2.

+
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
+64 bytes from 8.8.8.8: icmp_seq=1 ttl=108 time=4.53 ms
+64 bytes from 8.8.8.8: icmp_seq=2 ttl=108 time=3.94 ms
+64 bytes from 8.8.8.8: icmp_seq=3 ttl=108 time=3.97 ms
+64 bytes from 8.8.8.8: icmp_seq=4 ttl=108 time=3.78 ms
+64 bytes from 8.8.8.8: icmp_seq=5 ttl=108 time=3.77 ms
+64 bytes from 8.8.8.8: icmp_seq=6 ttl=108 time=3.76 ms
+64 bytes from 8.8.8.8: icmp_seq=7 ttl=108 time=3.81 ms
+
+

If you don't get any responses from the ping (i.e. no more output after the PING 8.8.8.8 (8.8.8.8) ... line), you need to configure the WSL and the VPN network adapter metric. Go to Solution 1.

+

To check if the DNS is working, we can again use the ping command, this time with a domain name:

+
ping google.com
+
+

If you get responses, the DNS and your internet connection are working! If not go to Section 2.

+

Solution 1: Fixing the Network Adapter

+

Run the following two commands in PowerShell as administrator:

+
Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match "Cisco AnyConnect"} | Set-NetIPInterface -InterfaceMetric 4000
+
+Get-NetIPInterface -InterfaceAlias "vEthernet (WSL)" | Set-NetIPInterface -InterfaceMetric 1
+
+

Let me explain what those two commands do. Both follow the same pattern of listing all network adapters, selecting a specific adapter from the list and setting its "metric".

+

You can imagine an adapter as a virtual network port on the back of your pc or laptop. But instead of sending packets through the wire, the driver for a specific port can do whatever it wants with those packets, in the case of a VPN, the packets get encrypted and forwarded to the internet via another adapter.

+

The InterfaceMetric is a value associated with each adapter that determines the order of those adapters. This allows windows to determine which adapter to prefer over another one.

+

By setting the interface metric of the Cisco adapter to 4000 and the metric of the WSL adapter to one, we allow the traffic from WSL to flow through the Cisco adapter. To be honest I do not exactly understand why this works but it does.

+

Solution 2: Registering the VPN DNS inside of WSL

+

Setting the DNS servers is, unfortunately, a little bit more involved than just running two commands, we need to edit the files /etc/wsl.conf and /etc/resolv.conf, and restart wsl in between. Let's get to it:

+

Edit the file /etc/wsl.conf inside of WSL2 using a text editor. I suggest doing this through the terminal since you need root permissions to do that:

+
sudo nano /etc/wsl.conf
+# feel free to use another editor such as vim or emacs
+
+

Most likely this file does not exist yet, otherwise, I suggest you create a backup of the original file to preserve the settings.

+

Add the following config settings into the file:

+
[network]
+generateResolvConf = false
+
+

This will instruct WSL to not override the /etc/resolv.conf file on every start-up. Save the file and restart WSL with the following command so that the changed config takes effect:

+
wsl.exe --shutdown
+
+

Now open a PowerShell terminal and list all network adapters with the following command:

+
ipconfig /all
+
+

Find the Cisco AnyConnect adapter and copy the IP addresses in the DNS-Server field. We will need those IPs in the next step.

+

Start WSL again and edit the /etc/resolv.conf file:

+
sudo nano /etc/resolv.conf
+
+

Most likely there is already something in this file, you can discard it. When undoing the changes, WSL will automatically regenerate this file anyway, so you don't need to back it up.

+

Delete all the contents and enter the IP addresses you noted down in the last step in the following format:

+
nameserver xxx.xxx.xxx.xxx
+
+

Put each address on a new line, preceded by the string nameserver. +Save the file and restart WSL with the same command as above:

+
wsl.exe --shutdown
+
+

Now open up WSL for the last time and set the immutable flag for the /etc/resolv.conf file:

+
chattr +i /etc/resolv.conf
+
+

And for the last time shut down WSL. Your DNS should now be working fine!

+

Undoing those changes

+

I did not have a need to undo the steps for Solution 1, and I'm pretty sure the metric resets after each system reboot anyway so there is not much to do.

+

To get DNS working again when not connected to the VPN run the following commands:

+
sudo chattr -i /etc/resolv.conf
+sudo rm /etc/resolv.conf
+sudo rm /etc/wsl.conf
+wsl.exe --shutdown
+
+

This will first clear the immutable flag off /etc/resolv.conf, and delete it. Next, it will delete /etc/wsl.conf if you have a backup of a previous wsl.conf file, you can replace it with that. At last, we shutdown WSL again for the changes to take effect.

+

Unfortunately, this is quite a procedure to get a VPN to work with WSL2, but I'm hopeful that this will soon not be necessairy anymore.

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2023-03-21-anyconnect-wsl2/__data.json b/blog/2023-03-21-anyconnect-wsl2/__data.json new file mode 100644 index 00000000..26893d51 --- /dev/null +++ b/blog/2023-03-21-anyconnect-wsl2/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":25},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"aliases":7,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_image_txt":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>\n\u003Cp>I will show you how I fixed this problem for me and explain what the commands I used do. This post is mostly for my reference, but I hope it helps anyone else as well.\u003C/p>\n\u003Ch2>Finding out what your problem is\u003C/h2>\n\u003Cp>Let's check first if we have internet access inside WSL2. For this run the ping command with an IP address as a destination:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping 8.8.8.8\n\u003C/code>\u003C/pre>\n\u003Cp>If you get something like this as the output, your internet connection is fine, and it's just the DNS nameserver addresses that are misconfigured, you can jump forward to Solution 2.\u003C/p>\n\u003Cpre>\u003Ccode>PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n64 bytes from 8.8.8.8: icmp_seq=1 ttl=108 time=4.53 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=108 time=3.94 ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=108 time=3.97 ms\n64 bytes from 8.8.8.8: icmp_seq=4 ttl=108 time=3.78 ms\n64 bytes from 8.8.8.8: icmp_seq=5 ttl=108 time=3.77 ms\n64 bytes from 8.8.8.8: icmp_seq=6 ttl=108 time=3.76 ms\n64 bytes from 8.8.8.8: icmp_seq=7 ttl=108 time=3.81 ms\n\u003C/code>\u003C/pre>\n\u003Cp>If you don't get any responses from the ping (i.e. no more output after the \u003Ccode>PING 8.8.8.8 (8.8.8.8) ...\u003C/code> line), you need to configure the WSL and the VPN network adapter metric. Go to Solution 1.\u003C/p>\n\u003Cp>To check if the DNS is working, we can again use the ping command, this time with a domain name:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping google.com\n\u003C/code>\u003C/pre>\n\u003Cp>If you get responses, the DNS and your internet connection are working! If not go to Section 2.\u003C/p>\n\u003Ch2>Solution 1: Fixing the Network Adapter\u003C/h2>\n\u003Cp>Run the following two commands in PowerShell as administrator:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match \"Cisco AnyConnect\"} | Set-NetIPInterface -InterfaceMetric 4000\n\nGet-NetIPInterface -InterfaceAlias \"vEthernet (WSL)\" | Set-NetIPInterface -InterfaceMetric 1\n\u003C/code>\u003C/pre>\n\u003Cp>Let me explain what those two commands do. Both follow the same pattern of listing all network adapters, selecting a specific adapter from the list and setting its \"metric\".\u003C/p>\n\u003Cp>You can imagine an adapter as a virtual network port on the back of your pc or laptop. But instead of sending packets through the wire, the driver for a specific port can do whatever it wants with those packets, in the case of a VPN, the packets get encrypted and forwarded to the internet via another adapter.\u003C/p>\n\u003Cp>The \u003Ca href=\"https://learn.microsoft.com/en-us/windows-server/networking/technologies/network-subsystem/net-sub-interface-metric\" rel=\"nofollow noopener noreferrer\">InterfaceMetric\u003C/a> is a value associated with each adapter that determines the order of those adapters. This allows windows to determine which adapter to prefer over another one.\u003C/p>\n\u003Cp>By setting the interface metric of the Cisco adapter to 4000 and the metric of the WSL adapter to one, we allow the traffic from WSL to flow through the Cisco adapter. To be honest I do not exactly understand why this works but it does.\u003C/p>\n\u003Ch2>Solution 2: Registering the VPN DNS inside of WSL\u003C/h2>\n\u003Cp>Setting the DNS servers is, unfortunately, a little bit more involved than just running two commands, we need to edit the files \u003Ccode>/etc/wsl.conf\u003C/code> and \u003Ccode>/etc/resolv.conf\u003C/code>, and restart wsl in between. Let's get to it:\u003C/p>\n\u003Cp>Edit the file \u003Ccode>/etc/wsl.conf\u003C/code> inside of WSL2 using a text editor. I suggest doing this through the terminal since you need root permissions to do that:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/wsl.conf\n# feel free to use another editor such as vim or emacs\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely this file does not exist yet, otherwise, I suggest you create a backup of the original file to preserve the settings.\u003C/p>\n\u003Cp>Add the following config settings into the file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-ini\">[network]\ngenerateResolvConf = false\n\u003C/code>\u003C/pre>\n\u003Cp>This will instruct WSL to not override the \u003Ccode>/etc/resolv.conf\u003C/code> file on every start-up. Save the file and restart WSL with the following command so that the changed config takes effect:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open a PowerShell terminal and list all network adapters with the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ipconfig /all\n\u003C/code>\u003C/pre>\n\u003Cp>Find the Cisco AnyConnect adapter and copy the IP addresses in the DNS-Server field. We will need those IPs in the next step.\u003C/p>\n\u003Cp>Start WSL again and edit the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely there is already something in this file, you can discard it. When undoing the changes, WSL will automatically regenerate this file anyway, so you don't need to back it up.\u003C/p>\n\u003Cp>Delete all the contents and enter the IP addresses you noted down in the last step in the following format:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-resolv\">nameserver xxx.xxx.xxx.xxx\n\u003C/code>\u003C/pre>\n\u003Cp>Put each address on a new line, preceded by the string \u003Ccode>nameserver\u003C/code>.\nSave the file and restart WSL with the same command as above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open up WSL for the last time and set the immutable flag for the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">chattr +i /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>And for the last time shut down WSL. Your DNS should now be working fine!\u003C/p>\n\u003Ch2>Undoing those changes\u003C/h2>\n\u003Cp>I did not have a need to undo the steps for \u003Ccode>Solution 1\u003C/code>, and I'm pretty sure the metric resets after each system reboot anyway so there is not much to do.\u003C/p>\n\u003Cp>To get DNS working again when not connected to the VPN run the following commands:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo chattr -i /etc/resolv.conf\nsudo rm /etc/resolv.conf\nsudo rm /etc/wsl.conf\nwsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>This will first clear the immutable flag off \u003Ccode>/etc/resolv.conf\u003C/code>, and delete it. Next, it will delete \u003Ccode>/etc/wsl.conf\u003C/code> if you have a backup of a previous \u003Ccode>wsl.conf\u003C/code> file, you can replace it with that. At last, we shutdown WSL again for the changes to take effect.\u003C/p>\n\u003Cp>Unfortunately, this is quite a procedure to get a VPN to work with WSL2, but I'm hopeful that this will soon not be necessairy anymore.\u003C/p>","blog/2023-03-21-anyconnect-wsl2","c67bc4dc-4c96-41b1-afb5-15a99457dedf",["Date","2023-03-15T15:22:04.511Z"],["Date","2023-03-15T15:22:04.511Z"],[8],null,"Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN",true,"I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.","https://media.tiim.ch/66ca4290-3fc0-450f-977b-f00f888e4af3.webp","Stable Diffusion - Anything V3.0 - 1boy, hacker, in front of computer, back of head visible, vintage neon color scheme, terminal, big monitor",[15,16,17,18],"wsl","vpn","networking","dns","\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>",[18,17,16,15],"article","blog",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"date":29,"created":30,"published":10,"abstract":31,"tags":32,"links":-1,"type":21,"cover_image":-1,"description":33,"folder":34},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2023-03-28-weechat-notification-ntfy.html b/blog/2023-03-28-weechat-notification-ntfy.html new file mode 100644 index 00000000..abfd7d72 --- /dev/null +++ b/blog/2023-03-28-weechat-notification-ntfy.html @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + Weechat Notifications with ntfy.sh - Tim Bachmann + + +
+ + +
+
+ +
Weechat Notifications with ntfy.sh +
stable diffusion - Anything V3.0 - boy using an old DOS computer, 90s vibes, muted pastel colors, stylized, thick lines, IRC, console
+ +

Weechat Notifications with ntfy.sh

+ + +

irc + ntfy.sh + weechat + wget +

+ +
by
+ published on +
+ + +

In one of my last blog posts I set up WeeChat in docker, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the trigger plugin. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.

+

Also, I recently found the web service ntfy.sh. It sends push notifications whenever you send an HTTP post request to a certain URL. I already have ntfy.sh installed on my android phone, and I also found a minimal and lightweight desktop client.

+

I managed to set a WeeChat trigger up that fires every time I get mentioned (highlighted in WeeChat terminology), and a trigger that fires every time I get a private message. Both of those triggers execute the /exec command which runs an arbitrary shell command. The exec command runs the wget program to send a post request to the ntfy.sh server, which in turn sends a notification to all apps that subscribe to the same URL as the post request was sent. I would usually use the curl program for this instead of wget, but the docker default docker image doesn't contain a curl install.

+

Here you can see the two /trigger commands:

+

trigger on mention

+
/trigger addreplace notify_highlight print '' '${tg_highlight}' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data "${tg_message}" "-                   -header=Title: New highlight: ${buffer.full_name}" https://ntfy.sh/my_ntfy_topic_1234'
+
+

trigger on private message

+
/trigger addreplace notify_privmsg print '' '${tg_tag_notify} == private && ${buffer.notify} > 0' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data "${tg_message}" "--header=Title: New private message: ${buffer.full_name}" https://ntfy.sh/my_ntfy_topic_1234'
+
+

The trigger commands in detail

+

In case you don't just want to copy and paste some random command from the internet into your WeeChat (which you shouldn't do anyway), I will try to explain the trigger command that fires when you get mentioned in a message:

+

Let's first look at the trigger command itself: +/trigger addreplace <name> <hook> <argument> <condition> <variable-replace> <command> +We call the /trigger command with the addreplace subcommand. This subcommand will either register a new trigger or replace it if one with the same name already exists.

+
    +
  • name - This argument is self-explanatory, the name of the trigger. In our case I called it notify_highlight, but you could call it whatever you want.
  • +
  • hook - This argument specifies which hook or event the trigger should listen for. WeeChat is built as an event-driven platform, so pretty much anything from mouse movements to IRC messages are handled via events. In this case, we want to trigger on the print event, which is fired every time a new message gets received from IRC.
  • +
  • argument - The argument is needed for some hooks, but not for the print hook, so we are going to ignore that one for now and just set it to an empty string ''.
  • +
  • condition - The condition must evaluate to true for the trigger to fire. This is helpful because the print trigger fires for every new message, but we only want to be notified when the new message mentions our nick. The condition for this is ${tg_highlight}. You can find the list of variables that you can access with the command /trigger monitor, which prints all variables for every trigger that gets executed.
  • +
  • variable-replace - This took me a while to understand. This command is used to manipulate data and save it to a variable. The syntax is inspired by the sed command. Explaining it fully is out of the scope of this blog post, but you can take a look at the docs. In our example, we replace the whole content of the variable tg_message with the format string ${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor} which results in a sting like <tiim> Hello world!.
  • +
  • command - The last argument is the command that gets executed whenever this trigger fires. In our case, we use the /execute command, which starts the wget command which in turn sends a post request to ntfy.sh. Make sure you set the ntfy topic (the part after https://ntfy.sh/) to something private and long enough so that nobody else is going to guess it by accident.
  • +
+

Don't forget to subscribe to the ntfy topic on your phone or whatever device you want to receive the notification on.

+

The possibilities with the trigger plugin are endless, I hope this inspires you to build your own customizations using weechat.

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2023-03-28-weechat-notification-ntfy/__data.json b/blog/2023-03-28-weechat-notification-ntfy/__data.json new file mode 100644 index 00000000..75874e98 --- /dev/null +++ b/blog/2023-03-28-weechat-notification-ntfy/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":25},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"aliases":7,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_image_txt":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\" rel=\"nofollow noopener noreferrer\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\" rel=\"nofollow noopener noreferrer\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>\n\u003Cp>Also, I recently found the web service \u003Ca href=\"https://ntfy.sh\" rel=\"nofollow noopener noreferrer\">ntfy.sh\u003C/a>. It sends push notifications whenever you send an HTTP post request to a certain URL. I already have ntfy.sh installed on my android phone, and I also found a minimal and lightweight \u003Ca href=\"https://github.com/lucas-bortoli/ntfysh-windows\" rel=\"nofollow noopener noreferrer\">desktop client\u003C/a>.\u003C/p>\n\u003Cp>I managed to set a WeeChat trigger up that fires every time I get mentioned (highlighted in WeeChat terminology), and a trigger that fires every time I get a private message. Both of those triggers execute the \u003Ccode>/exec\u003C/code> command which runs an arbitrary shell command. The exec command runs the \u003Ccode>wget\u003C/code> program to send a post request to the ntfy.sh server, which in turn sends a notification to all apps that subscribe to the same URL as the post request was sent. I would usually use the curl program for this instead of wget, but the docker default docker image doesn't contain a curl install.\u003C/p>\n\u003Cp>Here you can see the two \u003Ccode>/trigger\u003C/code> commands:\u003C/p>\n\u003Cp>\u003Cem>trigger on mention\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode>/trigger addreplace notify_highlight print '' '${tg_highlight}' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"- -header=Title: New highlight: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cem>trigger on private message\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-weechat\">/trigger addreplace notify_privmsg print '' '${tg_tag_notify} == private && ${buffer.notify} > 0' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"--header=Title: New private message: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Ch2>The trigger commands in detail\u003C/h2>\n\u003Cp>In case you don't just want to copy and paste some random command from the internet into your WeeChat (which you shouldn't do anyway), I will try to explain the trigger command that fires when you get mentioned in a message:\u003C/p>\n\u003Cp>Let's first look at the trigger command itself:\n\u003Ccode>/trigger addreplace <name> <hook> <argument> <condition> <variable-replace> <command>\u003C/code>\nWe call the \u003Ccode>/trigger\u003C/code> command with the \u003Ccode>addreplace\u003C/code> subcommand. This subcommand will either register a new trigger or replace it if one with the same name already exists.\u003C/p>\n\u003Cul>\n\u003Cli>\u003Ccode>name\u003C/code> - This argument is self-explanatory, the name of the trigger. In our case I called it \u003Ccode>notify_highlight\u003C/code>, but you could call it whatever you want.\u003C/li>\n\u003Cli>\u003Ccode>hook\u003C/code> - This argument specifies which hook or event the trigger should listen for. WeeChat is built as an event-driven platform, so pretty much anything from mouse movements to IRC messages are handled via events. In this case, we want to trigger on the \u003Ccode>print\u003C/code> event, which is fired every time a new message gets received from IRC.\u003C/li>\n\u003Cli>\u003Ccode>argument\u003C/code> - The argument is needed for some hooks, but not for the \u003Ccode>print\u003C/code> hook, so we are going to ignore that one for now and just set it to an empty string \u003Ccode>''\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>condition\u003C/code> - The condition must evaluate to \u003Ccode>true\u003C/code> for the trigger to fire. This is helpful because the \u003Ccode>print\u003C/code> trigger fires for every new message, but we only want to be notified when the new message mentions our nick. The condition for this is \u003Ccode>${tg_highlight}\u003C/code>. You can find the list of variables that you can access with the command \u003Ccode>/trigger monitor\u003C/code>, which prints all variables for every trigger that gets executed.\u003C/li>\n\u003Cli>\u003Ccode>variable-replace\u003C/code> - This took me a while to understand. This command is used to manipulate data and save it to a variable. The syntax is inspired by the sed command. Explaining it fully is out of the scope of this blog post, but you can take a look at the \u003Ca href=\"https://weechat.org/files/doc/devel/weechat_user.en.html#trigger_regex\" rel=\"nofollow noopener noreferrer\">docs\u003C/a>. In our example, we replace the whole content of the variable \u003Ccode>tg_message\u003C/code> with the format string \u003Ccode>${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}\u003C/code> which results in a sting like \u003Ccode><tiim> Hello world!\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>command\u003C/code> - The last argument is the command that gets executed whenever this trigger fires. In our case, we use the \u003Ccode>/execute\u003C/code> command, which starts the wget command which in turn sends a post request to ntfy.sh. Make sure you set the ntfy topic (the part after \u003Ccode>https://ntfy.sh/\u003C/code>) to something private and long enough so that nobody else is going to guess it by accident.\u003C/li>\n\u003C/ul>\n\u003Cp>Don't forget to subscribe to the ntfy topic on your phone or whatever device you want to receive the notification on.\u003C/p>\n\u003Cp>The possibilities with the trigger plugin are endless, I hope this inspires you to build your own customizations using weechat.\u003C/p>","blog/2023-03-28-weechat-notification-ntfy","ef51e944-86fa-44b0-ab9d-be7f8d8e569a",["Date","2023-03-28T10:05:19.000Z"],["Date","2023-01-15T20:35:40.643Z"],[8],null,"Weechat Notifications with ntfy.sh",true,"Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.","https://media.tiim.ch/97833b1d-d602-4d9a-9689-3077e96e45ba.webp","stable diffusion - Anything V3.0 - boy using an old DOS computer, 90s vibes, muted pastel colors, stylized, thick lines, IRC, console",[15,16,17,18],"weechat","ntfy.sh","wget","irc","\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>",[18,16,15,17],"article","blog",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"date":29,"created":30,"published":10,"abstract":31,"tags":32,"links":-1,"type":21,"cover_image":-1,"description":33,"folder":34},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2023-05-06-pdr-with-seed-heuristics.html b/blog/2023-05-06-pdr-with-seed-heuristics.html new file mode 100644 index 00000000..a9704d31 --- /dev/null +++ b/blog/2023-05-06-pdr-with-seed-heuristics.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + Automated Planning using Property-Directed Reachability with Seed Heuristics - Tim Bachmann + + +
+ + +
+
+ +
Automated Planning using Property-Directed Reachability with Seed Heuristics +
+ +

Automated Planning using Property-Directed Reachability with Seed Heuristics

+ + +

dev + heuristic + pdr + planning-system +

+ +
by
+ published on +last updated on
+ + +

Abstract

+

Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.

+

Download PDF

+

Cite

+
@phdthesis{bachmann2023,
+    author = {Bachmann, Tim},
+    year = {2023},
+    month = {05},
+    title = {Automated Planning using Property-Directed Reachability with Seed Heuristics},
+    doi = {10.13140/RG.2.2.11456.30727},
+    type = {Master's Thesis},
+    school = {University of Basel}
+}
+
+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2023-05-06-pdr-with-seed-heuristics/__data.json b/blog/2023-05-06-pdr-with-seed-heuristics/__data.json new file mode 100644 index 00000000..1a73c7bf --- /dev/null +++ b/blog/2023-05-06-pdr-with-seed-heuristics/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":24},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"aliases":7,"title":8,"published":9,"modified":10,"description":11,"cover_image":12,"cover_image_txt":7,"content_tags":13,"abstract":18,"tags":19,"links":-1,"type":20,"folder":21,"comments":22,"latestComment":23},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>\n\u003Cp>\u003Ca href=\"https://www.researchgate.net/publication/373994137_Automated_Planning_using_Property-Directed_Reachability_with_Seed_Heuristics\" rel=\"nofollow noopener noreferrer\">Download PDF\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-bibtex\">@phdthesis{bachmann2023,\n author = {Bachmann, Tim},\n year = {2023},\n month = {05},\n title = {Automated Planning using Property-Directed Reachability with Seed Heuristics},\n doi = {10.13140/RG.2.2.11456.30727},\n type = {Master's Thesis},\n school = {University of Basel}\n}\n\u003C/code>\u003C/pre>","blog/2023-05-06-pdr-with-seed-heuristics","111e68c4-0285-4f21-ab36-4c1ce1989da1",["Date","2023-05-06T11:15:53.000Z"],["Date","2023-05-06T11:15:53.000Z"],null,"Automated Planning using Property-Directed Reachability with Seed Heuristics",true,["Date","2023-09-18T13:32:00.000Z"],"Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.","https://media.tiim.ch/023c1722-ac3d-45fd-b66c-9ff319dfc180.webp",[14,15,16,17],"dev","planning-system","pdr","heuristic","\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>",[14,17,16,15],"article","blog",[],"2023-09-02T19:26:59Z",{"html":25,"slug":26,"uuid":27,"date":28,"created":29,"published":9,"abstract":30,"tags":31,"links":-1,"type":20,"cover_image":-1,"description":32,"folder":33},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2023-06-16-forums.html b/blog/2023-06-16-forums.html new file mode 100644 index 00000000..64c28465 --- /dev/null +++ b/blog/2023-06-16-forums.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + Forums - Tim Bachmann + + +
+ + +
+
+ +
Forums +
Stable Diffusion - bunch people talking to each other, social, speech bubbles, digital art, minimalistic
+ +

Forums

+ + +

activitypub + fediverse + forum + reddit +

+ +
by
+ published on +
+ + +

My first real programming experience was with a scripting language called AutoHotkey. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized. +When AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while. +Unfortunately, the German forum has since been discontinued, but some of the pages are still up on the Way back machine.

+

Another community I used to be really active in, was for a small indie roleplaying game called Illarion. Again, the community relied heavily on a forum for communications. This time it was used for players to engage in "out of character" communication, as well as a way to simulate a metaphorical bullet board in the game town square where characters could leave notes for each other. +Since the game was closely inspired by TTRPGs like D&D, the role-playing part was more important than the in-game mechanics. The forum allowed characters to interact with each other that were not online at the same time. Again, I got really invested in this community, even going so far as joining other guild-specific forums.

+

I eventually moved on from both of those amazing communities, because my interests changed. I left the AutoHotkey community because I started to get more involved with other programming languages, and I left the Illarion community because I (with the support of my parents) was looking for a less time-intensive game. Unfortunately, I never happened to find another online community like those two ever again...

+

Sometime later I joined Reddit and was amazed. It felt like a place where all communities come together on a single site. No need to check on multiple websites for new posts, everything neatly together in a single website, accessible on a single (third party) app. I remember wondering why people were still using forums when Reddit was so much simpler.

+

Jumping to the present and I realize that I was wrong. Even though I am subscribed to a bunch of communities on Reddit, I barely comment on any posts and posted even less. While I am a community member on record, I do not feel like one. The wealth of communities, as well as the incentive to go on the front page to see the most popular posts of the whole site, made me want to open Reddit, but it did not give me the feeling of belonging. I rather felt like a spectator that from time to time gathers the courage to shout his own ideas into the ether.

+
+

Side note: Discord comes much closer to the feeling of community. However, the nature of chat makes the interactions fleeting, being in a chat room with a few hundred other people, where every message is just a few sentences at most does not lead to the same connections. No one expects their message to be read again after a few days.

+
+

Now the company behind Reddit started to lose the goodwill of the users. While I don't think Reddit will die anytime soon, I think there are a lot of people looking for alternatives. And the best alternative to the website that killed forums is... forums.

+

While forums largely still work the same as they did 15 years ago, there have been developments that might make them more feasible for our desire to have everything accessible on a single site or on a single app. Last time a social media company, Twitter, annoyed its user base, the fediverse, and more specifically Mastodon, started to go more mainstream. This time I hope there will be other projects that profit. I have heard people mentioning the projects Kbin and Lemmy, both forum-like platforms that implement the ActivityPub specification. Same as Mastodon, this means users are able to interact with users on other instances. Even further, this should also allow users of any federated social network, such as Mastodon, to post and comment on any federated forum. Even established forum software such as Flarum and nodeBB are considering adding federation support.

+

I really hope that forums make a comeback, not only because of the nostalgia but also because to me it feels like a more sustainable way to build a community. And now with the possibility to federate via the fediverse, a forum doesn't have to be a walled garden of members any more. In the end, most importantly I hope people are still finding communities they can be as passionate about as I was, without any corporate overlords trying to keep their eyeballs on ads as long as possible.

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2023-06-16-forums/__data.json b/blog/2023-06-16-forums/__data.json new file mode 100644 index 00000000..67310679 --- /dev/null +++ b/blog/2023-06-16-forums/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":25},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"aliases":7,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_image_txt":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\" rel=\"nofollow noopener noreferrer\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\" rel=\"nofollow noopener noreferrer\">Way back machine\u003C/a>.\u003C/p>\n\u003Cp>Another community I used to be really active in, was for a small indie roleplaying game called \u003Ca href=\"\">Illarion\u003C/a>. Again, the community relied heavily on a forum for communications. This time it was used for players to engage in \"out of character\" communication, as well as a way to simulate a metaphorical bullet board in the game town square where characters could leave notes for each other.\nSince the game was closely inspired by TTRPGs like D&D, the role-playing part was more important than the in-game mechanics. The forum allowed characters to interact with each other that were not online at the same time. Again, I got really invested in this community, even going so far as joining other guild-specific forums.\u003C/p>\n\u003Cp>I eventually moved on from both of those amazing communities, because my interests changed. I left the AutoHotkey community because I started to get more involved with other programming languages, and I left the Illarion community because I (with the support of my parents) was looking for a less time-intensive game. Unfortunately, I never happened to find another online community like those two ever again...\u003C/p>\n\u003Cp>Sometime later I joined Reddit and was amazed. It felt like a place where all communities come together on a single site. No need to check on multiple websites for new posts, everything neatly together in a single website, accessible on a single (third party) app. I remember wondering why people were still using forums when Reddit was so much simpler.\u003C/p>\n\u003Cp>Jumping to the present and I realize that I was wrong. Even though I am subscribed to a bunch of communities on Reddit, I barely comment on any posts and posted even less. While I am a community member on record, I do not feel like one. The wealth of communities, as well as the incentive to go on the front page to see the most popular posts of the whole site, made me want to open Reddit, but it did not give me the feeling of belonging. I rather felt like a spectator that from time to time gathers the courage to shout his own ideas into the ether.\u003C/p>\n\u003Cblockquote>\n\u003Cp>Side note: Discord comes much closer to the feeling of community. However, the nature of chat makes the interactions fleeting, being in a chat room with a few hundred other people, where every message is just a few sentences at most does not lead to the same connections. No one expects their message to be read again after a few days.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Now the company behind Reddit started to lose the goodwill of the users. While I don't think Reddit will die anytime soon, I think there are a lot of people looking for alternatives. And the best alternative to the website that killed forums is... forums.\u003C/p>\n\u003Cp>While forums largely still work the same as they did 15 years ago, there have been developments that might make them more feasible for our desire to have everything accessible on a single site or on a single app. Last time a social media company, Twitter, annoyed its user base, the fediverse, and more specifically Mastodon, started to go more mainstream. This time I hope there will be other projects that profit. I have heard people mentioning the projects Kbin and Lemmy, both forum-like platforms that implement the ActivityPub specification. Same as Mastodon, this means users are able to interact with users on other instances. Even further, this should also allow users of any federated social network, such as Mastodon, to post and comment on any federated forum. Even established forum software such as \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">Flarum\u003C/a> and \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">nodeBB\u003C/a> are considering adding federation support.\u003C/p>\n\u003Cp>I really hope that forums make a comeback, not only because of the nostalgia but also because to me it feels like a more sustainable way to build a community. And now with the possibility to federate via the fediverse, a forum doesn't have to be a walled garden of members any more. In the end, most importantly I hope people are still finding communities they can be as passionate about as I was, without any corporate overlords trying to keep their eyeballs on ads as long as possible.\u003C/p>","blog/2023-06-16-forums","624afba6-2962-4710-9bc7-686702cc9b55",["Date","2023-06-16T18:56:56.000Z"],["Date","2023-06-16T15:09:15.488Z"],[8],null,"Forums",true,"My experience of using forums in my teens, what changed after I started using reddit and my hopes for internet communities in the future.","https://media.tiim.ch/fe5de393-9773-4eaa-877a-decffbd706b4.webp","Stable Diffusion - bunch people talking to each other, social, speech bubbles, digital art, minimalistic",[15,16,17,18],"forum","fediverse","reddit","activitypub","\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\">Way back machine\u003C/a>.\u003C/p>",[18,16,15,17],"article","blog",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"date":29,"created":30,"published":10,"abstract":31,"tags":32,"links":-1,"type":21,"cover_image":-1,"description":33,"folder":34},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/2023-09-20-ansible-absolute-path.html b/blog/2023-09-20-ansible-absolute-path.html new file mode 100644 index 00000000..0f6f3870 --- /dev/null +++ b/blog/2023-09-20-ansible-absolute-path.html @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + Getting the Absolute Path of a Remote Directory in Ansible - Tim Bachmann + + +
+ + +
+
+ +
Getting the Absolute Path of a Remote Directory in Ansible +
(stable doodle) server room, neon, cables
+ +

Getting the Absolute Path of a Remote Directory in Ansible

+ + +

ansible + bash + dev + linux +

+ +
by
+ published on +
+ + +

I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like ~/docker/myservice. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.

+

Deleting with elevated permission on the command line is easy: The command sudo rm -rf ~/docker/myservice performs the rm operation as the root user. In bash, this will delete the docker/myservice folder in the user's home directory, but when doing the equivalent in Ansible, this won't work!

+
# This does not work!
+- name: Delete the folder using root permissions
+  become: true
+  ansible.builtin.file:
+    path: "~/docker/myservice"
+    state: "absent"
+
+

This code will try to delete the file /user/root/docker/myservice, which is not what we wanted.

+

The bash version works because the shell first resolves the tilde in the argument to the current users' directory before calling the sudo command. In Ansible, we first switch to the root user and only then the tilde is resolved: this time to the home directory of the root user.

+

To circumvent this, we can manually resolve the path to an absolute path. Unfortunately, I have not found a straightforward way to do this in Ansible, however the bash command readlink -f <path> does exactly this. To use it in Ansible, we can use the following configuration:

+
- name: Get absolute folder path
+  ansible.builtin.command:
+    cmd: "readlink -f ~/docker/myservice"
+  register: folder_abs
+  changed_when: False
+
+- name: Debug
+  debug:
+    msg: "{{folder_abs.stdout}}" # prints /user/tim/docker/myservice
+
+- name: Delete the folder using root permissions
+  become: true
+  ansible.builtin.file:
+    path: "{{folder_abs.stdout}}"
+    state: "absent"
+
+

With this Ansible script, we manually resolve the absolute path and use it to delete the folder using root permissions. If you know of an easier way to resolve to an absolute path, please let me know!

+ + + +
You found an error in this post? Open a + pull request. +
+ +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/blog/2023-09-20-ansible-absolute-path/__data.json b/blog/2023-09-20-ansible-absolute-path/__data.json new file mode 100644 index 00000000..179af919 --- /dev/null +++ b/blog/2023-09-20-ansible-absolute-path/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":24},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"aliases":7,"title":8,"published":9,"modified":7,"description":10,"cover_image":11,"cover_image_txt":12,"content_tags":13,"abstract":18,"tags":19,"links":-1,"type":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>\n\u003Cp>Deleting with elevated permission on the command line is easy: The command \u003Ccode>sudo rm -rf ~/docker/myservice\u003C/code> performs the \u003Ccode>rm\u003C/code> operation as the root user. In bash, this will delete the \u003Ccode>docker/myservice\u003C/code> folder in the user's home directory, but when doing the equivalent in Ansible, this won't work!\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\"># This does not work!\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"~/docker/myservice\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>This code will try to delete the file \u003Ccode>/user/root/docker/myservice\u003C/code>, which is not what we wanted.\u003C/p>\n\u003Cp>The bash version works because the shell first resolves the tilde in the argument to the current users' directory before calling the sudo command. In Ansible, we first switch to the root user and only then the tilde is resolved: this time to the home directory of the root user.\u003C/p>\n\u003Cp>To circumvent this, we can manually resolve the path to an absolute path. Unfortunately, I have not found a straightforward way to do this in Ansible, however the bash command \u003Ccode>readlink -f <path>\u003C/code> does exactly this. To use it in Ansible, we can use the following configuration:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\">- name: Get absolute folder path\n ansible.builtin.command:\n cmd: \"readlink -f ~/docker/myservice\"\n register: folder_abs\n changed_when: False\n\n- name: Debug\n debug:\n msg: \"{{folder_abs.stdout}}\" # prints /user/tim/docker/myservice\n\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"{{folder_abs.stdout}}\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>With this Ansible script, we manually resolve the absolute path and use it to delete the folder using root permissions. If you know of an easier way to resolve to an absolute path, please let me know!\u003C/p>","blog/2023-09-20-ansible-absolute-path","ad58acaf-56b0-4bcf-9b72-d6c054fc48d4",["Date","2023-09-20T21:39:13.000Z"],["Date","2023-09-20T20:22:35.634Z"],null,"Getting the Absolute Path of a Remote Directory in Ansible",true,"There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.","https://media.tiim.ch/3c1246e4-3201-4df6-af87-6aa4ab98800e.webp","(stable doodle) server room, neon, cables",[14,15,16,17],"dev","ansible","linux","bash","\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>",[15,17,14,16],"article","blog",[],"2023-09-02T19:26:59Z",{"html":25,"slug":26,"uuid":27,"date":28,"created":29,"published":9,"abstract":30,"tags":31,"links":-1,"type":20,"cover_image":-1,"description":32,"folder":33},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["slug"]}}]} diff --git a/blog/__data.json b/blog/__data.json new file mode 100644 index 00000000..01cc14b6 --- /dev/null +++ b/blog/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1},[2,25,44,61,80,99,116,154,186,249,275,352,372,406,421,439,454],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_image_txt":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>\n\u003Cp>Deleting with elevated permission on the command line is easy: The command \u003Ccode>sudo rm -rf ~/docker/myservice\u003C/code> performs the \u003Ccode>rm\u003C/code> operation as the root user. In bash, this will delete the \u003Ccode>docker/myservice\u003C/code> folder in the user's home directory, but when doing the equivalent in Ansible, this won't work!\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\"># This does not work!\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"~/docker/myservice\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>This code will try to delete the file \u003Ccode>/user/root/docker/myservice\u003C/code>, which is not what we wanted.\u003C/p>\n\u003Cp>The bash version works because the shell first resolves the tilde in the argument to the current users' directory before calling the sudo command. In Ansible, we first switch to the root user and only then the tilde is resolved: this time to the home directory of the root user.\u003C/p>\n\u003Cp>To circumvent this, we can manually resolve the path to an absolute path. Unfortunately, I have not found a straightforward way to do this in Ansible, however the bash command \u003Ccode>readlink -f <path>\u003C/code> does exactly this. To use it in Ansible, we can use the following configuration:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\">- name: Get absolute folder path\n ansible.builtin.command:\n cmd: \"readlink -f ~/docker/myservice\"\n register: folder_abs\n changed_when: False\n\n- name: Debug\n debug:\n msg: \"{{folder_abs.stdout}}\" # prints /user/tim/docker/myservice\n\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"{{folder_abs.stdout}}\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>With this Ansible script, we manually resolve the absolute path and use it to delete the folder using root permissions. If you know of an easier way to resolve to an absolute path, please let me know!\u003C/p>","blog/2023-09-20-ansible-absolute-path","ad58acaf-56b0-4bcf-9b72-d6c054fc48d4",["Date","2023-09-20T21:39:13.000Z"],["Date","2023-09-20T20:22:35.634Z"],null,"Getting the Absolute Path of a Remote Directory in Ansible",true,"There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.","https://media.tiim.ch/3c1246e4-3201-4df6-af87-6aa4ab98800e.webp","(stable doodle) server room, neon, cables",[15,16,17,18],"dev","ansible","linux","bash","\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>",[16,18,15,17],"article","blog",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"date":29,"created":30,"aliases":31,"title":32,"published":10,"modified":8,"description":33,"cover_image":34,"cover_image_txt":35,"content_tags":36,"abstract":41,"tags":42,"links":-1,"type":21,"folder":22,"comments":43,"latestComment":24},"\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\" rel=\"nofollow noopener noreferrer\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\" rel=\"nofollow noopener noreferrer\">Way back machine\u003C/a>.\u003C/p>\n\u003Cp>Another community I used to be really active in, was for a small indie roleplaying game called \u003Ca href=\"\">Illarion\u003C/a>. Again, the community relied heavily on a forum for communications. This time it was used for players to engage in \"out of character\" communication, as well as a way to simulate a metaphorical bullet board in the game town square where characters could leave notes for each other.\nSince the game was closely inspired by TTRPGs like D&D, the role-playing part was more important than the in-game mechanics. The forum allowed characters to interact with each other that were not online at the same time. Again, I got really invested in this community, even going so far as joining other guild-specific forums.\u003C/p>\n\u003Cp>I eventually moved on from both of those amazing communities, because my interests changed. I left the AutoHotkey community because I started to get more involved with other programming languages, and I left the Illarion community because I (with the support of my parents) was looking for a less time-intensive game. Unfortunately, I never happened to find another online community like those two ever again...\u003C/p>\n\u003Cp>Sometime later I joined Reddit and was amazed. It felt like a place where all communities come together on a single site. No need to check on multiple websites for new posts, everything neatly together in a single website, accessible on a single (third party) app. I remember wondering why people were still using forums when Reddit was so much simpler.\u003C/p>\n\u003Cp>Jumping to the present and I realize that I was wrong. Even though I am subscribed to a bunch of communities on Reddit, I barely comment on any posts and posted even less. While I am a community member on record, I do not feel like one. The wealth of communities, as well as the incentive to go on the front page to see the most popular posts of the whole site, made me want to open Reddit, but it did not give me the feeling of belonging. I rather felt like a spectator that from time to time gathers the courage to shout his own ideas into the ether.\u003C/p>\n\u003Cblockquote>\n\u003Cp>Side note: Discord comes much closer to the feeling of community. However, the nature of chat makes the interactions fleeting, being in a chat room with a few hundred other people, where every message is just a few sentences at most does not lead to the same connections. No one expects their message to be read again after a few days.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Now the company behind Reddit started to lose the goodwill of the users. While I don't think Reddit will die anytime soon, I think there are a lot of people looking for alternatives. And the best alternative to the website that killed forums is... forums.\u003C/p>\n\u003Cp>While forums largely still work the same as they did 15 years ago, there have been developments that might make them more feasible for our desire to have everything accessible on a single site or on a single app. Last time a social media company, Twitter, annoyed its user base, the fediverse, and more specifically Mastodon, started to go more mainstream. This time I hope there will be other projects that profit. I have heard people mentioning the projects Kbin and Lemmy, both forum-like platforms that implement the ActivityPub specification. Same as Mastodon, this means users are able to interact with users on other instances. Even further, this should also allow users of any federated social network, such as Mastodon, to post and comment on any federated forum. Even established forum software such as \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">Flarum\u003C/a> and \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">nodeBB\u003C/a> are considering adding federation support.\u003C/p>\n\u003Cp>I really hope that forums make a comeback, not only because of the nostalgia but also because to me it feels like a more sustainable way to build a community. And now with the possibility to federate via the fediverse, a forum doesn't have to be a walled garden of members any more. In the end, most importantly I hope people are still finding communities they can be as passionate about as I was, without any corporate overlords trying to keep their eyeballs on ads as long as possible.\u003C/p>","blog/2023-06-16-forums","624afba6-2962-4710-9bc7-686702cc9b55",["Date","2023-06-16T18:56:56.000Z"],["Date","2023-06-16T15:09:15.488Z"],[8],"Forums","My experience of using forums in my teens, what changed after I started using reddit and my hopes for internet communities in the future.","https://media.tiim.ch/fe5de393-9773-4eaa-877a-decffbd706b4.webp","Stable Diffusion - bunch people talking to each other, social, speech bubbles, digital art, minimalistic",[37,38,39,40],"forum","fediverse","reddit","activitypub","\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\">Way back machine\u003C/a>.\u003C/p>",[40,38,37,39],[],{"html":45,"slug":46,"uuid":47,"date":48,"created":49,"aliases":8,"title":50,"published":10,"modified":51,"description":52,"cover_image":53,"cover_image_txt":8,"content_tags":54,"abstract":58,"tags":59,"links":-1,"type":21,"folder":22,"comments":60,"latestComment":24},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>\n\u003Cp>\u003Ca href=\"https://www.researchgate.net/publication/373994137_Automated_Planning_using_Property-Directed_Reachability_with_Seed_Heuristics\" rel=\"nofollow noopener noreferrer\">Download PDF\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-bibtex\">@phdthesis{bachmann2023,\n author = {Bachmann, Tim},\n year = {2023},\n month = {05},\n title = {Automated Planning using Property-Directed Reachability with Seed Heuristics},\n doi = {10.13140/RG.2.2.11456.30727},\n type = {Master's Thesis},\n school = {University of Basel}\n}\n\u003C/code>\u003C/pre>","blog/2023-05-06-pdr-with-seed-heuristics","111e68c4-0285-4f21-ab36-4c1ce1989da1",["Date","2023-05-06T11:15:53.000Z"],["Date","2023-05-06T11:15:53.000Z"],"Automated Planning using Property-Directed Reachability with Seed Heuristics",["Date","2023-09-18T13:32:00.000Z"],"Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.","https://media.tiim.ch/023c1722-ac3d-45fd-b66c-9ff319dfc180.webp",[15,55,56,57],"planning-system","pdr","heuristic","\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>",[15,57,56,55],[],{"html":62,"slug":63,"uuid":64,"date":65,"created":66,"aliases":67,"title":68,"published":10,"modified":8,"description":69,"cover_image":70,"cover_image_txt":71,"content_tags":72,"abstract":77,"tags":78,"links":-1,"type":21,"folder":22,"comments":79,"latestComment":24},"\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\" rel=\"nofollow noopener noreferrer\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\" rel=\"nofollow noopener noreferrer\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>\n\u003Cp>Also, I recently found the web service \u003Ca href=\"https://ntfy.sh\" rel=\"nofollow noopener noreferrer\">ntfy.sh\u003C/a>. It sends push notifications whenever you send an HTTP post request to a certain URL. I already have ntfy.sh installed on my android phone, and I also found a minimal and lightweight \u003Ca href=\"https://github.com/lucas-bortoli/ntfysh-windows\" rel=\"nofollow noopener noreferrer\">desktop client\u003C/a>.\u003C/p>\n\u003Cp>I managed to set a WeeChat trigger up that fires every time I get mentioned (highlighted in WeeChat terminology), and a trigger that fires every time I get a private message. Both of those triggers execute the \u003Ccode>/exec\u003C/code> command which runs an arbitrary shell command. The exec command runs the \u003Ccode>wget\u003C/code> program to send a post request to the ntfy.sh server, which in turn sends a notification to all apps that subscribe to the same URL as the post request was sent. I would usually use the curl program for this instead of wget, but the docker default docker image doesn't contain a curl install.\u003C/p>\n\u003Cp>Here you can see the two \u003Ccode>/trigger\u003C/code> commands:\u003C/p>\n\u003Cp>\u003Cem>trigger on mention\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode>/trigger addreplace notify_highlight print '' '${tg_highlight}' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"- -header=Title: New highlight: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cem>trigger on private message\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-weechat\">/trigger addreplace notify_privmsg print '' '${tg_tag_notify} == private && ${buffer.notify} > 0' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"--header=Title: New private message: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Ch2>The trigger commands in detail\u003C/h2>\n\u003Cp>In case you don't just want to copy and paste some random command from the internet into your WeeChat (which you shouldn't do anyway), I will try to explain the trigger command that fires when you get mentioned in a message:\u003C/p>\n\u003Cp>Let's first look at the trigger command itself:\n\u003Ccode>/trigger addreplace <name> <hook> <argument> <condition> <variable-replace> <command>\u003C/code>\nWe call the \u003Ccode>/trigger\u003C/code> command with the \u003Ccode>addreplace\u003C/code> subcommand. This subcommand will either register a new trigger or replace it if one with the same name already exists.\u003C/p>\n\u003Cul>\n\u003Cli>\u003Ccode>name\u003C/code> - This argument is self-explanatory, the name of the trigger. In our case I called it \u003Ccode>notify_highlight\u003C/code>, but you could call it whatever you want.\u003C/li>\n\u003Cli>\u003Ccode>hook\u003C/code> - This argument specifies which hook or event the trigger should listen for. WeeChat is built as an event-driven platform, so pretty much anything from mouse movements to IRC messages are handled via events. In this case, we want to trigger on the \u003Ccode>print\u003C/code> event, which is fired every time a new message gets received from IRC.\u003C/li>\n\u003Cli>\u003Ccode>argument\u003C/code> - The argument is needed for some hooks, but not for the \u003Ccode>print\u003C/code> hook, so we are going to ignore that one for now and just set it to an empty string \u003Ccode>''\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>condition\u003C/code> - The condition must evaluate to \u003Ccode>true\u003C/code> for the trigger to fire. This is helpful because the \u003Ccode>print\u003C/code> trigger fires for every new message, but we only want to be notified when the new message mentions our nick. The condition for this is \u003Ccode>${tg_highlight}\u003C/code>. You can find the list of variables that you can access with the command \u003Ccode>/trigger monitor\u003C/code>, which prints all variables for every trigger that gets executed.\u003C/li>\n\u003Cli>\u003Ccode>variable-replace\u003C/code> - This took me a while to understand. This command is used to manipulate data and save it to a variable. The syntax is inspired by the sed command. Explaining it fully is out of the scope of this blog post, but you can take a look at the \u003Ca href=\"https://weechat.org/files/doc/devel/weechat_user.en.html#trigger_regex\" rel=\"nofollow noopener noreferrer\">docs\u003C/a>. In our example, we replace the whole content of the variable \u003Ccode>tg_message\u003C/code> with the format string \u003Ccode>${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}\u003C/code> which results in a sting like \u003Ccode><tiim> Hello world!\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>command\u003C/code> - The last argument is the command that gets executed whenever this trigger fires. In our case, we use the \u003Ccode>/execute\u003C/code> command, which starts the wget command which in turn sends a post request to ntfy.sh. Make sure you set the ntfy topic (the part after \u003Ccode>https://ntfy.sh/\u003C/code>) to something private and long enough so that nobody else is going to guess it by accident.\u003C/li>\n\u003C/ul>\n\u003Cp>Don't forget to subscribe to the ntfy topic on your phone or whatever device you want to receive the notification on.\u003C/p>\n\u003Cp>The possibilities with the trigger plugin are endless, I hope this inspires you to build your own customizations using weechat.\u003C/p>","blog/2023-03-28-weechat-notification-ntfy","ef51e944-86fa-44b0-ab9d-be7f8d8e569a",["Date","2023-03-28T10:05:19.000Z"],["Date","2023-01-15T20:35:40.643Z"],[8],"Weechat Notifications with ntfy.sh","Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.","https://media.tiim.ch/97833b1d-d602-4d9a-9689-3077e96e45ba.webp","stable diffusion - Anything V3.0 - boy using an old DOS computer, 90s vibes, muted pastel colors, stylized, thick lines, IRC, console",[73,74,75,76],"weechat","ntfy.sh","wget","irc","\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>",[76,74,73,75],[],{"html":81,"slug":82,"uuid":83,"date":84,"created":85,"aliases":86,"title":87,"published":10,"modified":8,"description":88,"cover_image":89,"cover_image_txt":90,"content_tags":91,"abstract":96,"tags":97,"links":-1,"type":21,"folder":22,"comments":98,"latestComment":24},"\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>\n\u003Cp>I will show you how I fixed this problem for me and explain what the commands I used do. This post is mostly for my reference, but I hope it helps anyone else as well.\u003C/p>\n\u003Ch2>Finding out what your problem is\u003C/h2>\n\u003Cp>Let's check first if we have internet access inside WSL2. For this run the ping command with an IP address as a destination:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping 8.8.8.8\n\u003C/code>\u003C/pre>\n\u003Cp>If you get something like this as the output, your internet connection is fine, and it's just the DNS nameserver addresses that are misconfigured, you can jump forward to Solution 2.\u003C/p>\n\u003Cpre>\u003Ccode>PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n64 bytes from 8.8.8.8: icmp_seq=1 ttl=108 time=4.53 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=108 time=3.94 ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=108 time=3.97 ms\n64 bytes from 8.8.8.8: icmp_seq=4 ttl=108 time=3.78 ms\n64 bytes from 8.8.8.8: icmp_seq=5 ttl=108 time=3.77 ms\n64 bytes from 8.8.8.8: icmp_seq=6 ttl=108 time=3.76 ms\n64 bytes from 8.8.8.8: icmp_seq=7 ttl=108 time=3.81 ms\n\u003C/code>\u003C/pre>\n\u003Cp>If you don't get any responses from the ping (i.e. no more output after the \u003Ccode>PING 8.8.8.8 (8.8.8.8) ...\u003C/code> line), you need to configure the WSL and the VPN network adapter metric. Go to Solution 1.\u003C/p>\n\u003Cp>To check if the DNS is working, we can again use the ping command, this time with a domain name:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping google.com\n\u003C/code>\u003C/pre>\n\u003Cp>If you get responses, the DNS and your internet connection are working! If not go to Section 2.\u003C/p>\n\u003Ch2>Solution 1: Fixing the Network Adapter\u003C/h2>\n\u003Cp>Run the following two commands in PowerShell as administrator:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match \"Cisco AnyConnect\"} | Set-NetIPInterface -InterfaceMetric 4000\n\nGet-NetIPInterface -InterfaceAlias \"vEthernet (WSL)\" | Set-NetIPInterface -InterfaceMetric 1\n\u003C/code>\u003C/pre>\n\u003Cp>Let me explain what those two commands do. Both follow the same pattern of listing all network adapters, selecting a specific adapter from the list and setting its \"metric\".\u003C/p>\n\u003Cp>You can imagine an adapter as a virtual network port on the back of your pc or laptop. But instead of sending packets through the wire, the driver for a specific port can do whatever it wants with those packets, in the case of a VPN, the packets get encrypted and forwarded to the internet via another adapter.\u003C/p>\n\u003Cp>The \u003Ca href=\"https://learn.microsoft.com/en-us/windows-server/networking/technologies/network-subsystem/net-sub-interface-metric\" rel=\"nofollow noopener noreferrer\">InterfaceMetric\u003C/a> is a value associated with each adapter that determines the order of those adapters. This allows windows to determine which adapter to prefer over another one.\u003C/p>\n\u003Cp>By setting the interface metric of the Cisco adapter to 4000 and the metric of the WSL adapter to one, we allow the traffic from WSL to flow through the Cisco adapter. To be honest I do not exactly understand why this works but it does.\u003C/p>\n\u003Ch2>Solution 2: Registering the VPN DNS inside of WSL\u003C/h2>\n\u003Cp>Setting the DNS servers is, unfortunately, a little bit more involved than just running two commands, we need to edit the files \u003Ccode>/etc/wsl.conf\u003C/code> and \u003Ccode>/etc/resolv.conf\u003C/code>, and restart wsl in between. Let's get to it:\u003C/p>\n\u003Cp>Edit the file \u003Ccode>/etc/wsl.conf\u003C/code> inside of WSL2 using a text editor. I suggest doing this through the terminal since you need root permissions to do that:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/wsl.conf\n# feel free to use another editor such as vim or emacs\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely this file does not exist yet, otherwise, I suggest you create a backup of the original file to preserve the settings.\u003C/p>\n\u003Cp>Add the following config settings into the file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-ini\">[network]\ngenerateResolvConf = false\n\u003C/code>\u003C/pre>\n\u003Cp>This will instruct WSL to not override the \u003Ccode>/etc/resolv.conf\u003C/code> file on every start-up. Save the file and restart WSL with the following command so that the changed config takes effect:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open a PowerShell terminal and list all network adapters with the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ipconfig /all\n\u003C/code>\u003C/pre>\n\u003Cp>Find the Cisco AnyConnect adapter and copy the IP addresses in the DNS-Server field. We will need those IPs in the next step.\u003C/p>\n\u003Cp>Start WSL again and edit the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely there is already something in this file, you can discard it. When undoing the changes, WSL will automatically regenerate this file anyway, so you don't need to back it up.\u003C/p>\n\u003Cp>Delete all the contents and enter the IP addresses you noted down in the last step in the following format:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-resolv\">nameserver xxx.xxx.xxx.xxx\n\u003C/code>\u003C/pre>\n\u003Cp>Put each address on a new line, preceded by the string \u003Ccode>nameserver\u003C/code>.\nSave the file and restart WSL with the same command as above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open up WSL for the last time and set the immutable flag for the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">chattr +i /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>And for the last time shut down WSL. Your DNS should now be working fine!\u003C/p>\n\u003Ch2>Undoing those changes\u003C/h2>\n\u003Cp>I did not have a need to undo the steps for \u003Ccode>Solution 1\u003C/code>, and I'm pretty sure the metric resets after each system reboot anyway so there is not much to do.\u003C/p>\n\u003Cp>To get DNS working again when not connected to the VPN run the following commands:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo chattr -i /etc/resolv.conf\nsudo rm /etc/resolv.conf\nsudo rm /etc/wsl.conf\nwsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>This will first clear the immutable flag off \u003Ccode>/etc/resolv.conf\u003C/code>, and delete it. Next, it will delete \u003Ccode>/etc/wsl.conf\u003C/code> if you have a backup of a previous \u003Ccode>wsl.conf\u003C/code> file, you can replace it with that. At last, we shutdown WSL again for the changes to take effect.\u003C/p>\n\u003Cp>Unfortunately, this is quite a procedure to get a VPN to work with WSL2, but I'm hopeful that this will soon not be necessairy anymore.\u003C/p>","blog/2023-03-21-anyconnect-wsl2","c67bc4dc-4c96-41b1-afb5-15a99457dedf",["Date","2023-03-15T15:22:04.511Z"],["Date","2023-03-15T15:22:04.511Z"],[8],"Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN","I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.","https://media.tiim.ch/66ca4290-3fc0-450f-977b-f00f888e4af3.webp","Stable Diffusion - Anything V3.0 - 1boy, hacker, in front of computer, back of head visible, vintage neon color scheme, terminal, big monitor",[92,93,94,95],"wsl","vpn","networking","dns","\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>",[95,94,93,92],[],{"html":100,"slug":101,"uuid":102,"date":103,"created":104,"aliases":105,"title":106,"published":10,"modified":8,"description":107,"cover_image":-1,"cover_image_txt":108,"content_tags":109,"abstract":113,"tags":114,"links":-1,"type":21,"folder":22,"comments":115,"latestComment":24},"\u003Cp>Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message \u003Ccode>exec /app/indiego: no such file or directory\u003C/code>. I had removed the \u003Ccode>CGO_ENABLE=0\u003C/code> variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the \u003Ccode>scratch\u003C/code> image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.\u003C/p>\n\u003Cp>To include libc into the container, I simply changed the base image from \u003Ccode>scratch\u003C/code> to \u003Ccode>alpine\u003C/code>, which includes libc. This makes the image slightly larger but this seemed way easier than trying to include libc directly.\u003C/p>\n\u003Cp>As a bonus I got to delete the \u003Ccode>/usr/share/zoneinfo\u003C/code> and \u003Ccode>ca-certificates.crt\u003C/code> files, and rely on those provided by alpine.\u003C/p>\n\u003Cp>You can see the commit to IndieGo \u003Ca href=\"https://github.com/Tiim/IndieGo/commit/63968814de7e39f295386bf398b583aa8bf0411c\" rel=\"nofollow noopener noreferrer\">here\u003C/a>.\u003C/p>","blog/2023-01-24-no-such-file-or-directory-cgo","dd580343-9e0f-4754-93dd-25667e6b5859",["Date","2023-01-24T00:00:00.000Z"],["Date","2023-01-24T20:54:11.330Z"],[8],"\"no such file or directory\" after enabling CGO in Docker","Quick fix for the \"no such file or directory\" error after enabling CGO, when running in a scratch docker image.","",[110,111,112],"go","cgo","docker","\u003Cp>Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message \u003Ccode>exec /app/indiego: no such file or directory\u003C/code>. I had removed the \u003Ccode>CGO_ENABLE=0\u003C/code> variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the \u003Ccode>scratch\u003C/code> image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.\u003C/p>",[111,112,110],[],{"html":117,"slug":118,"uuid":119,"date":120,"created":121,"aliases":122,"title":123,"published":10,"modified":124,"description":125,"cover_image":126,"cover_image_txt":127,"content_tags":128,"abstract":129,"tags":130,"links":-1,"type":21,"folder":22,"comments":131,"latestComment":24},"\u003Cp>I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used \u003Ca href=\"https://hexchat.github.io/\" rel=\"nofollow noopener noreferrer\">HexChat\u003C/a> in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.\u003C/p>\n\u003Cp>The one client I have seen mentioned a lot is \u003Ca href=\"https://weechat.org/\" rel=\"nofollow noopener noreferrer\">WeeChat\u003C/a> (not to be confused with WeChat, the Chinese instant messenger). WeeChat runs in the terminal as a \u003Ca href=\"https://en.wikipedia.org/wiki/Text-based_user_interface\" rel=\"nofollow noopener noreferrer\">TUI\u003C/a> and after a while of getting used to (and after enabling 'mouse mode') it seems intuitive enough.\u003C/p>\n\u003Cp>The nice thing about WeeChat running not as a graphical application, is that it makes it possible to run on a server and access it from anywhere over ssh.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>INFO\u003C/span>\u003Cp>Except on mobile devices, but weechat has mobile apps that can connect to it directly.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Since I pretty much host all my selfhosted software in docker on a VPS, I was looking if someone already published a docker image for WeeChat. There is a bunch of them, but only \u003Ca href=\"https://hub.docker.com/r/weechat/weechat\" rel=\"nofollow noopener noreferrer\">weechat/weechat\u003C/a> (the official image) is still updated regularly. The docker hub page does not have any documentation, but I managed to find it in the \u003Ca href=\"https://github.com/weechat/weechat-container\" rel=\"nofollow noopener noreferrer\">weechat/weechat-container\u003C/a> github repo.\u003C/p>\n\u003Cp>As it says in the readme on github, you can start the container with\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">docker run -it weechat/weechat\n\u003C/code>\u003C/pre>\n\u003Cp>which will run weechat directly in the foreground.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>Don't skip the \u003Ccode>-it\u003C/code> command line flags. The \u003Ccode>-i\u003C/code> or \u003Ccode>--interactive\u003C/code> keeps stdin open, which is required to send input to weechat. Weechat also closes immediately if the stdin gets closed, which took me a while to figure out.\nThe \u003Ccode>-t\u003C/code> or \u003Ccode>--tty\u003C/code> flag is required to provide a fake tty to the container. I don't really understand what that means but without this you won't see the user interface of weechat.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Running in the foreground is not really that helpful if we want to run weechat on a server, so we need to detach (let it run in the background) from the container with the \u003Ccode>-d\u003C/code> or \u003Ccode>--detach\u003C/code> flag. It also helps to specify a name for the container with the \u003Ccode>--name <name>\u003C/code> argument, so we can quickly find the container again later. The docker command now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">docker run -it -d --name weechat weechat/weechat\n\u003C/code>\u003C/pre>\n\u003Cp>When we run this command, we will notice that weechat is running in the background. To access it we can run \u003Ccode>docker attach weechat\u003C/code>. To detach from weechat without exiting the container, we can press \u003Ccode>CTRL-p CTRL-q\u003C/code> as described in the \u003Ca href=\"https://docs.docker.com/engine/reference/commandline/attach/#description\" rel=\"nofollow noopener noreferrer\">docker attach reference\u003C/a>\u003C/p>\n\u003Cp>I noticed that there are two versions of the weechat image: a debian version and an alpine linux version. Generally the Alpine Linux versions of containers are smaller than the Debian versions, so I decided to use the alpine version: \u003Ccode>weechat/weechat:latest-alpine\u003C/code>.\u003C/p>\n\u003Cp>With this we are practically done, but if we ever remove and restart the container, all of the chat logs and customisations to weechat will be gone. To prevent this we need to add the config and log files to a volume.\u003C/p>\n\u003Cp>I generally use the folder \u003Ccode>~/docker/(service)\u003C/code> to point my docker volumes to, so I have a convenient place to inspect, modify and back up the data.\u003C/p>\n\u003Cp>Let's create the folder and add the volume to the docker container. I also added the \u003Ccode>--restart unless-stopped\u003C/code> flag to make sure the container gets restarted if it either exits for some reason of if docker restarts.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">mkdir -p ~/docker/weechat/data\nmkdir -p ~/docker/weechat/config\n\ndocker run -it -d --restart unless-stopped \\\n -v \"~/docker/weechat/data:/home/user/.weechat\" \\\n -v \"~/docker/weechat/config:/home/user/.config/weechat\" \\\n --name weechat weechat/weechat:latest-alpine`\n\u003C/code>\u003C/pre>\n\u003Cp>Running this command on the server is all we need to have weechat running in docker.\u003C/p>\n\u003Cblockquote>\n\u003Cp>But how do I quickly connect to weechat? Do I always have to first ssh into the server and then run docker attach?\u003C/p>\n\u003C/blockquote>\n\u003Cp>Yes but, as almost always, we can simplify this with a bash script:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-bash\">#!/usr/bin/env bash\n\nHOST=<ssh host>\nssh -t \"${HOST}\" docker attach weechat\n\u003C/code>\u003C/pre>\n\u003Cp>This bash script starts ssh with the \u003Ccode>-t\u003C/code> flag which tells ssh that the command is interactive.\nCopy this script into your \u003Ccode>~/.local/bin\u003C/code> folder and make it executable.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">nano ~/.local/bin/weechat.sh\nchmod +x weechat.sh\n\u003C/code>\u003C/pre>\n\u003Cp>And that's it! Running \u003Ccode>weechat.sh\u003C/code> will open an ssh session to your server and attach to the weechat container. Happy Chatting!\u003C/p>\n\u003Cp>If you liked this post, consider subscribing to my blog via \u003Ca href=\"https://tiim.ch/blog/rss.xml\" rel=\"nofollow noopener noreferrer\">RSS\u003C/a>, or on \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">social media\u003C/a>. If you have any questions, feel free to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>. I also usually hang out in \u003Ca href=\"irc://irc.libera.chat/##tiim\">\u003Ccode>##tiim\u003C/code> on irc.libera.chat\u003C/a>. My name on IRC is \u003Ccode>tiim\u003C/code>.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Update 2022-01-18\u003C/span>\u003Cp>I have found that at the beginning of a session, the input to weechat doesn't seem to work. Sometimes weechat refuses to let me type anything and/or doesn't recognize mouse events.\nAfter a while of spamming keys and \u003Ccode>Alt-m\u003C/code> (toggle mouse mode), it seems to fix itself most of the time.\nI have no idea if thats a problem with weechat, with docker or with ssh, and so far have not found a solution for this. If you have the same problem or even know how to fix it, feel free to reach out.\u003C/p>\n\u003C/blockquote>","blog/2023-01-15-weechat-docker","889ff4db-3ccb-4ab1-9676-a2b0ea8f19eb",["Date","2023-01-15T00:00:00.000Z"],["Date","2023-01-15T00:17:07.000Z"],[8],"Running the WeeChat IRC Client on a VPS in Docker",["Date","2023-01-18T11:34:27.000Z"],"Walkthrough on how to setup the WeeChat IRC client in docker.","https://media.tiim.ch/a28c65a1-ed95-43d3-af87-a2ad222bee7f.jpg","Stable Diffusion - anime landscape, pastel colors, thick outlines, forest, mountains, golden light",[76,73,112],"\u003Cp>I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used \u003Ca href=\"https://hexchat.github.io/\">HexChat\u003C/a> in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.\u003C/p>",[112,76,73],[132,138,143,148],{"id":133,"type":134,"replyTo":108,"timestamp":135,"page":118,"url":136,"content":69,"name":137},"52b7b3e6-e233-4379-8f0c-3332aed562a6","webmention","2023-03-28T10:10:56Z","https://tiim.ch/blog/2023-03-28-weechat-notification-ntfy","Tim Bachmann",{"id":139,"type":134,"replyTo":108,"timestamp":140,"page":118,"url":141,"content":108,"name":142},"f6f58ebf-a68f-415e-baed-cb8bf38189fd","2023-03-02T00:05:49Z","https://brid.gy/like/twitter/tiimb/1614403118258601987/1557313575072546816","DM Cyber Security",{"id":144,"type":134,"replyTo":108,"timestamp":145,"page":118,"url":146,"content":108,"name":147},"45d40e9f-6498-4432-bdb0-01210e55d092","2023-01-25T18:20:43Z","https://brid.gy/like/twitter/tiimb/1614403118258601987/8717982","Christopher Scott",{"id":149,"type":134,"replyTo":108,"timestamp":150,"page":118,"url":151,"content":152,"name":153},"979c42d0-a8fc-4a52-a85d-bedb672fb144","2023-01-25T09:22:51Z","https://brid.gy/repost/twitter/tiimb/1614403118258601987/1618133427139792898","New blog post: A walkthrough on how to set up the WeeChat IRC client in docker. #irc #docker @WeeChatClient\ntiim.ch/blog/2023-01-1…","WeeChat",{"html":155,"slug":156,"uuid":157,"date":158,"aliases":159,"title":160,"published":10,"modified":8,"description":161,"cover_image":162,"cover_caption":163,"content_tags":164,"abstract":169,"tags":170,"links":-1,"type":21,"folder":22,"comments":175,"latestComment":24},"\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\" rel=\"nofollow noopener noreferrer\">TOS\u003C/a>:\u003C/p>\n\u003Cblockquote>\n\u003Cp>[...] Also, don't use Imgur to host image libraries you link to from elsewhere, content for your website, advertising, avatars, or anything else that turns us into your content delivery network.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Finally when I started \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">indie-webifying my website\u003C/a>, and was implementing the micropub protocol (which I will blog about at a later time), I decided that it was at the time to host the images on a platform that was meant to do that. I looked at a few storage providers such as cloudinary and S3 based object storage and landed on \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">Storj.io\u003C/a>, mostly because of the generous free tier, which should suffice for this little blog for quite a while.\u003C/p>\n\u003Cp>One thing that bothered me slightly was that all storage providers I looked at charge for traffic. It's not the fact that it's an additional expense (if your not in the free tier anymore) that bothers me, but the fact that I don't have any control over how much this will cost me. In all likelihood this will never cost me anything since this blog has not much traffic, but if a post were to go viral (one can dream...), this could result in a surprise bill at the end of the month.\u003C/p>\n\u003Cp>To help with the traffic costs I decided to try to use the free CDN functionality of Cloudflare to reduce the traffic to Storj. In this blog post I will describe how I did that.\u003C/p>\n\u003Ch2>Is this the right solution for you?\u003C/h2>\n\u003Cp>If you are in a similar situation as me, and just want to have somewhere to host your images for a personal website or to share images or screenshots as links while still having control over all your data, this could be a good solution.\u003C/p>\n\u003Cp>If you want to build a robust image pipeline with resizing and image optimization, or you are building an enterprise website this is probably not the right way. You should take a look at cloudinary or one of the big cloud providers.\u003C/p>\n\u003Ch2>Prerequisites\u003C/h2>\n\u003Cp>To use Cloudflare as a CDN, you need to have Cloudflare setup as your DNS host for the domain you want to serve the images from. Even if you just want to use a subdomain like \u003Ccode>media.example.com\u003C/code>, the whole \u003Ccode>example.com\u003C/code> domain needs to be on cloudflare. For me this was not much of an issue, I followed the instructions from cloudflare and pointed the nameserver of my domain to cloudflare. Although I did have an issue during the migration, which resulted in my website being down for two hours. But I'm pretty sure this was caused by my previous nameserver provider.\u003C/p>\n\u003Ch2>Setting up Storj & Cloudflare\u003C/h2>\n\u003Cp>I assume you already have an account at \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">storj.io\u003C/a>. The next step is creating a bucket for your images. A bucket is just a place for your files and folders to live in storj, just like in any other S3 compatible storage provider. (Actually there are no folders in storj and other S3 services, the folders are just prefixes of the filenames). When creating a bucket, make sure you save the passphrase securely, such as in your password manager. Whenever storj asks you for the passphrase, make sure you don't let storj generate a new one! Every new passphrase will create access to a new bucket.\u003C/p>\n\u003Cp>The next step is \u003Ca href=\"https://docs.storj.io/dcs/downloads/download-uplink-cli\" rel=\"nofollow noopener noreferrer\">installing the uplink cli\u003C/a>. Follow the quick start tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object\" rel=\"nofollow noopener noreferrer\">get an access grant\u003C/a>. Remember to use the same passphrase from above. Now follow the next quickstart tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object/set-up-uplink-cli\" rel=\"nofollow noopener noreferrer\">add the bucket to the uplink cli\u003C/a>. The file \u003Ccode>accessgrant.txt\u003C/code> in the tutorial only contains the access-grant string that you got from the last step.\u003C/p>\n\u003Cp>Finally we want to share the bucket so the images can be accessed from the web. For this you can run the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">uplink share --dns <domain> sj://<bucket>/<prefix> --not-after=none\n\u003C/code>\u003C/pre>\n\u003Cp>Replace \u003Ccode><domain>\u003C/code> with the domain you want to serve the images from. In my case I use \u003Ccode>media.tiim.ch\u003C/code>. Then replace \u003Ccode><bucket>\u003C/code> with the name of your bucket and \u003Ccode><prefix>\u003C/code> with the prefix.\u003C/p>\n\u003Cp>As mentioned above, you can think of a prefix as a folder. If you use for example \u003Ccode>media-site1\u003C/code> as a prefix, then every file in the \"folder\" \u003Ccode>media-site1\u003C/code> will be shared. This means you can use multiple prefixes to serve files for multiple websites in the same bucket.\u003C/p>\n\u003Cp>You will get the following output:\u003C/p>\n\u003Cpre>\u003Ccode>[...]\n=========== DNS INFO =====================================================================\nRemember to update the $ORIGIN with your domain name. You may also change the $TTL.\n$ORIGIN example.com.\n$TTL 3600\nmedia.example.com IN CNAME link.storjshare.io.\ntxt-media.example.com IN TXT storj-root:mybucket/myprefix\ntxt-media.example.com IN TXT storj-access:totallyrandomstringofnonsens\n\u003C/code>\u003C/pre>\n\u003Cp>Create the DNS entries in Cloudflare with the values printed in the last three lines. Make sure you enable the proxy setting when entering the CNAME entry to enable Cloudflares CDN service.\u003C/p>\n\u003Cp>And that's it. All files you put in the bucket with the correct prefix are now available under your domain! :)\u003C/p>\n\u003Cp>If this blog post helped you, or you have some issues or thoughts on this, leave a comment via the comment box below or via webmention.\u003C/p>","blog/2022-12-storj-cloudflare-image-hosting","6d5a964d-328e-43d7-9189-40280b012074",["Date","2022-12-03T13:37:33.000Z"],[8],"Hosting Images with Storj and Cloudflare","Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.","https://media.tiim.ch/d280fad4-632a-4b5a-b6b2-6a5c0026b61c.jpg","Image generated by Dall-E: travel postcards scattered on grass, top down view, photoreal",[165,166,167,168],"CDN","IndieWeb","Cloudflare","Storj","\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\">TOS\u003C/a>:\u003C/p>",[171,172,173,174],"cdn","cloudflare","indieweb","storj",[176,181],{"id":177,"type":134,"replyTo":108,"timestamp":178,"page":156,"url":179,"content":108,"name":180},"edeb5ddb-357a-41cf-b2b2-704df571d70c","2023-01-11T06:03:38Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/1570290779419017216","Laura Forster",{"id":182,"type":134,"replyTo":108,"timestamp":183,"page":156,"url":184,"content":108,"name":185},"2331980c-acee-4549-a260-61b4119c63a9","2022-12-05T06:12:20Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/275384865","kevin",{"html":187,"slug":188,"uuid":189,"date":190,"aliases":191,"title":192,"published":10,"modified":193,"description":194,"cover_image":195,"content_tags":196,"syndication":201,"abstract":203,"tags":204,"links":-1,"type":21,"folder":22,"comments":206,"latestComment":24},"\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\" rel=\"nofollow noopener noreferrer\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\" rel=\"nofollow noopener noreferrer\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>\n\u003Ch2>The IndieWeb\u003C/h2>\n\u003Cp>I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.\u003C/p>\n\u003Cp>The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.\u003C/p>\n\u003Cp>The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.\u003C/p>\n\u003Cp>The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.\u003C/p>\n\u003Cp>Some of the standards in the IndieWeb include:\u003C/p>\n\u003Cul>\n\u003Cli>Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.\u003C/li>\n\u003Cli>Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.\u003C/li>\n\u003Cli>IndieAuth, an OAuth2-based way to log in using only your domain name.\u003C/li>\n\u003C/ul>\n\u003Ch2>The implementation on my website\u003C/h2>\n\u003Cp>As explained in my earlier post \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">First Go Project: A Jam-stack Commenting API\u003C/a>, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.\u003C/p>\n\u003Ch3>Making the website machine-readable with Microformats\u003C/h3>\n\u003Cp>As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called \u003Ca href=\"http://microformats.org/wiki/h-entry\" rel=\"nofollow noopener noreferrer\">h-entry\u003C/a>. By adding the \u003Ccode>h-entry\u003C/code> class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as \u003Ccode>p-name\u003C/code>, \u003Ccode>p-author\u003C/code> or \u003Ccode>dt-published\u003C/code>.\u003C/p>\n\u003Cp>While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.\u003C/p>\n\u003Cp>Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.\u003C/p>\n\u003Ch3>Accepting comments and other interactions via Webmentions\u003C/h3>\n\u003Cp>The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.\u003C/p>\n\u003Cp>In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.\u003C/p>\n\u003Cp>Since I already have a \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">small custom service\u003C/a> running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions\u003C/p>\n\u003Cp>It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database.\nI found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.\u003C/p>\n\u003Cp>I have tested my webmentions implementation using \u003Ca href=\"https://webmention.rocks\" rel=\"nofollow noopener noreferrer\">webmention.rocks\u003C/a>, but I would appreciate it if you left me a mention as well 😃\u003C/p>\n\u003Ch3>Publishing short-form content such as replies, likes and bookmarks: A notes post type\u003C/h3>\n\u003Cp>The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">notes\u003C/a>. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.\u003C/p>\n\u003Cp>I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: \u003Ca href=\"https://tiim.ch/full-rss.xml\" rel=\"nofollow noopener noreferrer\">full-rss.xml\u003C/a>. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.\u003C/p>\n\u003Ch3>Notifying referenced websites: Sending Webmentions\u003C/h3>\n\u003Cp>Sending webmentions was easy compared to receiving webmentions:\u003C/p>\n\u003Cp>On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.\u003C/p>\n\u003Cp>Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library \u003Ca href=\"https://github.com/go-co-op/gocron\" rel=\"nofollow noopener noreferrer\">gocron\u003C/a> for scheduling the regular intervals, \u003Ca href=\"https://github.com/mmcdole/gofeed\" rel=\"nofollow noopener noreferrer\">gofeed\u003C/a> for parsing the RSS feed and \u003Ca href=\"https://willnorris.com/go/webmention\" rel=\"nofollow noopener noreferrer\">webmention\u003C/a> for extracting links and sending webmentions.\u003C/p>\n\u003Ch3>In the future: IndieAuth\u003C/h3>\n\u003Cp>The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.\u003C/p>\n\u003Cp>Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.\u003C/p>\u003Cdiv class=\"mf2\">\u003Cblockquote class=\"syndication\">This post is also on \u003Cul>\u003Cli>\u003Ca class=\"u-syndication\" href=\"https://news.indieweb.org/en\">news.indieweb.org\u003C/a>\u003C/li>\u003C/ul>\u003C/blockquote>\u003C/div>\n","blog/2022-12-indiewebifying-my-website-part-1","3b342241-c414-4670-bd22-03e13d6531b7",["Date","2022-11-12T10:55:14.000Z"],[8],"IndieWebifying my Website Part 1 - Microformats and Webmentions",["Date","2022-12-03T20:56:54.000Z"],"This site now supports sending and receiving webmentions and surfacing structured data using microformats2.","https://i.imgur.com/FpgIBxI.jpg",[166,197,198,199,110,200],"Webmentions","mf2","tiim.ch","indiego",[202],"https://news.indieweb.org/en","\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>",[110,200,173,198,199,205],"webmentions",[207,211,216,220,226,231,237,243],{"id":208,"type":134,"replyTo":108,"timestamp":24,"page":188,"url":209,"content":108,"name":210},"41435fdd-5fd2-4175-b9ea-ef9ce0dec154","https://evgenykuznetsov.org/en/reactions/2022/like-337215655/","Evgeny Kuznetsov",{"id":212,"type":134,"replyTo":108,"timestamp":213,"page":188,"url":214,"content":215,"name":137},"68bd3601-4f00-42c1-b28c-1f2ef75ac851","2023-08-02T09:10:03Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...",{"id":217,"type":134,"replyTo":108,"timestamp":218,"page":188,"url":219,"content":161,"name":137},"5f508a42-8b83-4c10-9f7e-9c1b80e23ab1","2022-12-09T10:49:06Z","https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting",{"id":221,"type":134,"replyTo":108,"timestamp":222,"page":188,"url":223,"content":224,"name":225},"dc8dbf30-ff1f-4e13-ba36-37f12666005c","2022-12-05T08:41:07Z","https://martymcgui.re/2022/12/05/033926/","★ Liked https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","https://martymcgui.re/",{"id":227,"type":134,"replyTo":108,"timestamp":228,"page":188,"url":229,"content":108,"name":230},"6f6c1f11-2ae0-41a4-b7d0-e343ef63aa52","2022-11-27T22:32:27Z","https://brid.gy/like/twitter/tiimb/1591417020557525003/48372745","Jimmy Lipham",{"id":232,"type":134,"replyTo":108,"timestamp":233,"page":188,"url":234,"content":235,"name":236},"5e1c4149-8fb9-48fb-b285-5efbd626b259","2022-11-27T22:31:57Z","https://brid.gy/repost/twitter/tiimb/1591417020557525003/1591418692008558592","I published a new blog post:\nIndieWebifying my Website Part 1 - Microformats and Webmentions\ntiim.ch/blog/2022-12-i…\n#indieweb #microformats #webmentions #golang","Golang Smart Bot",{"id":238,"type":134,"replyTo":108,"timestamp":239,"page":188,"url":240,"content":241,"name":242},"6e0bf830-1735-42a2-9aa2-ea4c40ab7a45","2022-11-15T12:47:35Z","https://webmention.rocks/receive/1/f0fa5421056e068fe902932ef98f6d71","This test verifies that you accept a Webmention request that contains a valid source and target URL. To pass this test, your Webmention endpoint must return either HTTP 200, 201 or 202 along with the appropriate headers.\nIf your endpoint returns HTTP 201, then it MUST also return a Location header. If it returns HTTP 200 or 202, then it MUST NOT include a Location header.","Webmention Rocks!",{"id":244,"type":134,"replyTo":108,"timestamp":245,"page":188,"url":246,"content":247,"name":248},"4e237d08-9f22-480e-a46e-8f40adf06c5e","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/rm8as/","Liked\nIndieWebifying my Website Part 1 - Microformats and Webmentions\nPost detailsThis site now supports sending and receiving webmentions and surfacing structured data using microformats2. https://i.imgur.com/FpgIBxI.jpg","Jamie Tanna",{"html":250,"slug":251,"uuid":252,"date":253,"created":254,"aliases":255,"title":256,"published":10,"modified":8,"description":257,"cover_image":258,"content_tags":259,"abstract":264,"tags":265,"links":-1,"type":21,"folder":22,"comments":267,"latestComment":24},"\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\" rel=\"nofollow noopener noreferrer\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>\n\u003Cp>Server-side rendering (SSR) is one of the great features of SvelteKit. I will try to keep this blog post short and will therefore not explain what server-side rendering is and why you should take advantage of it \u003Cem>(you really should!)\u003C/em>. If you want to know more about SSR you can take a look at this article: \u003Ca href=\"https://towardsdev.com/server-side-rendering-srr-in-javascript-a1b7298f0d04\" rel=\"nofollow noopener noreferrer\">A Deep Dive into Server-Side Rendering (SSR) in JavaScript\u003C/a>.\u003C/p>\n\u003Ch2>Background - SSR in SvelteKit\u003C/h2>\n\u003Cp>SvelteKit implements SSR by providing a \u003Ca href=\"https://kit.svelte.dev/docs/load\" rel=\"nofollow noopener noreferrer\">\u003Ccode>load\u003C/code> function\u003C/a> for every layout and page component. If a page or layout needs to perform some asynchronous operation, this should be done inside of this load function. SvelteKit executes this function asynchronously on the server side as well as on the client side and the return value of this function is assigned to the \u003Ccode>data\u003C/code> prop of the associated component. Usually, this asynchronous operation is loading data from an external service, like in the case of this blog post a GraphQL server.\nYou can of course load data directly in the component, but SvelteKit will not wait for this to complete when doing SSR, and the resulting HTML will not include the loaded data.\u003C/p>\n\u003Ch2>Background - @urql/svelte\u003C/h2>\n\u003Cp>The urql library allows us to easily issue GraphQL queries and mutations. Some of the functionality it has to make our lives easier include:\u003C/p>\n\u003Cul>\n\u003Cli>Reloading a query when a query variable changes\u003C/li>\n\u003Cli>Reloading a query after a mutation that touches the same data as the query\u003C/li>\n\u003C/ul>\n\u003Cp>We want to keep these features, even when using urql when doing SSR.\u003C/p>\n\u003Ch2>The Problem\u003C/h2>\n\u003Cp>When implementing SSR in my project, I ran into two problems. I couldn't find any documentation or any articles solving them, so I decided to write down my solutions to those problems in this blog post.\u003C/p>\n\u003Ch3>Problem 1 - Svelte and urql Reactivity\u003C/h3>\n\u003Cp>Let's say we have the following load function, which executes a GraphQL query to load a list of red cars:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\n\n/** @type {import('./$types').PageLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const carColor = \"red\";\n\n const cars = client\n .query(carsQuery, {\n color: carColor,\n })\n .toPromise()\n .then((c) => c.data?.car);\n\n return {\n cars,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>This example uses the urql method \u003Ccode>client.query\u003C/code> to start a query to get us a list of cars with a red colour (The GraphQL query is not shown but the exact query is not important for this example).\nThe client gets a \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">special fetch function\u003C/a> from the event which has a few nice properties, like preventing a second network request on the client side if that same request was just issued on the server-side.\u003C/p>\n\u003Cp>Since the query code is now located in the load function and not in a svelte component, there is no way to easily change the \u003Ccode>carColor\u003C/code> and have urql automatically reload the query. The only way to change the variable is to set the value as a query parameter and read that from the \u003Ccode>event\u003C/code> argument. This however means that we have to refresh the whole page just to reload this query.\u003C/p>\n\u003Cp>The other thing urql does for us, reloading the query when we do a mutation on the same data, will not work with the above code either.\u003C/p>\n\u003Ch3>The solution: A query in the load function and a query in the component\u003C/h3>\n\u003Cp>To fix those two drawbacks we have to add the same query as in the load function to our component code as well. Unfortunately, this means when a user loads the page, it sends a request from the client side, even though the same request got sent from the server side already.\u003C/p>\n\u003Cp>I created a small wrapper function \u003Ccode>queryStoreInitialData\u003C/code> that creates the query inside of the component and intelligently switches from the (possibly stale) data from the load function to the new data. Using this wrapper, the page or layout might look as follows:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-svelte\"><script>\n import { queryStoreInitialData } from \"@/lib/gql-client\"; // The helper function mentioned above\n import { getContextClient } from \"@urql/svelte\";\n import { carsQuery } from \"./query\"; // The query\n\n export let data;\n\n $: gqlStore = queryStoreInitialData(\n {\n client: getContextClient(),\n query: carsQuery,\n },\n data.cars\n );\n $: cars = $gqlStore?.data?.car;\n</script>\n\n<div>\n <pre>\n {JSON.stringify(cars, null, 2)}\n </pre>\n</div>\n\u003C/code>\u003C/pre>\n\u003Col>\n\u003Cli>The native \u003Ccode>queryStore\u003C/code> function gets replaced with the wrapper function.\u003C/li>\n\u003Cli>The initial value of the query is supplied to the wrapper\u003C/li>\n\u003C/ol>\n\u003Cp>Unfortunately, we can not return the query result from the load function directly like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const result = await client.query(cars, {}).toPromise();\n\nreturn {\n cars: toInitialValue(result),\n};\n\u003C/code>\u003C/pre>\n\u003Cp>This results in the following error:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-stacktrace\">Cannot stringify a function (data.events.operation.context.fetch)\nError: Cannot stringify a function (data.events.operation.context.fetch)\n at render_response (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/render.js:181:20)\n at runMicrotasks (<anonymous>)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async render_page (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/index.js:276:10)\n at async resolve (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:232:17)\n at async respond (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:284:20)\n at async file:///app/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:406:22\n\u003C/code>\u003C/pre>\n\u003Cp>This is because the query result contains data that is not serializable.\nTo fix this I created the \u003Ccode>toInitialValue\u003C/code> function, which deletes all non-serializable elements from the result. The load function now looks like follows;\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\nimport { createServerClient, toInitialValue } from \"@/lib/gql-client\";\nimport { parse } from \"cookie\";\nimport { carsQuery } from \"./query\";\n\n/** @type {import('./$types').PageServerLoad} */\nexport const load = async (event) => {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const result = await client.query(cars, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n};\n\u003C/code>\u003C/pre>\n\u003Ch3>Problem 2 - Authentication\u003C/h3>\n\u003Cp>We will look at the same \u003Ccode>load\u003C/code> function as #Problem 1 - Svelte and urql Reactivity: the function creates a urql client with the fetch function from the event object and uses this client to send a query.\u003C/p>\n\u003Cp>Sometimes however the GraphQL API requires authentication in the form of a cookie to allow access.\u003C/p>\n\u003Cp>Unfortunately, the \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">fetch function that we get from the load event\u003C/a> will only pass the cookies on if the requested domain is the same as the base domain or a more specific subdomain of it. This means if your SvelteKit site runs on \u003Ccode>example.com\u003C/code> and your GraphQL server runs on \u003Ccode>gql.example.com\u003C/code> then the cookies will get forwarded and everything is fine. This however is, in my experience, often not the case. Either you might use an external service for your GraphQL API or you host it yourself and want to use its internal domain.\u003C/p>\n\u003Cp>The only way to pass the cookies on to the GraphQL server, in this case, is by manually setting the cookie header when creating the urql client. This however forces us to use the server-only load function, as we do not have access to the cookie header in the normal load function.\u003C/p>\n\u003Cp>The new code now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch,\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // inject the cookie header\n // FIXME: change the cookie name\n Cookie: `gql-session=${event.cookies.get(\"gql-session\")}`,\n },\n },\n });\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>To keep the size of the load functions across my codebase smaller I created a small wrapper function \u003Ccode>createServerClient\u003C/code>:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createServerClient(event.cookies);\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Ch2>The Code\u003C/h2>\n\u003Cp>Below you can find the three functions \u003Ccode>createServerClient\u003C/code>, \u003Ccode>queryStoreInitialData\u003C/code> and \u003Ccode>toInitialValue\u003C/code> that we used above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/lib/gql-client.js\n\nimport { browser } from \"$app/environment\";\nimport { urls } from \"@/config\";\nimport { createClient, queryStore } from \"@urql/svelte\";\nimport { derived, readable } from \"svelte/store\";\n\n/**\n * Helper function to create an urql client for a server-side-only load function\n *\n *\n * @param {import('@sveltejs/kit').Cookies} cookies\n * @returns\n */\nexport function createServerClient(cookies) {\n return createClient({\n // FIXME: adjust your graphql url\n url: urls.gql,\n fetch,\n // FIXME: if you don't need to authenticate, delete the following object:\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // FIXME: if you want to set a cookie adjust the cookie name\n Cookie: `gql-session=${cookies.get(\"gql-session\")}`,\n },\n },\n });\n}\n\n/**\n * Helper method to send a GraphQL query but use the data from the SvelteKit load function initially.\n *\n *\n * @param {any} queryArgs\n * @param {any} initialValue\n * @returns\n */\nexport function queryStoreInitialData(queryArgs, initialValue) {\n if (!initialValue || (!initialValue.error && !initialValue.data)) {\n throw new Error(\"No initial value from server\");\n }\n\n let query = readable({ fetching: true });\n if (browser) {\n query = queryStore(queryArgs);\n }\n\n return derived(query, (value, set) => {\n if (value.fetching) {\n set({ ...initialValue, source: \"server\", fetching: true });\n } else {\n set({ ...value, source: \"client\" });\n }\n });\n}\n\n/**\n * Make the result object of a urql query serialisable.\n *\n *\n * @template T\n * @param {Promise<import('@urql/svelte').OperationResult<T, any >>|import('@urql/svelte').OperationResult<T, any >} result\n * @returns {Promise<{fetching:false, error: undefined | {name?: string, message?: string; graphQLErrors?: any[]; networkError?: Error; response?: any;}, data: T|undefined}>}\n */\nexport async function toInitialValue(result) {\n const { error, data } = await result;\n\n // required to turn class array into array of javascript objects\n const errorObject = error ? {} : undefined;\n if (errorObject) {\n console.warn(error);\n errorObject.graphQLErrors = error?.graphQLErrors?.map((e) => ({ ...e }));\n errorObject.networkError = { ...error?.networkError };\n errorObject.response = { value: \"response omitted\" };\n }\n\n return {\n fetching: false,\n error: { ...error, ...errorObject },\n data,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Ca href=\"https://gist.github.com/Tiim/1adeb4d74ce7ae09d0d0aa4176a6195d\" rel=\"nofollow noopener noreferrer\">Link to the Gist\u003C/a>\u003C/p>\n\u003Ch2>End remarks\u003C/h2>\n\u003Cp>Even though I think this solution is not too bad, I wish @urql/svelte would implement a better way to handle SSR with sveltekit. I posted a \u003Ca href=\"https://github.com/FormidableLabs/urql/discussions/2703\" rel=\"nofollow noopener noreferrer\">question on the urql GitHub discussions board\u003C/a>, but I have not gotten any response yet.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>This article was written with \u003Ccode>@svelte/kit\u003C/code> version \u003Ccode>1.0.0-next.499\u003C/code> and \u003Ccode>@urql/svelte\u003C/code> version \u003Ccode>3.0.1\u003C/code>.\nI will try to update this article as I update my codebase to newer versions.\u003C/p>\n\u003C/blockquote>\n\u003Cp>If this post helped you, or you found a better or different way to solve SSR with urql, please let me know in the comments, write me an email or tag me on twitter \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a>.\u003C/p>","blog/2022-09-27-sveltekit-ssr-with-urql","1e223cab-bca2-4b3b-a75a-71f158c90cba",["Date","2022-09-26T00:00:00.000Z"],["Date","2022-09-26T08:55:23.886Z"],[8],"SvelteKit Server-Side Rendering (SSR) with @urql/svelte","Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.","https://i.imgur.com/5DBIbbT.png",[260,261,262,263],"urql","sveltekit","SSR","graphql","\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>",[263,266,261,260],"ssr",[268],{"id":269,"type":270,"replyTo":108,"timestamp":271,"page":251,"url":272,"content":273,"name":274},"a38babef-2c4c-41e6-a748-8723b6cc34ef","comment","2023-02-05T00:02:51Z","https://tiim.ch/blog/2022-09-27-sveltekit-ssr-with-urql#a38babef-2c4c-41e6-a748-8723b6cc34ef","Hi\n\ninspiring article. Anyway, inspired with it I tried to find more seamless integration - get SSR rendered queries but keep original interface. \n\nYou may be interested in my approach, it's dropped in discussion you open \nhttps://github.com/urql-graphql/urql/discussions/2703","Farin",{"html":276,"slug":277,"uuid":278,"date":279,"created":280,"aliases":281,"title":282,"published":10,"modified":283,"description":284,"cover_image":285,"content_tags":286,"abstract":289,"tags":290,"links":-1,"type":21,"folder":22,"comments":291,"latestComment":24},"\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\" rel=\"nofollow noopener noreferrer\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>\n\u003Cp>I kept my eyes open for a service that I liked, but I did not want to just integrate any old service into my website, I did have some requirements:\u003C/p>\n\u003Cul>\n\u003Cli>The service should not cost anything. I would rather host something myself than sign up for another subscription (because I'm already paying for a VPS anyway).\u003C/li>\n\u003Cli>I want to control how the comments on my website are displayed. I quite like my website design and I don't want a generic comment box below my posts.\u003C/li>\n\u003Cli>The service should respect the privacy of the people using my website.\u003C/li>\n\u003Cli>There should be an option to comment without setting up an account with the service.\u003C/li>\n\u003C/ul>\n\u003Cp>While looking around for how other people integrated comments into their static websites, I found a nice \u003Ca href=\"https://averagelinuxuser.com/static-website-commenting/\" rel=\"nofollow noopener noreferrer\">blog post from Average Linux User\u003C/a> which compares a few popular commenting systems.\nUnfortunately, most systems either are not very privacy-friendly, cost money or store the comments as comments on Github issues..?\nAfter looking through the options I decided to use this opportunity to write my own commenting system and dabble with the Go programming language.\u003C/p>\n\u003Ch2>Writing a commenting API in Go\u003C/h2>\n\u003Cp>First thing first, if you want to take a look at the code, check out the \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">Github repo\u003C/a>.\u003C/p>\n\u003Cp>I decided to write the commenting system in Go because I have been looking for an excuse to practice Go for a while, and this seemed like the perfect fit. It is a small CRUD app, consisting of a storage component, an API component and a small event component in the middle to easily compose the functionality I want.\u003C/p>\n\u003Cp>Currently, it supports the following functionality:\u003C/p>\n\u003Cul>\n\u003Cli>Listing all comments (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Listing all comments for a specified page (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Posting comments through the API\u003C/li>\n\u003Cli>A simple admin dashboard that lists all comments and allows the admin to delete them\u003C/li>\n\u003Cli>Email notifications when someone comments\u003C/li>\n\u003Cli>Email notifications when someone replies to your comment\u003C/li>\n\u003Cli>SQLite storage for comments\u003C/li>\n\u003C/ul>\n\u003Cp>The code is built in a way to make it easy to customise the features.\nFor example to disable features like the email reply notifications you can just \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/main.go#L52\" rel=\"nofollow noopener noreferrer\">comment out the line in the main.go\u003C/a> file that registers that hook.\u003C/p>\n\u003Cp>To write custom hooks that get executed when a new comment gets submitted or one gets deleted, just implement the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/event/handler.go\" rel=\"nofollow noopener noreferrer\">Handler\u003C/a> interface and register it in the main method.\u003C/p>\n\u003Cp>You can also easily add other storage options like databases or file storage by implementing the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/model/store.go\" rel=\"nofollow noopener noreferrer\">Store and SubscribtionStore\u003C/a> interfaces.\u003C/p>\n\u003Ch2>Can it be used in production? 🚗💨\u003C/h2>\n\u003Cp>I currently use it on this website! Go test it out (I might delete the comments if they are rude though 🤔).\u003C/p>\n\u003Cp>In all seriousness, I would not use it for a website where the comments are critical. But for a personal blog or similar, I don't see why not.\u003C/p>\n\u003Cp>If you want to host your own version, there is a Dockerfile available. If you decide to integrate this into your website, please comment below, ping me \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> or shoot me an email \u003Ca href=\"mailto:hey@tiim.ch\">hey@tiim.ch\u003C/a>, I would love to check it out.\u003C/p>","blog/2022-07-12-first-go-project-commenting-api","bff14052-4f3f-4dcb-bcee-155ae1c6b09e",["Date","2022-07-12T00:00:00.000Z"],["Date","2022-07-08T16:24:37.766Z"],[8],"First Go Project: A Jam-stack Commenting API",["Date","2022-11-23T21:42:29.000Z"],"I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!","/assets/2022-07-first-go-project-commenting-api.png",[110,287,288,199,200],"web-api","project","\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>",[110,200,288,199,287],[292,295,301,308,313,319,323,328,335,341,346],{"id":293,"type":134,"replyTo":108,"timestamp":294,"page":277,"url":214,"content":215,"name":137},"31ec5f44-15b2-498a-890d-350e38b9a83e","2023-08-02T09:10:04Z",{"id":296,"type":270,"replyTo":108,"timestamp":297,"page":277,"url":298,"content":299,"name":300},"6d792d24-ba58-4408-83a4-3583667ff4ad","2023-07-09T19:25:02Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#6d792d24-ba58-4408-83a4-3583667ff4ad","Heya just saw your post on Reddit about this comment feature, didn't want to leave without using it ^^. Nicely done!","Anonymous",{"id":302,"type":270,"replyTo":303,"timestamp":304,"page":277,"url":305,"content":306,"name":307},"99dd9ccf-5349-4f41-9553-67986e1a1074","1c8ba0da-10df-4a7a-b067-55875441de2d","2022-12-07T23:01:37Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#99dd9ccf-5349-4f41-9553-67986e1a1074","You are right, there is not any documentation in the readme yet. Although hopefully, I will work on that soon. I'm in the middle of refactoring the project.\n\nTo query the comments there are two rest endpoints [\"/comment\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L41-L71) and [\"/comment/:page\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L73-L107) which return all comments or the comments for a specific page. The comments are loaded from this API endpoint when the site is generated.\n\nTo display the comments without rebuilding the site, new comments are fetched in the browser with the \"?since=\u003Ctime-of-last-build>\" query parameter.","Tiim",{"id":303,"type":270,"replyTo":108,"timestamp":309,"page":277,"url":310,"content":311,"name":312},"2022-12-07T22:33:44Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#1c8ba0da-10df-4a7a-b067-55875441de2d","Good stuff. Always nice to see a site supporting comments and/or Webmentions. Maybe I missed it in the readme but I am curious as to how one queries the API for comments. Do you pull in the comments from the database when you generate the site?","Poorchop",{"id":314,"type":134,"replyTo":108,"timestamp":315,"page":277,"url":316,"content":317,"name":318},"e42756e4-d5a5-4727-8c58-434d285b7ab3","2022-11-27T22:31:58Z","https://brid.gy/repost/twitter/tiimb/1546801590593638400/1546801615264415745","I published a new blog post:\nFirst Go Project: A jam-stack Commenting API\ntiim.ch/blog/2022-07-1…\n#golang #jamstack #API","Golang Bot",{"id":320,"type":134,"replyTo":108,"timestamp":321,"page":277,"url":322,"content":194,"name":137},"b8d3ae8b-0059-4379-8c35-30c97269908f","2022-11-21T22:19:23Z","https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1",{"id":324,"type":270,"replyTo":108,"timestamp":325,"page":277,"url":326,"content":327,"name":327},"171e3444-f1d0-492d-8bc7-c0a133a41783","2022-07-18T08:44:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#171e3444-f1d0-492d-8bc7-c0a133a41783","hola",{"id":329,"type":270,"replyTo":330,"timestamp":331,"page":277,"url":332,"content":333,"name":334},"5875bba1-e69b-467a-bab5-23ef4160d257","621574fd-eea2-48d6-87c8-aebd0f05f1aa","2022-07-13T21:06:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#5875bba1-e69b-467a-bab5-23ef4160d257","And a polite reply","polite",{"id":336,"type":270,"replyTo":108,"timestamp":337,"page":277,"url":338,"content":339,"name":340},"8494c653-ef37-47a2-ae1b-00d00e4815a9","2022-07-13T18:31:31Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#8494c653-ef37-47a2-ae1b-00d00e4815a9","Pretty cool dudez","somGuy",{"id":330,"type":270,"replyTo":108,"timestamp":342,"page":277,"url":343,"content":344,"name":345},"2022-07-12T21:40:39Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#621574fd-eea2-48d6-87c8-aebd0f05f1aa","This is a rude comment ;)","rude",{"id":347,"type":270,"replyTo":108,"timestamp":348,"page":277,"url":349,"content":350,"name":351},"fb6278ae-e48c-4397-ba29-bec4e5cb3a57","2022-07-12T13:30:14Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#fb6278ae-e48c-4397-ba29-bec4e5cb3a57","Good job dude!","wdup",{"html":353,"slug":354,"uuid":355,"date":356,"created":357,"aliases":358,"title":359,"published":10,"modified":8,"description":360,"cover_image":361,"content_tags":362,"abstract":365,"tags":366,"links":-1,"type":21,"folder":22,"comments":367,"latestComment":24},"\u003Cp>I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.\u003C/p>\n\u003Cp>But there is another way! By subscribing to RSS feeds you are in control of what you are shown. Most websites, blogs, news sites and even social media sites provide RSS feeds to subscribe to. You get only the articles, videos or audio content you are subscribed to, without any algorithm messing with your attention.\u003C/p>\n\u003Ch2>But what exactly is an RSS feed?\u003C/h2>\n\u003Cp>RSS stands for \"Really Simple Syndication\", and it is a protocol for a website to provide a list of content. It is an old protocol, the first version was introduced in 1999, but it might be more useful nowadays than ever.\nIf you listen to podcasts, you are already familiar with RSS feeds: a podcast is an RSS feed which links to audio files instead of online articles.\nAn RSS feed is just an XML document which contains information about the feed and a list of content.\nWhen you use an app to subscribe to an RSS feed, this app will just save the URL to the XML document and load it regularly to check if new content is available. You are completely in control of how often the feed is refreshed and what feeds you want to subscribe to. Some RSS reader apps also allow you to specify some rules for example about if you should be notified, based on the feed, the content or the tags.\u003C/p>\n\u003Ch2>How to subscribe to a feed?\u003C/h2>\n\u003Cp>Since an RSS feed is just an XML document, you don't \u003Cem>technically\u003C/em> have to subscribe to a feed to read it, you \u003Cem>could\u003C/em> just open the document and read the XML. But that would be painful. Luckily there are several plugins, apps and services that allow you to easily subscribe to and read RSS feeds.\u003C/p>\n\u003Cp>If you want to start using RSS and are not sure if you will take the time to open a dedicated app, I would recommend using an RSS plugin for another software that you are using regularly. For example, the \u003Ca href=\"https://thunderbird.net/\" rel=\"nofollow noopener noreferrer\">Thunderbird\u003C/a> email client already has built-in RSS support. If you want to read to the feeds directly inside of your browser, you can use the \u003Ca href=\"https://nodetics.com/feedbro/\" rel=\"nofollow noopener noreferrer\">feedbro\u003C/a> extension for Chrome, Firefox, and other Chromium-based browsers. I use the \u003Ca href=\"https://vivaldi.com\" rel=\"nofollow noopener noreferrer\">Vivaldi\u003C/a> browser which comes with an integrated RSS feed reader.\u003C/p>\n\u003Ch2>What if there is no RSS feed?\u003C/h2>\n\u003Cp>Unfortunately not every website offers an RSS feed. Although it might be worth it to hunt for them. Some websites offer an RSS feed but do not link to it anywhere.\nIf there is no feed, but a newsletter is offered, the service \"\u003Ca href=\"https://kill-the-newsletter.com\" rel=\"nofollow noopener noreferrer\">Kill The Newsletter\u003C/a>\" will provide you with email addresses and a corresponding RSS URL to convert any newsletter to a feed. Another service to consider is \u003Ca href=\"http://fetchrss.com\" rel=\"nofollow noopener noreferrer\">FetchRSS\u003C/a>. It turns any website into an RSS feed.\u003C/p>\n\u003Ch2>RSS Apps\u003C/h2>\n\u003Cp>If you want to have a dedicated app for your reading, you're in luck! There is a plethora of apps to choose from, all with different features and user interfaces.\nThere are three main types of apps: standalone apps, service-based apps, and self-hosted apps. Most apps are standalone, meaning they fetch the RSS feeds only when open, and don't sync to your other devices. The service-based apps rely on a cloud service which will fetch the feeds around the clock, even when all your devices are off. They can also send you a summary mail if you forget to check for some time and they can sync your subscriptions across all your devices. Unfortunately, most service-based apps only offer a limited experience for free. The last category is self-hosted apps. They are similar to the service based apps but instead of some company running the service, you have to provide a server for the service to run yourself.\u003C/p>\n\u003Cp>I use a standalone app, because I do not want to rely on a service, but I also don't want to go through the hassle of setting up a self-hosted solution.\u003C/p>\n\u003Cp>If you are still unsure what RSS app you could try out, I provided a list below. Make sure to add the \u003Ca href=\"https://tiim.ch/blog/rss.xml\" rel=\"nofollow noopener noreferrer\">RSS feed for my blog\u003C/a> (\u003Ccode>https://tiim.ch/blog/rss.xml\u003C/code>) to test it out 😉\u003C/p>\n\u003Ch3>Standalone Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://thunderbird.net\" rel=\"nofollow noopener noreferrer\">Thunderbird\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://ravenreader.app\" rel=\"nofollow noopener noreferrer\">RavenReader\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://netnewswire.com\" rel=\"nofollow noopener noreferrer\">NetNewsWire\u003C/a> (Free, Integration with Services possible)\u003C/li>\n\u003Cli>\u003Ca href=\"https://vivaldi.com\" rel=\"nofollow noopener noreferrer\">Vivaldi Browser\u003C/a> (Free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://nodetics.com/feedbro/\" rel=\"nofollow noopener noreferrer\">feedbro browser extension\u003C/a> (Free)\u003C/li>\n\u003C/ul>\n\u003Ch3>Service-Based Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://feedreader.com\" rel=\"nofollow noopener noreferrer\">FeedReader\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://feeder.co\" rel=\"nofollow noopener noreferrer\">Feeder\u003C/a> (Freemium, 10 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.inoreader.com/pricing\" rel=\"nofollow noopener noreferrer\">Inoreader\u003C/a> (Freemium, Ads and 150 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://newsblur.com\" rel=\"nofollow noopener noreferrer\">NewsBlur\u003C/a> (Freemium, 64 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.feedspot.com\" rel=\"nofollow noopener noreferrer\">Feedspot\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://feedly.com\" rel=\"nofollow noopener noreferrer\">Feedly\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://feedbin.com\" rel=\"nofollow noopener noreferrer\">Feedbin\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://theoldreader.com\" rel=\"nofollow noopener noreferrer\">TheOldReader\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://bazqux.com\" rel=\"nofollow noopener noreferrer\">BazQux\u003C/a>\u003C/li>\n\u003C/ul>\n\u003Ch3>Self-hosted Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://www.commafeed.com/\" rel=\"nofollow noopener noreferrer\">CommaFeed\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://freshrss.org\" rel=\"nofollow noopener noreferrer\">FreshRSS\u003C/a> (Free, OSS)\u003C/li>\n\u003C/ul>","blog/2022-06-use-rss","ee633c0d-d668-48e5-af57-aaae9d243099",["Date","2022-06-05T00:00:00.000Z"],["Date","2022-06-04T22:01:15.123Z"],[8],"You should be using RSS","Decide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read.","https://i.imgur.com/t3mebu7.png",[363,15,364],"rss","software","\u003Cp>I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.\u003C/p>",[15,363,364],[368],{"id":369,"type":134,"replyTo":108,"timestamp":245,"page":354,"url":370,"content":371,"name":248},"31ea6d40-e8c2-4ada-ac16-028a3036d2de","https://www.jvt.me/mf2/2022/11/oatm9/","Liked\nYou should be using RSS\nPost detailsDecide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read. https://i.imgur.com/t3mebu7.png",{"html":373,"slug":374,"uuid":375,"title":376,"published":10,"date":377,"description":378,"cover_image":379,"content_tags":380,"abstract":384,"tags":385,"links":-1,"type":21,"folder":22,"comments":388,"latestComment":24},"\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\" rel=\"nofollow noopener noreferrer\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\" rel=\"nofollow noopener noreferrer\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\" rel=\"nofollow noopener noreferrer\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\" rel=\"nofollow noopener noreferrer\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>\n\u003Ch2>Installing the OpenSSH Server\u003C/h2>\n\u003Cp>Windows has been shipping with an OpenSSH client and server for a long time. They are not installed by default but can be activated either in the settings as described \u003Ca href=\"https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse\" rel=\"nofollow noopener noreferrer\">in the official docs\u003C/a> or with the following PowerShell commands.\u003C/p>\n\u003Cp>\u003Cstrong>You will need to start PowerShell as Administrator\u003C/strong>\u003C/p>\n\u003Cp>First, install the OpenSSH client and server.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0\nAdd-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0\n\u003C/code>\u003C/pre>\n\u003Cp>Enable the SSH service and make sure the firewall rule is configured:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\"># Enable the service\nStart-Service sshd\nSet-Service -Name sshd -StartupType 'Automatic'\n\n# Confirm the firewall rule is configured. It should be created automatically by setup. Run the following to verify\nif (!(Get-NetFirewallRule -Name \"OpenSSH-Server-In-TCP\" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {\n Write-Output \"Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it...\"\n New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22\n} else {\n Write-Output \"Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists.\"\n}\n\u003C/code>\u003C/pre>\n\u003Cp>Congratulations, you have installed the SSH server on your Windows machine. And all without manually setting up a background service or modifying config files.\u003C/p>\n\u003Ch2>Setting WSL as Default Shell\u003C/h2>\n\u003Cp>To directly boot into WSL when connecting, we need to change the default shell from \u003Ccode>cmd.exe\u003C/code> or \u003Ccode>PowerShell.exe\u003C/code> to \u003Ccode>bash.exe\u003C/code>, which in turn runs the default WSL distribution. This can be done with the PowerShell command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-ItemProperty -Path \"HKLM:\\SOFTWARE\\OpenSSH\" -Name DefaultShell -Value \"C:\\WINDOWS\\System32\\bash.exe\" -PropertyType String -Force\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: even though the shell is running on the Linux side, the SSH server is still on windows. This means you have to use to windows username to log in, and the SCP command copies files relative to the user directory on windows.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (non-Admin User)\u003C/h2>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: If the user account has Admin permissions, read the next chapter, otherwise continue reading.\u003C/p>\n\u003Cp>Create the folder \u003Ccode>.ssh\u003C/code> in the users home directory on windows: (e.g. \u003Ccode>C:\\Users\\<username>\\.ssh\u003C/code>). Run the following commands in PowerShell (not as administrator).\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path ~\\.ssh -ItmeType \"directory\"\nNew-Item -Path ~\\.ssh\\authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>The file \u003Ccode>.ssh\\autzorized_keys\u003C/code> will contain a list of all public keys that shall be allowed to connect to the SSH server.\u003C/p>\n\u003Cp>Copy the contents of your public key file (usually stored in \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>) to the \u003Ccode>authorized_keys\u003C/code> file. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (Admin User)\u003C/h2>\n\u003Cp>If the user is in the Administrators group, it is not possible to have the \u003Ccode>authorized_keys\u003C/code> file in the user directory for security purposes.\nInstead, it needs to be located on the following path \u003Ccode>%ProgramData%\\ssh\\administrators_authorized_keys\u003C/code>. A second requirement is that it is only accessible to Administrator users, to prevent a normal user from gaining admin permissions.\u003C/p>\n\u003Cp>To create the file start PowerShell as administrator and run the following command.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path $env:programdata\\ssh\\administrators_authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>This will create the file with the correct permissions. Now open the file and paste your public key into it. The public key should be located at \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Verifying everything works\u003C/h2>\n\u003Cp>Verify that you can SSH into your machine by running the following inside WSL:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">IP=$(cat /etc/resolv.conf | grep nameserver | cut -d \" \" -f2) # get the windows host ip address\nssh <user>@$IP\n\u003C/code>\u003C/pre>\n\u003Cp>Or from PowerShell and cmd:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ssh <user>@localhost\n\u003C/code>\u003C/pre>\n\u003Ch2>Drawbacks\u003C/h2>\n\u003Cp>There are some drawbacks to this approach. If you rely on some programs or scripts to work over SSH, this might not be the method for you. Most scripts expect a unix machine on the other end, or if they expect a windows machine they will most likely not be configured to deal with WSL.\u003C/p>\n\u003Cp>If you however just want to connect to your pc to copy some files or change some settings this approach is perfectly fine.\u003C/p>","blog/2022-03-ssh-windows-wsl","03b5a86c-5f4d-4086-9f5f-e1e46b4bcf58","How to set up an SSH Server on Windows with WSL",["Date","2022-03-02T00:00:00.000Z"],"It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.","/assets/2022-03-ssh-windows-wsl.png",[381,382,383,15],"SSH","WSL","Windows","\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>",[15,386,387,92],"ssh","windows",[389,395,401],{"id":390,"type":270,"replyTo":391,"timestamp":392,"page":374,"url":393,"content":394,"name":108},"20f2c526-7466-4c21-83ac-51750f278328","f7c1891b-e97b-4030-863e-19344ed84d32","2022-09-20T14:59:17Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#20f2c526-7466-4c21-83ac-51750f278328","Hi Tim, no problem. I had this error in \"Event Viewer > Applications and Services Logs > OpenSSH > Admin\" and figure it out that sshd seems to search the Administrators groups to operate, literal name and not properly localized by region.\n\nerroid:2 user:SYSTEM details:\"sshd: error: unable to resolve group administrators\"\n\nMaybe is not all the non-english windows with this problem, but I have it but after created the group works like a charm.",{"id":391,"type":270,"replyTo":396,"timestamp":397,"page":374,"url":398,"content":399,"name":400},"5de01bc5-b7c7-4522-9dfe-c67f103d4c03","2022-09-20T12:58:29Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#f7c1891b-e97b-4030-863e-19344ed84d32","Hi FinderX\n\nThanks for the heads up. I don't remember having to create a new user group, even though my system language is German. Maybe I just forgot about that though.","Tim",{"id":396,"type":270,"replyTo":108,"timestamp":402,"page":374,"url":403,"content":404,"name":405},"2022-09-20T07:50:46Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#5de01bc5-b7c7-4522-9dfe-c67f103d4c03","Hi!\nI add some roundabouts about admin-users, if your windows ssh server system language is NOT english, you must create 'Administrators' group (without quotes) in your language equivalent of \"Users and Local Groups > Groups\", if your server is a DC (Domain Controller) create it in your language equivalent of \"Active Directories Users and Computers\".\n\nCreate the user group with name Administrators, description whatever, ex. \"Dummy group for sshd to work correctly.\", and in Members add your language equivalent of the user Administrator.\n\nThis is optional but I suggest you change these settings in \"%programdata%\\ssh\\sshd_config\" after you successfully copy your public key to the ssh server :\n\nStrictModes yes\nPubkeyAuthentication yes\nPasswordAuthentication no\n\nYou can see the log activity in your language equivalent of \"Applications and Services Logs > OpenSSH > Admin or Operational\"\n\nBest Regards.","FinderX",{"html":407,"slug":408,"uuid":409,"title":410,"published":10,"date":411,"description":412,"cover_image":413,"content_tags":414,"abstract":418,"tags":419,"links":-1,"type":21,"folder":22,"comments":420,"latestComment":24},"\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>\n\u003Cp>\u003Cem>TLDR\u003C/em>:\u003C/p>\n\u003Cul>\n\u003Cli>Either use Bluetooth Audio Receiver from the Microsoft Store to connect you phone via Bluetooth,\u003C/li>\n\u003Cli>Or use an audio cable to connect the phone to the \"line-in\" on your PC.\u003C/li>\n\u003C/ul>\n\u003Ch2>Bluetooth (recommended)\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>: A PC with integrated Bluetooth or a Bluetooth dongle.\u003C/p>\n\u003Cp>I recommend this approach more than the wired one because it is way less effort, you don't have to deal with a USB or lightning to audio dongle and in my opinion it is more reliable.\u003C/p>\n\u003Cp>Pair your phone with your PC as normal, by opening the Bluetooth settings on your phone and on the PC and wait for the devices to show up. When you successfully paired the phone once you will not have to do this again. Now you need an app that will tell the phone that it can use the PC as a wireless speaker. The only app I found that will do this is the \u003Ca href=\"https://www.microsoft.com/de-de/p/bluetooth-audio-receiver/9n9wclwdqs5j\" rel=\"nofollow noopener noreferrer\">Bluetooth Audio Receiver\u003C/a> app from the Windows Store. Install and and open it. You should see your phone on the list of Bluetooth devices on the app. Select it and click on the \u003Ccode>Open Connection\u003C/code> button. It might take a moment but after it connected, you should hear all sounds from your phone on your PC.\u003C/p>\n\u003Ch2>Wired\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>:\u003C/p>\n\u003Cul>\n\u003Cli>Male-to-Male audio cable (3.5mm audio jack).\u003C/li>\n\u003Cli>A line-in port on your PC (usually blue audio jack on the back)\u003C/li>\n\u003Cli>USB-C to audio jack adapter (Optional)\u003C/li>\n\u003Cli>Lighting to audio jack adapter (Optional)\u003C/li>\n\u003C/ul>\n\u003Cp>This approach works if your PC doesn't support Bluetooth, or if the Bluetooth connection drops for some reason. Connect the audio cable to the blue line-in jack on the back of the computer. Then, connect the phone to the other end of the audio cable. If your phone does not have an audio jack, use the adapter on the USB-C or Lightning port. If your PC detects that you connected a new line-in device, it might open the audio settings automatically. If not, right-click on the volume icon on the taskbar next to the clock and select \u003Ccode>Sounds\u003C/code>. Navigate to the \u003Ccode>Input\u003C/code> tab and double click on the Line-In entry (the one with a cable icon). Navigate to the Monitor tab and select the check box for \"Use this device as a playback source\". This will tell windows it should play all sounds received through this input directly to the speakers. Usually this is used to monitor microphones but it works for this use case too. You should now hear any sound from your phone through your PC headphones or speakers. Make sure you turn this checkbox off when you disconnect your phone. Otherwise you might hear a crackle or other sounds when the loose cable gets touched.\u003C/p>\n\u003Cp>\u003Cem>Photo by Lisa Fotios from Pexels\u003C/em>\u003C/p>","blog/2022-02-phone-audio-to-pc","be57f2df-d58f-4b79-8a51-e20d482f46cf","How to Listen to Phone Audio on PC",["Date","2022-02-12T00:00:00.000Z"],"Learn how to connect your phone audio to your PC over wire or Bluetooth.","/assets/2022-02-phone-audio-to-pc.jpg",[415,416,387,417,364],"how-to","audio","bluetooth","\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>",[416,417,415,364,387],[],{"html":422,"slug":423,"uuid":424,"title":425,"published":10,"description":426,"content_tags":427,"date":431,"modified":432,"cover_image":433,"abstract":434,"tags":435,"links":-1,"type":21,"folder":22,"comments":438,"latestComment":24},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>\n\u003Cp>\u003Ca href=\"https://tiim.ch/assets/2021-01-20-Thesis.pdf\" rel=\"nofollow noopener noreferrer\">Download Thesis\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode>@thesis{bachmann2021,\n\ttitle = {Modelling Git Operations as Planning Problems},\n\tauthor = {Tim Bachmann},\n\tyear = {2021},\n month = {01},\n\ttype = {Bachelor's Thesis},\n\tschool = {University of Basel},\n\tdoi = {10.13140/RG.2.2.24784.17922}\n}\n\u003C/code>\u003C/pre>","blog/2021-01-git-operations-as-planning-problems","dc6e6d10-c460-4d3c-8fe2-4ce7535b4af1","Modelling Git Operations as Planning Problems","Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.",[428,429,430,15],"Git","PDDL","Planning-System",["Date","2021-01-20T00:00:00.000Z"],["Date","2023-09-18T11:41:51.000Z"],"/assets/2021-01-git-operations-as-planning-problems.png","\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>",[15,436,437,55],"git","pddl",[],{"html":440,"slug":441,"uuid":442,"title":443,"published":10,"description":444,"content_tags":445,"date":448,"cover_image":449,"abstract":450,"tags":451,"links":-1,"type":21,"folder":22,"comments":453,"latestComment":24},"\u003Ch2>The problem\u003C/h2>\n\u003Cp>Let's say you have a rest API with the following endpoint that returns all of the books in your database:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-rest\">GET /book/\n\u003C/code>\u003C/pre>\n\u003Cp>Your SQL query might look like something like this\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT *\nFROM books\n\u003C/code>\u003C/pre>\n\u003Cp>Sometimes you want to only list books, for example, from a specific author. How do we do this in SQL?\u003C/p>\n\u003Ch2>Naive solution: String concatenation ✂\u003C/h2>\n\u003Cp>One way would be to concatenate your sql query something like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const arguments = [];\nconst queryString = \"SELECT * FROM books WHERE true\";\nif (authorFilter != null) {\n queryString += \"AND author = ?\";\n arguments.push(authorFilter);\n}\ndb.query(queryString, arguments);\n\u003C/code>\u003C/pre>\n\u003Cp>I'm not much of a fan of manually concatenating strings.\u003C/p>\n\u003Ch2>The coalesce function 🌟\u003C/h2>\n\u003Cp>Most Databases have the function \u003Ccode>coalesce\u003C/code> which accepts a variable amount of arguments and returns the first argument that is not null.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- Examle\nSELECT coalesce(null, null, 'tiim.ch', null, '@TiimB') as example;\n\n-- Will return\n\nexample\n---------\ntiim.ch\n\u003C/code>\u003C/pre>\n\u003Cp>But how will this function help us?\u003C/p>\n\u003Ch2>Optional filters with the coalesce function\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT *\nFROM books\nWHERE\n author = coalesce(?, author);\n\u003C/code>\u003C/pre>\n\u003Cp>If the filter value is null the coalesce expression will resolve to \u003Ccode>author\u003C/code>\nand the comparison \u003Ccode>author = author\u003C/code> will be true.\u003C/p>\n\u003Cp>If on the other hand the value is set for example to Shakespeare then the author will be compared to Shakespeare.\u003C/p>\n\u003Cp>I came across this way to implement optional filters only recently. If you have a more idiomatic way to do this let me know please ✨\u003C/p>\n\u003Cp>If you liked this post please follow me on here or on Twitter under \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> 😎\u003C/p>","blog/2019-07-sql-optional-filters-coalesce","899fb73c-a78e-4cd9-b712-1886715b2d56","How to write optional filters in SQL","A simple way to filter by optional values in SQL with the COALESCE function.",[446,447,15],"SQL","quick-tip",["Date","2019-07-11T00:00:00.000Z"],"/assets/2019-07-sql-optional-filters-coalesce.png","\u003Cp>Let's say you have a rest API with the following endpoint that returns all of the books in your database:\u003C/p>",[15,447,452],"sql",[],{"html":455,"slug":456,"uuid":457,"title":458,"published":10,"description":459,"content_tags":460,"date":464,"cover_image":465,"abstract":466,"tags":467,"links":-1,"type":21,"folder":22,"comments":471,"latestComment":24},"\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\" rel=\"nofollow noopener noreferrer\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>\n\u003Cp>If you want to see an example of this method in action, go check out my \u003Ca href=\"https://tiimb.work\" rel=\"nofollow noopener noreferrer\">personal website\u003C/a> on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>\u003C/p>\n\u003Cp>I won't be explaining how to setup a Vue project. If you're looking for a Tutorial on that go check out the awesome \u003Ca href=\"https://vuejs.org/v2/guide/\" rel=\"nofollow noopener noreferrer\">Vue.js Guide\u003C/a>.\u003C/p>\n\u003Cp>So you have setup your awesome Vue project and want to host it on GitHub Pages. The way Muhammad explained it you would build the project using \u003Ccode>npm run build\u003C/code>, commit the \u003Ccode>dist/\u003C/code> folder along with your source files and point GitHub to the dist folder. This might get quite messy because you either have commit messages with the sole purpose of uploading the dist folder or you commit the code changes at the same time which makes it hard to find the relevant changes if you ever want to look at your commits again.\u003C/p>\n\u003Cp>So what can you do about this?\u003C/p>\n\u003Cp>Git to the rescue, let's use a branch that contains all the build files.\u003C/p>\n\u003Ch2>Step 1 - keeping our working branch clean 🛀\u003C/h2>\n\u003Cp>To make sure that the branch we are working from stays clean of any build files we are gonna add a \u003Ccode>.gitignore\u003C/code> file to the root.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># .gitignore\ndist/\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 2 - adding a second branch 🌳\u003C/h2>\n\u003Cp>We are not goint to branch off master like how we would do it if we were to modify our code with the intention to merge it back to the main branch. Instead we are gonna create a squeaky clean new branch that will only ever hold the dist files. After all we will not ever need to merge these two branches together.\u003C/p>\n\u003Cp>We do this by creating a new git repository inside the dist folder:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">cd dist/\ngit init\ngit add .\ngit commit -m 'Deploying my awesome vue app'\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 3 - deploying 🚚\u003C/h2>\n\u003Cp>We are gonna force push our new git repository to a branch on GitHub. This might go against git best practices but since we won't ever checkout this branch we don't have to worry about that.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">git push -f git@github.com:<username>/<repo>.git <branch>\n\u003C/code>\u003C/pre>\n\u003Cp>⚠️ Make sure you double or tripple check your destination branch! You don't want to accidentally overwrite your working branch. Using the branch \u003Ccode>gh-pages\u003C/code> will most likely be a good idea.\u003C/p>\n\u003Ch2>Step 4 - pointing GitHub to the right place 👈\u003C/h2>\n\u003Cp>Now we are almost done. The only thing left is telling GitHub where our assets live.\u003C/p>\n\u003Cp>Go to your repo, on the top right navigate to \u003Ccode>Settings\u003C/code> and scroll down to GitHub pages. Enable it and set your source branch to the branch you force pushed to, for example \u003Ccode>gh-pages\u003C/code>.\u003C/p>\n\u003Ch2>Step 5 - automating everything 😴\u003C/h2>\n\u003Cp>If you don't mind doing this whole process (Step 2 and 3) every time you want to deploy you can stop now. If you're as lazy as me, here is the script I use to deploy with one command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># deploy.sh\n\n#!/usr/bin/env sh\n\n# abort on errors\nset -e\n\n# build\necho Linting..\nnpm run lint\necho Building. this may take a minute...\nnpm run build\n\n# navigate into the build output directory\ncd dist\n\n# if you are deploying to a custom domain\n# echo 'example.com' > CNAME\n\necho Deploying..\ngit init\ngit add -A\ngit commit -m 'deploy'\n\n# deploy\ngit push -f git@github.com:<username>/<repo>.git <branch>\n\ncd -\n\n\u003C/code>\u003C/pre>\n\u003Cp>If your on windows look into the Windows Subsystem for Linus (WSL) it will be worth it.\u003C/p>\n\u003Cp>If you are still reading, thank you very much. This is actually my first article and I'm really happy to hear about any opinions and criticisms.\nHappy Coding ♥\u003C/p>","blog/2019-05-vue-on-github-pages","96054292-eb45-4d8a-9aca-bb050175ff2a","How I use Vue.js on GitHub Pages","How to properly deploy a Vue.js app on GitHub Pages",[461,462,463,15],"GitHub Pages","Vue.js","Javascript",["Date","2019-05-04T00:00:00.000Z"],"/assets/2019-05-vue-on-github-pages.png","\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>",[15,468,469,470],"github-pages","javascript","vue.js",[]],"uses":{}}]} diff --git a/blog/rss.xml b/blog/rss.xml new file mode 100644 index 00000000..d3d42836 --- /dev/null +++ b/blog/rss.xml @@ -0,0 +1,1599 @@ + + + + Tim Bachmann's Blog + https://tiim.ch/ + Blog about web development, programming, and anything that might interest me. + Wed, 20 Sep 2023 21:42:33 GMT + https://validator.w3.org/feed/docs/rss2.html + https://github.com/jpmonette/feed + en + + Tim Bachmann's Blog + https://tiim.ch/swim-emoji.png + https://tiim.ch/ + + Tim Bachmann + + + <![CDATA[Getting the Absolute Path of a Remote Directory in Ansible]]> + https://tiim.ch/blog/2023-09-20-ansible-absolute-path + https://tiim.ch/blog/2023-09-20-ansible-absolute-path + Wed, 20 Sep 2023 21:39:13 GMT + + I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like ~/docker/myservice. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.

+

Deleting with elevated permission on the command line is easy: The command sudo rm -rf ~/docker/myservice performs the rm operation as the root user. In bash, this will delete the docker/myservice folder in the user's home directory, but when doing the equivalent in Ansible, this won't work!

+
# This does not work!
+- name: Delete the folder using root permissions
+  become: true
+  ansible.builtin.file:
+    path: "~/docker/myservice"
+    state: "absent"
+
+

This code will try to delete the file /user/root/docker/myservice, which is not what we wanted.

+

The bash version works because the shell first resolves the tilde in the argument to the current users' directory before calling the sudo command. In Ansible, we first switch to the root user and only then the tilde is resolved: this time to the home directory of the root user.

+

To circumvent this, we can manually resolve the path to an absolute path. Unfortunately, I have not found a straightforward way to do this in Ansible, however the bash command readlink -f <path> does exactly this. To use it in Ansible, we can use the following configuration:

+
- name: Get absolute folder path
+  ansible.builtin.command:
+    cmd: "readlink -f ~/docker/myservice"
+  register: folder_abs
+  changed_when: False
+
+- name: Debug
+  debug:
+    msg: "{{folder_abs.stdout}}" # prints /user/tim/docker/myservice
+
+- name: Delete the folder using root permissions
+  become: true
+  ansible.builtin.file:
+    path: "{{folder_abs.stdout}}"
+    state: "absent"
+
+

With this Ansible script, we manually resolve the absolute path and use it to delete the folder using root permissions. If you know of an easier way to resolve to an absolute path, please let me know!

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + ansible + bash + dev + linux + +
+ + <![CDATA[Forums]]> + https://tiim.ch/blog/2023-06-16-forums + https://tiim.ch/blog/2023-06-16-forums + Fri, 16 Jun 2023 18:56:56 GMT + + My first real programming experience was with a scripting language called AutoHotkey. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized. +When AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while. +Unfortunately, the German forum has since been discontinued, but some of the pages are still up on the Way back machine.

+

Another community I used to be really active in, was for a small indie roleplaying game called Illarion. Again, the community relied heavily on a forum for communications. This time it was used for players to engage in "out of character" communication, as well as a way to simulate a metaphorical bullet board in the game town square where characters could leave notes for each other. +Since the game was closely inspired by TTRPGs like D&D, the role-playing part was more important than the in-game mechanics. The forum allowed characters to interact with each other that were not online at the same time. Again, I got really invested in this community, even going so far as joining other guild-specific forums.

+

I eventually moved on from both of those amazing communities, because my interests changed. I left the AutoHotkey community because I started to get more involved with other programming languages, and I left the Illarion community because I (with the support of my parents) was looking for a less time-intensive game. Unfortunately, I never happened to find another online community like those two ever again...

+

Sometime later I joined Reddit and was amazed. It felt like a place where all communities come together on a single site. No need to check on multiple websites for new posts, everything neatly together in a single website, accessible on a single (third party) app. I remember wondering why people were still using forums when Reddit was so much simpler.

+

Jumping to the present and I realize that I was wrong. Even though I am subscribed to a bunch of communities on Reddit, I barely comment on any posts and posted even less. While I am a community member on record, I do not feel like one. The wealth of communities, as well as the incentive to go on the front page to see the most popular posts of the whole site, made me want to open Reddit, but it did not give me the feeling of belonging. I rather felt like a spectator that from time to time gathers the courage to shout his own ideas into the ether.

+
+

Side note: Discord comes much closer to the feeling of community. However, the nature of chat makes the interactions fleeting, being in a chat room with a few hundred other people, where every message is just a few sentences at most does not lead to the same connections. No one expects their message to be read again after a few days.

+
+

Now the company behind Reddit started to lose the goodwill of the users. While I don't think Reddit will die anytime soon, I think there are a lot of people looking for alternatives. And the best alternative to the website that killed forums is... forums.

+

While forums largely still work the same as they did 15 years ago, there have been developments that might make them more feasible for our desire to have everything accessible on a single site or on a single app. Last time a social media company, Twitter, annoyed its user base, the fediverse, and more specifically Mastodon, started to go more mainstream. This time I hope there will be other projects that profit. I have heard people mentioning the projects Kbin and Lemmy, both forum-like platforms that implement the ActivityPub specification. Same as Mastodon, this means users are able to interact with users on other instances. Even further, this should also allow users of any federated social network, such as Mastodon, to post and comment on any federated forum. Even established forum software such as Flarum and nodeBB are considering adding federation support.

+

I really hope that forums make a comeback, not only because of the nostalgia but also because to me it feels like a more sustainable way to build a community. And now with the possibility to federate via the fediverse, a forum doesn't have to be a walled garden of members any more. In the end, most importantly I hope people are still finding communities they can be as passionate about as I was, without any corporate overlords trying to keep their eyeballs on ads as long as possible.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + activitypub + fediverse + forum + reddit + +
+ + <![CDATA[Automated Planning using Property-Directed Reachability with Seed Heuristics]]> + https://tiim.ch/blog/2023-05-06-pdr-with-seed-heuristics + https://tiim.ch/blog/2023-05-06-pdr-with-seed-heuristics + Mon, 18 Sep 2023 13:32:00 GMT + + Abstract +

Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.

+

Download PDF

+

Cite

+
@phdthesis{bachmann2023,
+    author = {Bachmann, Tim},
+    year = {2023},
+    month = {05},
+    title = {Automated Planning using Property-Directed Reachability with Seed Heuristics},
+    doi = {10.13140/RG.2.2.11456.30727},
+    type = {Master's Thesis},
+    school = {University of Basel}
+}
+
+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + heuristic + pdr + planning-system + +
+ + <![CDATA[Weechat Notifications with ntfy.sh]]> + https://tiim.ch/blog/2023-03-28-weechat-notification-ntfy + https://tiim.ch/blog/2023-03-28-weechat-notification-ntfy + Tue, 28 Mar 2023 10:05:19 GMT + + In one of my last blog posts I set up WeeChat in docker, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the trigger plugin. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.

+

Also, I recently found the web service ntfy.sh. It sends push notifications whenever you send an HTTP post request to a certain URL. I already have ntfy.sh installed on my android phone, and I also found a minimal and lightweight desktop client.

+

I managed to set a WeeChat trigger up that fires every time I get mentioned (highlighted in WeeChat terminology), and a trigger that fires every time I get a private message. Both of those triggers execute the /exec command which runs an arbitrary shell command. The exec command runs the wget program to send a post request to the ntfy.sh server, which in turn sends a notification to all apps that subscribe to the same URL as the post request was sent. I would usually use the curl program for this instead of wget, but the docker default docker image doesn't contain a curl install.

+

Here you can see the two /trigger commands:

+

trigger on mention

+
/trigger addreplace notify_highlight print '' '${tg_highlight}' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data "${tg_message}" "-                   -header=Title: New highlight: ${buffer.full_name}" https://ntfy.sh/my_ntfy_topic_1234'
+
+

trigger on private message

+
/trigger addreplace notify_privmsg print '' '${tg_tag_notify} == private && ${buffer.notify} > 0' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data "${tg_message}" "--header=Title: New private message: ${buffer.full_name}" https://ntfy.sh/my_ntfy_topic_1234'
+
+

The trigger commands in detail

+

In case you don't just want to copy and paste some random command from the internet into your WeeChat (which you shouldn't do anyway), I will try to explain the trigger command that fires when you get mentioned in a message:

+

Let's first look at the trigger command itself: +/trigger addreplace <name> <hook> <argument> <condition> <variable-replace> <command> +We call the /trigger command with the addreplace subcommand. This subcommand will either register a new trigger or replace it if one with the same name already exists.

+
    +
  • name - This argument is self-explanatory, the name of the trigger. In our case I called it notify_highlight, but you could call it whatever you want.
  • +
  • hook - This argument specifies which hook or event the trigger should listen for. WeeChat is built as an event-driven platform, so pretty much anything from mouse movements to IRC messages are handled via events. In this case, we want to trigger on the print event, which is fired every time a new message gets received from IRC.
  • +
  • argument - The argument is needed for some hooks, but not for the print hook, so we are going to ignore that one for now and just set it to an empty string ''.
  • +
  • condition - The condition must evaluate to true for the trigger to fire. This is helpful because the print trigger fires for every new message, but we only want to be notified when the new message mentions our nick. The condition for this is ${tg_highlight}. You can find the list of variables that you can access with the command /trigger monitor, which prints all variables for every trigger that gets executed.
  • +
  • variable-replace - This took me a while to understand. This command is used to manipulate data and save it to a variable. The syntax is inspired by the sed command. Explaining it fully is out of the scope of this blog post, but you can take a look at the docs. In our example, we replace the whole content of the variable tg_message with the format string ${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor} which results in a sting like <tiim> Hello world!.
  • +
  • command - The last argument is the command that gets executed whenever this trigger fires. In our case, we use the /execute command, which starts the wget command which in turn sends a post request to ntfy.sh. Make sure you set the ntfy topic (the part after https://ntfy.sh/) to something private and long enough so that nobody else is going to guess it by accident.
  • +
+

Don't forget to subscribe to the ntfy topic on your phone or whatever device you want to receive the notification on.

+

The possibilities with the trigger plugin are endless, I hope this inspires you to build your own customizations using weechat.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + irc + ntfy.sh + weechat + wget + +
+ + <![CDATA[Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN]]> + https://tiim.ch/blog/2023-03-21-anyconnect-wsl2 + https://tiim.ch/blog/2023-03-21-anyconnect-wsl2 + Wed, 15 Mar 2023 15:22:04 GMT + + I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.

+

I will show you how I fixed this problem for me and explain what the commands I used do. This post is mostly for my reference, but I hope it helps anyone else as well.

+

Finding out what your problem is

+

Let's check first if we have internet access inside WSL2. For this run the ping command with an IP address as a destination:

+
ping 8.8.8.8
+
+

If you get something like this as the output, your internet connection is fine, and it's just the DNS nameserver addresses that are misconfigured, you can jump forward to Solution 2.

+
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
+64 bytes from 8.8.8.8: icmp_seq=1 ttl=108 time=4.53 ms
+64 bytes from 8.8.8.8: icmp_seq=2 ttl=108 time=3.94 ms
+64 bytes from 8.8.8.8: icmp_seq=3 ttl=108 time=3.97 ms
+64 bytes from 8.8.8.8: icmp_seq=4 ttl=108 time=3.78 ms
+64 bytes from 8.8.8.8: icmp_seq=5 ttl=108 time=3.77 ms
+64 bytes from 8.8.8.8: icmp_seq=6 ttl=108 time=3.76 ms
+64 bytes from 8.8.8.8: icmp_seq=7 ttl=108 time=3.81 ms
+
+

If you don't get any responses from the ping (i.e. no more output after the PING 8.8.8.8 (8.8.8.8) ... line), you need to configure the WSL and the VPN network adapter metric. Go to Solution 1.

+

To check if the DNS is working, we can again use the ping command, this time with a domain name:

+
ping google.com
+
+

If you get responses, the DNS and your internet connection are working! If not go to Section 2.

+

Solution 1: Fixing the Network Adapter

+

Run the following two commands in PowerShell as administrator:

+
Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match "Cisco AnyConnect"} | Set-NetIPInterface -InterfaceMetric 4000
+
+Get-NetIPInterface -InterfaceAlias "vEthernet (WSL)" | Set-NetIPInterface -InterfaceMetric 1
+
+

Let me explain what those two commands do. Both follow the same pattern of listing all network adapters, selecting a specific adapter from the list and setting its "metric".

+

You can imagine an adapter as a virtual network port on the back of your pc or laptop. But instead of sending packets through the wire, the driver for a specific port can do whatever it wants with those packets, in the case of a VPN, the packets get encrypted and forwarded to the internet via another adapter.

+

The InterfaceMetric is a value associated with each adapter that determines the order of those adapters. This allows windows to determine which adapter to prefer over another one.

+

By setting the interface metric of the Cisco adapter to 4000 and the metric of the WSL adapter to one, we allow the traffic from WSL to flow through the Cisco adapter. To be honest I do not exactly understand why this works but it does.

+

Solution 2: Registering the VPN DNS inside of WSL

+

Setting the DNS servers is, unfortunately, a little bit more involved than just running two commands, we need to edit the files /etc/wsl.conf and /etc/resolv.conf, and restart wsl in between. Let's get to it:

+

Edit the file /etc/wsl.conf inside of WSL2 using a text editor. I suggest doing this through the terminal since you need root permissions to do that:

+
sudo nano /etc/wsl.conf
+# feel free to use another editor such as vim or emacs
+
+

Most likely this file does not exist yet, otherwise, I suggest you create a backup of the original file to preserve the settings.

+

Add the following config settings into the file:

+
[network]
+generateResolvConf = false
+
+

This will instruct WSL to not override the /etc/resolv.conf file on every start-up. Save the file and restart WSL with the following command so that the changed config takes effect:

+
wsl.exe --shutdown
+
+

Now open a PowerShell terminal and list all network adapters with the following command:

+
ipconfig /all
+
+

Find the Cisco AnyConnect adapter and copy the IP addresses in the DNS-Server field. We will need those IPs in the next step.

+

Start WSL again and edit the /etc/resolv.conf file:

+
sudo nano /etc/resolv.conf
+
+

Most likely there is already something in this file, you can discard it. When undoing the changes, WSL will automatically regenerate this file anyway, so you don't need to back it up.

+

Delete all the contents and enter the IP addresses you noted down in the last step in the following format:

+
nameserver xxx.xxx.xxx.xxx
+
+

Put each address on a new line, preceded by the string nameserver. +Save the file and restart WSL with the same command as above:

+
wsl.exe --shutdown
+
+

Now open up WSL for the last time and set the immutable flag for the /etc/resolv.conf file:

+
chattr +i /etc/resolv.conf
+
+

And for the last time shut down WSL. Your DNS should now be working fine!

+

Undoing those changes

+

I did not have a need to undo the steps for Solution 1, and I'm pretty sure the metric resets after each system reboot anyway so there is not much to do.

+

To get DNS working again when not connected to the VPN run the following commands:

+
sudo chattr -i /etc/resolv.conf
+sudo rm /etc/resolv.conf
+sudo rm /etc/wsl.conf
+wsl.exe --shutdown
+
+

This will first clear the immutable flag off /etc/resolv.conf, and delete it. Next, it will delete /etc/wsl.conf if you have a backup of a previous wsl.conf file, you can replace it with that. At last, we shutdown WSL again for the changes to take effect.

+

Unfortunately, this is quite a procedure to get a VPN to work with WSL2, but I'm hopeful that this will soon not be necessairy anymore.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dns + networking + vpn + wsl + +
+ + <![CDATA["no such file or directory" after enabling CGO in Docker]]> + https://tiim.ch/blog/2023-01-24-no-such-file-or-directory-cgo + https://tiim.ch/blog/2023-01-24-no-such-file-or-directory-cgo + Tue, 24 Jan 2023 00:00:00 GMT + + Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message exec /app/indiego: no such file or directory. I had removed the CGO_ENABLE=0 variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the scratch image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.

+

To include libc into the container, I simply changed the base image from scratch to alpine, which includes libc. This makes the image slightly larger but this seemed way easier than trying to include libc directly.

+

As a bonus I got to delete the /usr/share/zoneinfo and ca-certificates.crt files, and rely on those provided by alpine.

+

You can see the commit to IndieGo here.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + cgo + docker + go +
+ + <![CDATA[Running the WeeChat IRC Client on a VPS in Docker]]> + https://tiim.ch/blog/2023-01-15-weechat-docker + https://tiim.ch/blog/2023-01-15-weechat-docker + Wed, 18 Jan 2023 11:34:27 GMT + + I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used HexChat in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.

+

The one client I have seen mentioned a lot is WeeChat (not to be confused with WeChat, the Chinese instant messenger). WeeChat runs in the terminal as a TUI and after a while of getting used to (and after enabling 'mouse mode') it seems intuitive enough.

+

The nice thing about WeeChat running not as a graphical application, is that it makes it possible to run on a server and access it from anywhere over ssh.

+
+INFO

Except on mobile devices, but weechat has mobile apps that can connect to it directly.

+
+

Since I pretty much host all my selfhosted software in docker on a VPS, I was looking if someone already published a docker image for WeeChat. There is a bunch of them, but only weechat/weechat (the official image) is still updated regularly. The docker hub page does not have any documentation, but I managed to find it in the weechat/weechat-container github repo.

+

As it says in the readme on github, you can start the container with

+
docker run -it weechat/weechat
+
+

which will run weechat directly in the foreground.

+
+Info

Don't skip the -it command line flags. The -i or --interactive keeps stdin open, which is required to send input to weechat. Weechat also closes immediately if the stdin gets closed, which took me a while to figure out. +The -t or --tty flag is required to provide a fake tty to the container. I don't really understand what that means but without this you won't see the user interface of weechat.

+
+

Running in the foreground is not really that helpful if we want to run weechat on a server, so we need to detach (let it run in the background) from the container with the -d or --detach flag. It also helps to specify a name for the container with the --name <name> argument, so we can quickly find the container again later. The docker command now looks like this:

+
docker run -it -d --name weechat weechat/weechat
+
+

When we run this command, we will notice that weechat is running in the background. To access it we can run docker attach weechat. To detach from weechat without exiting the container, we can press CTRL-p CTRL-q as described in the docker attach reference

+

I noticed that there are two versions of the weechat image: a debian version and an alpine linux version. Generally the Alpine Linux versions of containers are smaller than the Debian versions, so I decided to use the alpine version: weechat/weechat:latest-alpine.

+

With this we are practically done, but if we ever remove and restart the container, all of the chat logs and customisations to weechat will be gone. To prevent this we need to add the config and log files to a volume.

+

I generally use the folder ~/docker/(service) to point my docker volumes to, so I have a convenient place to inspect, modify and back up the data.

+

Let's create the folder and add the volume to the docker container. I also added the --restart unless-stopped flag to make sure the container gets restarted if it either exits for some reason of if docker restarts.

+
mkdir -p ~/docker/weechat/data
+mkdir -p ~/docker/weechat/config
+
+docker run -it -d --restart unless-stopped \
+    -v "~/docker/weechat/data:/home/user/.weechat" \
+    -v "~/docker/weechat/config:/home/user/.config/weechat" \
+    --name weechat weechat/weechat:latest-alpine`
+
+

Running this command on the server is all we need to have weechat running in docker.

+
+

But how do I quickly connect to weechat? Do I always have to first ssh into the server and then run docker attach?

+
+

Yes but, as almost always, we can simplify this with a bash script:

+
#!/usr/bin/env bash
+
+HOST=<ssh host>
+ssh -t "${HOST}" docker attach weechat
+
+

This bash script starts ssh with the -t flag which tells ssh that the command is interactive. +Copy this script into your ~/.local/bin folder and make it executable.

+
nano ~/.local/bin/weechat.sh
+chmod +x weechat.sh
+
+

And that's it! Running weechat.sh will open an ssh session to your server and attach to the weechat container. Happy Chatting!

+

If you liked this post, consider subscribing to my blog via RSS, or on social media. If you have any questions, feel free to contact me. I also usually hang out in ##tiim on irc.libera.chat. My name on IRC is tiim.

+
+Update 2022-01-18

I have found that at the beginning of a session, the input to weechat doesn't seem to work. Sometimes weechat refuses to let me type anything and/or doesn't recognize mouse events. +After a while of spamming keys and Alt-m (toggle mouse mode), it seems to fix itself most of the time. +I have no idea if thats a problem with weechat, with docker or with ssh, and so far have not found a solution for this. If you have the same problem or even know how to fix it, feel free to reach out.

+
+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + docker + irc + weechat + +
+ + <![CDATA[Hosting Images with Storj and Cloudflare]]> + https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting + https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting + Sat, 03 Dec 2022 13:37:33 GMT + + For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs TOS:

+
+

[...] Also, don't use Imgur to host image libraries you link to from elsewhere, content for your website, advertising, avatars, or anything else that turns us into your content delivery network.

+
+

Finally when I started indie-webifying my website, and was implementing the micropub protocol (which I will blog about at a later time), I decided that it was at the time to host the images on a platform that was meant to do that. I looked at a few storage providers such as cloudinary and S3 based object storage and landed on Storj.io, mostly because of the generous free tier, which should suffice for this little blog for quite a while.

+

One thing that bothered me slightly was that all storage providers I looked at charge for traffic. It's not the fact that it's an additional expense (if your not in the free tier anymore) that bothers me, but the fact that I don't have any control over how much this will cost me. In all likelihood this will never cost me anything since this blog has not much traffic, but if a post were to go viral (one can dream...), this could result in a surprise bill at the end of the month.

+

To help with the traffic costs I decided to try to use the free CDN functionality of Cloudflare to reduce the traffic to Storj. In this blog post I will describe how I did that.

+

Is this the right solution for you?

+

If you are in a similar situation as me, and just want to have somewhere to host your images for a personal website or to share images or screenshots as links while still having control over all your data, this could be a good solution.

+

If you want to build a robust image pipeline with resizing and image optimization, or you are building an enterprise website this is probably not the right way. You should take a look at cloudinary or one of the big cloud providers.

+

Prerequisites

+

To use Cloudflare as a CDN, you need to have Cloudflare setup as your DNS host for the domain you want to serve the images from. Even if you just want to use a subdomain like media.example.com, the whole example.com domain needs to be on cloudflare. For me this was not much of an issue, I followed the instructions from cloudflare and pointed the nameserver of my domain to cloudflare. Although I did have an issue during the migration, which resulted in my website being down for two hours. But I'm pretty sure this was caused by my previous nameserver provider.

+

Setting up Storj & Cloudflare

+

I assume you already have an account at storj.io. The next step is creating a bucket for your images. A bucket is just a place for your files and folders to live in storj, just like in any other S3 compatible storage provider. (Actually there are no folders in storj and other S3 services, the folders are just prefixes of the filenames). When creating a bucket, make sure you save the passphrase securely, such as in your password manager. Whenever storj asks you for the passphrase, make sure you don't let storj generate a new one! Every new passphrase will create access to a new bucket.

+

The next step is installing the uplink cli. Follow the quick start tutorial to get an access grant. Remember to use the same passphrase from above. Now follow the next quickstart tutorial to add the bucket to the uplink cli. The file accessgrant.txt in the tutorial only contains the access-grant string that you got from the last step.

+

Finally we want to share the bucket so the images can be accessed from the web. For this you can run the following command:

+
uplink share --dns <domain> sj://<bucket>/<prefix> --not-after=none
+
+

Replace <domain> with the domain you want to serve the images from. In my case I use media.tiim.ch. Then replace <bucket> with the name of your bucket and <prefix> with the prefix.

+

As mentioned above, you can think of a prefix as a folder. If you use for example media-site1 as a prefix, then every file in the "folder" media-site1 will be shared. This means you can use multiple prefixes to serve files for multiple websites in the same bucket.

+

You will get the following output:

+
[...]
+=========== DNS INFO =====================================================================
+Remember to update the $ORIGIN with your domain name. You may also change the $TTL.
+$ORIGIN example.com.
+$TTL    3600
+media.example.com           IN      CNAME   link.storjshare.io.
+txt-media.example.com       IN      TXT     storj-root:mybucket/myprefix
+txt-media.example.com       IN      TXT     storj-access:totallyrandomstringofnonsens
+
+

Create the DNS entries in Cloudflare with the values printed in the last three lines. Make sure you enable the proxy setting when entering the CNAME entry to enable Cloudflares CDN service.

+

And that's it. All files you put in the bucket with the correct prefix are now available under your domain! :)

+

If this blog post helped you, or you have some issues or thoughts on this, leave a comment via the comment box below or via webmention.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + cdn + cloudflare + indieweb + storj + +
+ + <![CDATA[IndieWebifying my Website Part 1 - Microformats and Webmentions]]> + https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1 + https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1 + Sat, 03 Dec 2022 20:56:54 GMT + + A few weeks ago, I stumbled on one of Jamie Tanna's blog posts about microformats2 by accident. That is when I first learned about the wonderful world of the IndieWeb. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?

+

The IndieWeb

+

I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.

+

The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.

+

The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.

+

The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.

+

Some of the standards in the IndieWeb include:

+
    +
  • Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.
  • +
  • Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.
  • +
  • IndieAuth, an OAuth2-based way to log in using only your domain name.
  • +
+

The implementation on my website

+

As explained in my earlier post First Go Project: A Jam-stack Commenting API, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.

+

Making the website machine-readable with Microformats

+

As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called h-entry. By adding the h-entry class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as p-name, p-author or dt-published.

+

While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.

+

Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.

+

Accepting comments and other interactions via Webmentions

+

The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.

+

In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.

+

Since I already have a small custom service running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions

+

It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database. +I found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.

+

I have tested my webmentions implementation using webmention.rocks, but I would appreciate it if you left me a mention as well 😃

+

Publishing short-form content such as replies, likes and bookmarks: A notes post type

+

The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called notes. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.

+

I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: full-rss.xml. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.

+

Notifying referenced websites: Sending Webmentions

+

Sending webmentions was easy compared to receiving webmentions:

+

On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.

+

Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library gocron for scheduling the regular intervals, gofeed for parsing the RSS feed and webmention for extracting links and sending webmentions.

+

In the future: IndieAuth

+

The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.

+

Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.

+ + + +]]>
+ hey@tiim.ch (Tim Bachmann) + go + indiego + indieweb + mf2 + tiim.ch + webmentions + +
+ + <![CDATA[SvelteKit Server-Side Rendering (SSR) with @urql/svelte]]> + https://tiim.ch/blog/2022-09-27-sveltekit-ssr-with-urql + https://tiim.ch/blog/2022-09-27-sveltekit-ssr-with-urql + Mon, 26 Sep 2022 00:00:00 GMT + + In this blog post, I will explain why server-side rendering with the urql GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.

+

Server-side rendering (SSR) is one of the great features of SvelteKit. I will try to keep this blog post short and will therefore not explain what server-side rendering is and why you should take advantage of it (you really should!). If you want to know more about SSR you can take a look at this article: A Deep Dive into Server-Side Rendering (SSR) in JavaScript.

+

Background - SSR in SvelteKit

+

SvelteKit implements SSR by providing a load function for every layout and page component. If a page or layout needs to perform some asynchronous operation, this should be done inside of this load function. SvelteKit executes this function asynchronously on the server side as well as on the client side and the return value of this function is assigned to the data prop of the associated component. Usually, this asynchronous operation is loading data from an external service, like in the case of this blog post a GraphQL server. +You can of course load data directly in the component, but SvelteKit will not wait for this to complete when doing SSR, and the resulting HTML will not include the loaded data.

+

Background - @urql/svelte

+

The urql library allows us to easily issue GraphQL queries and mutations. Some of the functionality it has to make our lives easier include:

+
    +
  • Reloading a query when a query variable changes
  • +
  • Reloading a query after a mutation that touches the same data as the query
  • +
+

We want to keep these features, even when using urql when doing SSR.

+

The Problem

+

When implementing SSR in my project, I ran into two problems. I couldn't find any documentation or any articles solving them, so I decided to write down my solutions to those problems in this blog post.

+

Problem 1 - Svelte and urql Reactivity

+

Let's say we have the following load function, which executes a GraphQL query to load a list of red cars:

+
// src/routes/car/+page.js
+
+/** @type {import('./$types').PageLoad} */
+export function load(event) {
+  const client = createClient({
+    url: config.url,
+    fetch: event.fetch,
+  });
+
+  const carColor = "red";
+
+  const cars = client
+    .query(carsQuery, {
+      color: carColor,
+    })
+    .toPromise()
+    .then((c) => c.data?.car);
+
+  return {
+    cars,
+  };
+}
+
+

This example uses the urql method client.query to start a query to get us a list of cars with a red colour (The GraphQL query is not shown but the exact query is not important for this example). +The client gets a special fetch function from the event which has a few nice properties, like preventing a second network request on the client side if that same request was just issued on the server-side.

+

Since the query code is now located in the load function and not in a svelte component, there is no way to easily change the carColor and have urql automatically reload the query. The only way to change the variable is to set the value as a query parameter and read that from the event argument. This however means that we have to refresh the whole page just to reload this query.

+

The other thing urql does for us, reloading the query when we do a mutation on the same data, will not work with the above code either.

+

The solution: A query in the load function and a query in the component

+

To fix those two drawbacks we have to add the same query as in the load function to our component code as well. Unfortunately, this means when a user loads the page, it sends a request from the client side, even though the same request got sent from the server side already.

+

I created a small wrapper function queryStoreInitialData that creates the query inside of the component and intelligently switches from the (possibly stale) data from the load function to the new data. Using this wrapper, the page or layout might look as follows:

+
<script>
+  import { queryStoreInitialData } from "@/lib/gql-client"; // The helper function mentioned above
+  import { getContextClient } from "@urql/svelte";
+  import { carsQuery } from "./query"; // The query
+
+  export let data;
+
+  $: gqlStore = queryStoreInitialData(
+    {
+      client: getContextClient(),
+      query: carsQuery,
+    },
+    data.cars
+  );
+  $: cars = $gqlStore?.data?.car;
+</script>
+
+<div>
+  <pre>
+    {JSON.stringify(cars, null, 2)}
+  </pre>
+</div>
+
+
    +
  1. The native queryStore function gets replaced with the wrapper function.
  2. +
  3. The initial value of the query is supplied to the wrapper
  4. +
+

Unfortunately, we can not return the query result from the load function directly like this:

+
const result = await client.query(cars, {}).toPromise();
+
+return {
+  cars: toInitialValue(result),
+};
+
+

This results in the following error:

+
Cannot stringify a function (data.events.operation.context.fetch)
+Error: Cannot stringify a function (data.events.operation.context.fetch)
+    at render_response (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/render.js:181:20)
+    at runMicrotasks (<anonymous>)
+    at processTicksAndRejections (node:internal/process/task_queues:96:5)
+    at async render_page (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/index.js:276:10)
+    at async resolve (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:232:17)
+    at async respond (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:284:20)
+    at async file:///app/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:406:22
+
+

This is because the query result contains data that is not serializable. +To fix this I created the toInitialValue function, which deletes all non-serializable elements from the result. The load function now looks like follows;

+
// src/routes/car/+page.js
+import { createServerClient, toInitialValue } from "@/lib/gql-client";
+import { parse } from "cookie";
+import { carsQuery } from "./query";
+
+/** @type {import('./$types').PageServerLoad} */
+export const load = async (event) => {
+  const client = createClient({
+    url: config.url,
+    fetch: event.fetch,
+  });
+
+  const result = await client.query(cars, {}).toPromise();
+
+  return {
+    cars: toInitialValue(result),
+  };
+};
+
+

Problem 2 - Authentication

+

We will look at the same load function as #Problem 1 - Svelte and urql Reactivity: the function creates a urql client with the fetch function from the event object and uses this client to send a query.

+

Sometimes however the GraphQL API requires authentication in the form of a cookie to allow access.

+

Unfortunately, the fetch function that we get from the load event will only pass the cookies on if the requested domain is the same as the base domain or a more specific subdomain of it. This means if your SvelteKit site runs on example.com and your GraphQL server runs on gql.example.com then the cookies will get forwarded and everything is fine. This however is, in my experience, often not the case. Either you might use an external service for your GraphQL API or you host it yourself and want to use its internal domain.

+

The only way to pass the cookies on to the GraphQL server, in this case, is by manually setting the cookie header when creating the urql client. This however forces us to use the server-only load function, as we do not have access to the cookie header in the normal load function.

+

The new code now looks like this:

+
// /src/routes/car/+page.server.js
+
+/** @type {import('./$types').PageServerLoad} */
+export function load(event) {
+  const client = createClient({
+    url: config.url,
+    fetch,
+    fetchOptions: {
+      credentials: "include",
+      headers: {
+        // inject the cookie header
+        // FIXME: change the cookie name
+        Cookie: `gql-session=${event.cookies.get("gql-session")}`,
+      },
+    },
+  });
+
+  const cars = client.query(carsQuery, {}).toPromise();
+
+  return {
+    cars: toInitialValue(result),
+  };
+}
+
+

To keep the size of the load functions across my codebase smaller I created a small wrapper function createServerClient:

+
// /src/routes/car/+page.server.js
+
+/** @type {import('./$types').PageServerLoad} */
+export function load(event) {
+  const client = createServerClient(event.cookies);
+
+  const cars = client.query(carsQuery, {}).toPromise();
+
+  return {
+    cars: toInitialValue(result),
+  };
+}
+
+

The Code

+

Below you can find the three functions createServerClient, queryStoreInitialData and toInitialValue that we used above:

+
// /src/lib/gql-client.js
+
+import { browser } from "$app/environment";
+import { urls } from "@/config";
+import { createClient, queryStore } from "@urql/svelte";
+import { derived, readable } from "svelte/store";
+
+/**
+ * Helper function to create an urql client for a server-side-only load function
+ *
+ *
+ * @param {import('@sveltejs/kit').Cookies} cookies
+ * @returns
+ */
+export function createServerClient(cookies) {
+  return createClient({
+    // FIXME: adjust your graphql url
+    url: urls.gql,
+    fetch,
+    // FIXME: if you don't need to authenticate, delete the following object:
+    fetchOptions: {
+      credentials: "include",
+      headers: {
+        // FIXME: if you want to set a cookie adjust the cookie name
+        Cookie: `gql-session=${cookies.get("gql-session")}`,
+      },
+    },
+  });
+}
+
+/**
+ * Helper method to send a GraphQL query but use the data from the SvelteKit load function initially.
+ *
+ *
+ * @param {any} queryArgs
+ * @param {any} initialValue
+ * @returns
+ */
+export function queryStoreInitialData(queryArgs, initialValue) {
+  if (!initialValue || (!initialValue.error && !initialValue.data)) {
+    throw new Error("No initial value from server");
+  }
+
+  let query = readable({ fetching: true });
+  if (browser) {
+    query = queryStore(queryArgs);
+  }
+
+  return derived(query, (value, set) => {
+    if (value.fetching) {
+      set({ ...initialValue, source: "server", fetching: true });
+    } else {
+      set({ ...value, source: "client" });
+    }
+  });
+}
+
+/**
+ * Make the result object of a urql query serialisable.
+ *
+ *
+ * @template T
+ * @param {Promise<import('@urql/svelte').OperationResult<T, any >>|import('@urql/svelte').OperationResult<T, any >} result
+ * @returns {Promise<{fetching:false, error: undefined | {name?: string, message?: string; graphQLErrors?: any[]; networkError?: Error; response?: any;}, data: T|undefined}>}
+ */
+export async function toInitialValue(result) {
+  const { error, data } = await result;
+
+  // required to turn class array into array of javascript objects
+  const errorObject = error ? {} : undefined;
+  if (errorObject) {
+    console.warn(error);
+    errorObject.graphQLErrors = error?.graphQLErrors?.map((e) => ({ ...e }));
+    errorObject.networkError = { ...error?.networkError };
+    errorObject.response = { value: "response omitted" };
+  }
+
+  return {
+    fetching: false,
+    error: { ...error, ...errorObject },
+    data,
+  };
+}
+
+

Link to the Gist

+

End remarks

+

Even though I think this solution is not too bad, I wish @urql/svelte would implement a better way to handle SSR with sveltekit. I posted a question on the urql GitHub discussions board, but I have not gotten any response yet.

+
+Info

This article was written with @svelte/kit version 1.0.0-next.499 and @urql/svelte version 3.0.1. +I will try to update this article as I update my codebase to newer versions.

+
+

If this post helped you, or you found a better or different way to solve SSR with urql, please let me know in the comments, write me an email or tag me on twitter @TiimB.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + graphql + ssr + sveltekit + urql + +
+ + <![CDATA[First Go Project: A Jam-stack Commenting API]]> + https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api + https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api + Wed, 23 Nov 2022 21:42:29 GMT + + I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on Github Pages, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.

+

I kept my eyes open for a service that I liked, but I did not want to just integrate any old service into my website, I did have some requirements:

+
    +
  • The service should not cost anything. I would rather host something myself than sign up for another subscription (because I'm already paying for a VPS anyway).
  • +
  • I want to control how the comments on my website are displayed. I quite like my website design and I don't want a generic comment box below my posts.
  • +
  • The service should respect the privacy of the people using my website.
  • +
  • There should be an option to comment without setting up an account with the service.
  • +
+

While looking around for how other people integrated comments into their static websites, I found a nice blog post from Average Linux User which compares a few popular commenting systems. +Unfortunately, most systems either are not very privacy-friendly, cost money or store the comments as comments on Github issues..? +After looking through the options I decided to use this opportunity to write my own commenting system and dabble with the Go programming language.

+

Writing a commenting API in Go

+

First thing first, if you want to take a look at the code, check out the Github repo.

+

I decided to write the commenting system in Go because I have been looking for an excuse to practice Go for a while, and this seemed like the perfect fit. It is a small CRUD app, consisting of a storage component, an API component and a small event component in the middle to easily compose the functionality I want.

+

Currently, it supports the following functionality:

+
    +
  • Listing all comments (optionally since a specified timestamp)
  • +
  • Listing all comments for a specified page (optionally since a specified timestamp)
  • +
  • Posting comments through the API
  • +
  • A simple admin dashboard that lists all comments and allows the admin to delete them
  • +
  • Email notifications when someone comments
  • +
  • Email notifications when someone replies to your comment
  • +
  • SQLite storage for comments
  • +
+

The code is built in a way to make it easy to customise the features. +For example to disable features like the email reply notifications you can just comment out the line in the main.go file that registers that hook.

+

To write custom hooks that get executed when a new comment gets submitted or one gets deleted, just implement the Handler interface and register it in the main method.

+

You can also easily add other storage options like databases or file storage by implementing the Store and SubscribtionStore interfaces.

+

Can it be used in production? 🚗💨

+

I currently use it on this website! Go test it out (I might delete the comments if they are rude though 🤔).

+

In all seriousness, I would not use it for a website where the comments are critical. But for a personal blog or similar, I don't see why not.

+

If you want to host your own version, there is a Dockerfile available. If you decide to integrate this into your website, please comment below, ping me @TiimB or shoot me an email hey@tiim.ch, I would love to check it out.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + go + indiego + project + tiim.ch + web-api + +
+ + <![CDATA[You should be using RSS]]> + https://tiim.ch/blog/2022-06-use-rss + https://tiim.ch/blog/2022-06-use-rss + Sun, 05 Jun 2022 00:00:00 GMT + + I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.

+

But there is another way! By subscribing to RSS feeds you are in control of what you are shown. Most websites, blogs, news sites and even social media sites provide RSS feeds to subscribe to. You get only the articles, videos or audio content you are subscribed to, without any algorithm messing with your attention.

+

But what exactly is an RSS feed?

+

RSS stands for "Really Simple Syndication", and it is a protocol for a website to provide a list of content. It is an old protocol, the first version was introduced in 1999, but it might be more useful nowadays than ever. +If you listen to podcasts, you are already familiar with RSS feeds: a podcast is an RSS feed which links to audio files instead of online articles. +An RSS feed is just an XML document which contains information about the feed and a list of content. +When you use an app to subscribe to an RSS feed, this app will just save the URL to the XML document and load it regularly to check if new content is available. You are completely in control of how often the feed is refreshed and what feeds you want to subscribe to. Some RSS reader apps also allow you to specify some rules for example about if you should be notified, based on the feed, the content or the tags.

+

How to subscribe to a feed?

+

Since an RSS feed is just an XML document, you don't technically have to subscribe to a feed to read it, you could just open the document and read the XML. But that would be painful. Luckily there are several plugins, apps and services that allow you to easily subscribe to and read RSS feeds.

+

If you want to start using RSS and are not sure if you will take the time to open a dedicated app, I would recommend using an RSS plugin for another software that you are using regularly. For example, the Thunderbird email client already has built-in RSS support. If you want to read to the feeds directly inside of your browser, you can use the feedbro extension for Chrome, Firefox, and other Chromium-based browsers. I use the Vivaldi browser which comes with an integrated RSS feed reader.

+

What if there is no RSS feed?

+

Unfortunately not every website offers an RSS feed. Although it might be worth it to hunt for them. Some websites offer an RSS feed but do not link to it anywhere. +If there is no feed, but a newsletter is offered, the service "Kill The Newsletter" will provide you with email addresses and a corresponding RSS URL to convert any newsletter to a feed. Another service to consider is FetchRSS. It turns any website into an RSS feed.

+

RSS Apps

+

If you want to have a dedicated app for your reading, you're in luck! There is a plethora of apps to choose from, all with different features and user interfaces. +There are three main types of apps: standalone apps, service-based apps, and self-hosted apps. Most apps are standalone, meaning they fetch the RSS feeds only when open, and don't sync to your other devices. The service-based apps rely on a cloud service which will fetch the feeds around the clock, even when all your devices are off. They can also send you a summary mail if you forget to check for some time and they can sync your subscriptions across all your devices. Unfortunately, most service-based apps only offer a limited experience for free. The last category is self-hosted apps. They are similar to the service based apps but instead of some company running the service, you have to provide a server for the service to run yourself.

+

I use a standalone app, because I do not want to rely on a service, but I also don't want to go through the hassle of setting up a self-hosted solution.

+

If you are still unsure what RSS app you could try out, I provided a list below. Make sure to add the RSS feed for my blog (https://tiim.ch/blog/rss.xml) to test it out 😉

+

Standalone Apps

+ +

Service-Based Apps

+ +

Self-hosted Apps

+ + + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + rss + software + +
+ + <![CDATA[How to set up an SSH Server on Windows with WSL]]> + https://tiim.ch/blog/2022-03-ssh-windows-wsl + https://tiim.ch/blog/2022-03-ssh-windows-wsl + Wed, 02 Mar 2022 00:00:00 GMT + + There are many guides on the internet showing how to set up an SSH server inside WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).

+

Installing the OpenSSH Server

+

Windows has been shipping with an OpenSSH client and server for a long time. They are not installed by default but can be activated either in the settings as described in the official docs or with the following PowerShell commands.

+

You will need to start PowerShell as Administrator

+

First, install the OpenSSH client and server.

+
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
+Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
+
+

Enable the SSH service and make sure the firewall rule is configured:

+
# Enable the service
+Start-Service sshd
+Set-Service -Name sshd -StartupType 'Automatic'
+
+# Confirm the firewall rule is configured. It should be created automatically by setup. Run the following to verify
+if (!(Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {
+    Write-Output "Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it..."
+    New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
+} else {
+    Write-Output "Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists."
+}
+
+

Congratulations, you have installed the SSH server on your Windows machine. And all without manually setting up a background service or modifying config files.

+

Setting WSL as Default Shell

+

To directly boot into WSL when connecting, we need to change the default shell from cmd.exe or PowerShell.exe to bash.exe, which in turn runs the default WSL distribution. This can be done with the PowerShell command:

+
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\WINDOWS\System32\bash.exe" -PropertyType String -Force
+
+

Note: even though the shell is running on the Linux side, the SSH server is still on windows. This means you have to use to windows username to log in, and the SCP command copies files relative to the user directory on windows.

+

Enable Key-based Authentication (non-Admin User)

+

Note: If the user account has Admin permissions, read the next chapter, otherwise continue reading.

+

Create the folder .ssh in the users home directory on windows: (e.g. C:\Users\<username>\.ssh). Run the following commands in PowerShell (not as administrator).

+
New-Item -Path ~\.ssh -ItmeType "directory"
+New-Item -Path ~\.ssh\authorized_keys
+
+

The file .ssh\autzorized_keys will contain a list of all public keys that shall be allowed to connect to the SSH server.

+

Copy the contents of your public key file (usually stored in ~/.ssh/id_rsa.pub) to the authorized_keys file. If a key is already present, paste your key on a new line.

+

Enable Key-based Authentication (Admin User)

+

If the user is in the Administrators group, it is not possible to have the authorized_keys file in the user directory for security purposes. +Instead, it needs to be located on the following path %ProgramData%\ssh\administrators_authorized_keys. A second requirement is that it is only accessible to Administrator users, to prevent a normal user from gaining admin permissions.

+

To create the file start PowerShell as administrator and run the following command.

+
New-Item -Path $env:programdata\ssh\administrators_authorized_keys
+
+

This will create the file with the correct permissions. Now open the file and paste your public key into it. The public key should be located at ~/.ssh/id_rsa.pub. If a key is already present, paste your key on a new line.

+

Verifying everything works

+

Verify that you can SSH into your machine by running the following inside WSL:

+
IP=$(cat /etc/resolv.conf | grep nameserver | cut -d " " -f2) # get the windows host ip address
+ssh <user>@$IP
+
+

Or from PowerShell and cmd:

+
ssh <user>@localhost
+
+

Drawbacks

+

There are some drawbacks to this approach. If you rely on some programs or scripts to work over SSH, this might not be the method for you. Most scripts expect a unix machine on the other end, or if they expect a windows machine they will most likely not be configured to deal with WSL.

+

If you however just want to connect to your pc to copy some files or change some settings this approach is perfectly fine.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + ssh + windows + wsl + +
+ + <![CDATA[How to Listen to Phone Audio on PC]]> + https://tiim.ch/blog/2022-02-phone-audio-to-pc + https://tiim.ch/blog/2022-02-phone-audio-to-pc + Sat, 12 Feb 2022 00:00:00 GMT + + Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.

+

TLDR:

+
    +
  • Either use Bluetooth Audio Receiver from the Microsoft Store to connect you phone via Bluetooth,
  • +
  • Or use an audio cable to connect the phone to the "line-in" on your PC.
  • +
+

Bluetooth (recommended)

+

Requirements: A PC with integrated Bluetooth or a Bluetooth dongle.

+

I recommend this approach more than the wired one because it is way less effort, you don't have to deal with a USB or lightning to audio dongle and in my opinion it is more reliable.

+

Pair your phone with your PC as normal, by opening the Bluetooth settings on your phone and on the PC and wait for the devices to show up. When you successfully paired the phone once you will not have to do this again. Now you need an app that will tell the phone that it can use the PC as a wireless speaker. The only app I found that will do this is the Bluetooth Audio Receiver app from the Windows Store. Install and and open it. You should see your phone on the list of Bluetooth devices on the app. Select it and click on the Open Connection button. It might take a moment but after it connected, you should hear all sounds from your phone on your PC.

+

Wired

+

Requirements:

+
    +
  • Male-to-Male audio cable (3.5mm audio jack).
  • +
  • A line-in port on your PC (usually blue audio jack on the back)
  • +
  • USB-C to audio jack adapter (Optional)
  • +
  • Lighting to audio jack adapter (Optional)
  • +
+

This approach works if your PC doesn't support Bluetooth, or if the Bluetooth connection drops for some reason. Connect the audio cable to the blue line-in jack on the back of the computer. Then, connect the phone to the other end of the audio cable. If your phone does not have an audio jack, use the adapter on the USB-C or Lightning port. If your PC detects that you connected a new line-in device, it might open the audio settings automatically. If not, right-click on the volume icon on the taskbar next to the clock and select Sounds. Navigate to the Input tab and double click on the Line-In entry (the one with a cable icon). Navigate to the Monitor tab and select the check box for "Use this device as a playback source". This will tell windows it should play all sounds received through this input directly to the speakers. Usually this is used to monitor microphones but it works for this use case too. You should now hear any sound from your phone through your PC headphones or speakers. Make sure you turn this checkbox off when you disconnect your phone. Otherwise you might hear a crackle or other sounds when the loose cable gets touched.

+

Photo by Lisa Fotios from Pexels

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + audio + bluetooth + how-to + software + windows + +
+ + <![CDATA[Modelling Git Operations as Planning Problems]]> + https://tiim.ch/blog/2021-01-git-operations-as-planning-problems + https://tiim.ch/blog/2021-01-git-operations-as-planning-problems + Mon, 18 Sep 2023 11:41:51 GMT + + Abstract +

Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.

+

Download Thesis

+

Cite

+
@thesis{bachmann2021,
+	title        = {Modelling Git Operations as Planning Problems},
+	author       = {Tim Bachmann},
+	year         = {2021},
+  month        = {01},
+	type         = {Bachelor's Thesis},
+	school       = {University of Basel},
+	doi          = {10.13140/RG.2.2.24784.17922}
+}
+
+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + git + pddl + planning-system + +
+ + <![CDATA[How to write optional filters in SQL]]> + https://tiim.ch/blog/2019-07-sql-optional-filters-coalesce + https://tiim.ch/blog/2019-07-sql-optional-filters-coalesce + Thu, 11 Jul 2019 00:00:00 GMT + + The problem +

Let's say you have a rest API with the following endpoint that returns all of the books in your database:

+
GET /book/
+
+

Your SQL query might look like something like this

+
SELECT *
+FROM books
+
+

Sometimes you want to only list books, for example, from a specific author. How do we do this in SQL?

+

Naive solution: String concatenation ✂

+

One way would be to concatenate your sql query something like this:

+
const arguments = [];
+const queryString = "SELECT * FROM books WHERE true";
+if (authorFilter != null) {
+  queryString += "AND author = ?";
+  arguments.push(authorFilter);
+}
+db.query(queryString, arguments);
+
+

I'm not much of a fan of manually concatenating strings.

+

The coalesce function 🌟

+

Most Databases have the function coalesce which accepts a variable amount of arguments and returns the first argument that is not null.

+
-- Examle
+SELECT coalesce(null, null, 'tiim.ch', null, '@TiimB') as example;
+
+-- Will return
+
+example
+---------
+tiim.ch
+
+

But how will this function help us?

+

Optional filters with the coalesce function

+
SELECT *
+FROM books
+WHERE
+  author = coalesce(?, author);
+
+

If the filter value is null the coalesce expression will resolve to author +and the comparison author = author will be true.

+

If on the other hand the value is set for example to Shakespeare then the author will be compared to Shakespeare.

+

I came across this way to implement optional filters only recently. If you have a more idiomatic way to do this let me know please ✨

+

If you liked this post please follow me on here or on Twitter under @TiimB 😎

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + quick-tip + sql + +
+ + <![CDATA[How I use Vue.js on GitHub Pages]]> + https://tiim.ch/blog/2019-05-vue-on-github-pages + https://tiim.ch/blog/2019-05-vue-on-github-pages + Sat, 04 May 2019 00:00:00 GMT + + I recently read the Article Serving Vue.js apps on GitHub Pages and it inspired me to write about what I'm doing differently.

+

If you want to see an example of this method in action, go check out my personal website on GitHub

+

I won't be explaining how to setup a Vue project. If you're looking for a Tutorial on that go check out the awesome Vue.js Guide.

+

So you have setup your awesome Vue project and want to host it on GitHub Pages. The way Muhammad explained it you would build the project using npm run build, commit the dist/ folder along with your source files and point GitHub to the dist folder. This might get quite messy because you either have commit messages with the sole purpose of uploading the dist folder or you commit the code changes at the same time which makes it hard to find the relevant changes if you ever want to look at your commits again.

+

So what can you do about this?

+

Git to the rescue, let's use a branch that contains all the build files.

+

Step 1 - keeping our working branch clean 🛀

+

To make sure that the branch we are working from stays clean of any build files we are gonna add a .gitignore file to the root.

+
# .gitignore
+dist/
+
+

Step 2 - adding a second branch 🌳

+

We are not goint to branch off master like how we would do it if we were to modify our code with the intention to merge it back to the main branch. Instead we are gonna create a squeaky clean new branch that will only ever hold the dist files. After all we will not ever need to merge these two branches together.

+

We do this by creating a new git repository inside the dist folder:

+
cd dist/
+git init
+git add .
+git commit -m 'Deploying my awesome vue app'
+
+

Step 3 - deploying 🚚

+

We are gonna force push our new git repository to a branch on GitHub. This might go against git best practices but since we won't ever checkout this branch we don't have to worry about that.

+
git push -f git@github.com:<username>/<repo>.git <branch>
+
+

⚠️ Make sure you double or tripple check your destination branch! You don't want to accidentally overwrite your working branch. Using the branch gh-pages will most likely be a good idea.

+

Step 4 - pointing GitHub to the right place 👈

+

Now we are almost done. The only thing left is telling GitHub where our assets live.

+

Go to your repo, on the top right navigate to Settings and scroll down to GitHub pages. Enable it and set your source branch to the branch you force pushed to, for example gh-pages.

+

Step 5 - automating everything 😴

+

If you don't mind doing this whole process (Step 2 and 3) every time you want to deploy you can stop now. If you're as lazy as me, here is the script I use to deploy with one command:

+
# deploy.sh
+
+#!/usr/bin/env sh
+
+# abort on errors
+set -e
+
+# build
+echo Linting..
+npm run lint
+echo Building. this may take a minute...
+npm run build
+
+# navigate into the build output directory
+cd dist
+
+# if you are deploying to a custom domain
+# echo 'example.com' > CNAME
+
+echo Deploying..
+git init
+git add -A
+git commit -m 'deploy'
+
+# deploy
+git push -f git@github.com:<username>/<repo>.git <branch>
+
+cd -
+
+
+

If your on windows look into the Windows Subsystem for Linus (WSL) it will be worth it.

+

If you are still reading, thank you very much. This is actually my first article and I'm really happy to hear about any opinions and criticisms. +Happy Coding ♥

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + github-pages + javascript + vue.js + +
+
+
\ No newline at end of file diff --git a/contact.html b/contact.html new file mode 100644 index 00000000..7ca184c8 --- /dev/null +++ b/contact.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + + +
+ + +
+ +

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/contact/__data.json b/contact/__data.json new file mode 100644 index 00000000..b4f6afa2 --- /dev/null +++ b/contact/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},null]} diff --git a/favicon.png b/favicon.png new file mode 100644 index 00000000..825b9e65 Binary files /dev/null and b/favicon.png differ diff --git a/follow.html b/follow.html new file mode 100644 index 00000000..79fade06 --- /dev/null +++ b/follow.html @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + diff --git a/follow/__data.json b/follow/__data.json new file mode 100644 index 00000000..b4f6afa2 --- /dev/null +++ b/follow/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},null]} diff --git a/full-rss.xml b/full-rss.xml new file mode 100644 index 00000000..5b31026c --- /dev/null +++ b/full-rss.xml @@ -0,0 +1,2778 @@ + + + + Tim Bachmann's Blog - Full RSS Feed + https://tiim.ch/ + Everything about web development, programming, and other things that might interest me. This feed contains blog posts as well as notes, replys and other content. + Wed, 20 Sep 2023 21:42:33 GMT + https://validator.w3.org/feed/docs/rss2.html + https://github.com/jpmonette/feed + en + + Tim Bachmann's Blog - Full RSS Feed + https://tiim.ch/swim-emoji.png + https://tiim.ch/ + + Tim Bachmann + + + <![CDATA[Getting the Absolute Path of a Remote Directory in Ansible]]> + https://tiim.ch/blog/2023-09-20-ansible-absolute-path + https://tiim.ch/blog/2023-09-20-ansible-absolute-path + Wed, 20 Sep 2023 21:39:13 GMT + + I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like ~/docker/myservice. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.

+

Deleting with elevated permission on the command line is easy: The command sudo rm -rf ~/docker/myservice performs the rm operation as the root user. In bash, this will delete the docker/myservice folder in the user's home directory, but when doing the equivalent in Ansible, this won't work!

+
# This does not work!
+- name: Delete the folder using root permissions
+  become: true
+  ansible.builtin.file:
+    path: "~/docker/myservice"
+    state: "absent"
+
+

This code will try to delete the file /user/root/docker/myservice, which is not what we wanted.

+

The bash version works because the shell first resolves the tilde in the argument to the current users' directory before calling the sudo command. In Ansible, we first switch to the root user and only then the tilde is resolved: this time to the home directory of the root user.

+

To circumvent this, we can manually resolve the path to an absolute path. Unfortunately, I have not found a straightforward way to do this in Ansible, however the bash command readlink -f <path> does exactly this. To use it in Ansible, we can use the following configuration:

+
- name: Get absolute folder path
+  ansible.builtin.command:
+    cmd: "readlink -f ~/docker/myservice"
+  register: folder_abs
+  changed_when: False
+
+- name: Debug
+  debug:
+    msg: "{{folder_abs.stdout}}" # prints /user/tim/docker/myservice
+
+- name: Delete the folder using root permissions
+  become: true
+  ansible.builtin.file:
+    path: "{{folder_abs.stdout}}"
+    state: "absent"
+
+

With this Ansible script, we manually resolve the absolute path and use it to delete the folder using root permissions. If you know of an easier way to resolve to an absolute path, please let me know!

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + ansible + bash + dev + linux + +
+ + <![CDATA[Forums]]> + https://tiim.ch/blog/2023-06-16-forums + https://tiim.ch/blog/2023-06-16-forums + Fri, 16 Jun 2023 18:56:56 GMT + + My first real programming experience was with a scripting language called AutoHotkey. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized. +When AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while. +Unfortunately, the German forum has since been discontinued, but some of the pages are still up on the Way back machine.

+

Another community I used to be really active in, was for a small indie roleplaying game called Illarion. Again, the community relied heavily on a forum for communications. This time it was used for players to engage in "out of character" communication, as well as a way to simulate a metaphorical bullet board in the game town square where characters could leave notes for each other. +Since the game was closely inspired by TTRPGs like D&D, the role-playing part was more important than the in-game mechanics. The forum allowed characters to interact with each other that were not online at the same time. Again, I got really invested in this community, even going so far as joining other guild-specific forums.

+

I eventually moved on from both of those amazing communities, because my interests changed. I left the AutoHotkey community because I started to get more involved with other programming languages, and I left the Illarion community because I (with the support of my parents) was looking for a less time-intensive game. Unfortunately, I never happened to find another online community like those two ever again...

+

Sometime later I joined Reddit and was amazed. It felt like a place where all communities come together on a single site. No need to check on multiple websites for new posts, everything neatly together in a single website, accessible on a single (third party) app. I remember wondering why people were still using forums when Reddit was so much simpler.

+

Jumping to the present and I realize that I was wrong. Even though I am subscribed to a bunch of communities on Reddit, I barely comment on any posts and posted even less. While I am a community member on record, I do not feel like one. The wealth of communities, as well as the incentive to go on the front page to see the most popular posts of the whole site, made me want to open Reddit, but it did not give me the feeling of belonging. I rather felt like a spectator that from time to time gathers the courage to shout his own ideas into the ether.

+
+

Side note: Discord comes much closer to the feeling of community. However, the nature of chat makes the interactions fleeting, being in a chat room with a few hundred other people, where every message is just a few sentences at most does not lead to the same connections. No one expects their message to be read again after a few days.

+
+

Now the company behind Reddit started to lose the goodwill of the users. While I don't think Reddit will die anytime soon, I think there are a lot of people looking for alternatives. And the best alternative to the website that killed forums is... forums.

+

While forums largely still work the same as they did 15 years ago, there have been developments that might make them more feasible for our desire to have everything accessible on a single site or on a single app. Last time a social media company, Twitter, annoyed its user base, the fediverse, and more specifically Mastodon, started to go more mainstream. This time I hope there will be other projects that profit. I have heard people mentioning the projects Kbin and Lemmy, both forum-like platforms that implement the ActivityPub specification. Same as Mastodon, this means users are able to interact with users on other instances. Even further, this should also allow users of any federated social network, such as Mastodon, to post and comment on any federated forum. Even established forum software such as Flarum and nodeBB are considering adding federation support.

+

I really hope that forums make a comeback, not only because of the nostalgia but also because to me it feels like a more sustainable way to build a community. And now with the possibility to federate via the fediverse, a forum doesn't have to be a walled garden of members any more. In the end, most importantly I hope people are still finding communities they can be as passionate about as I was, without any corporate overlords trying to keep their eyeballs on ads as long as possible.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + activitypub + fediverse + forum + reddit + +
+ + <![CDATA[Automated Planning using Property-Directed Reachability with Seed Heuristics]]> + https://tiim.ch/blog/2023-05-06-pdr-with-seed-heuristics + https://tiim.ch/blog/2023-05-06-pdr-with-seed-heuristics + Mon, 18 Sep 2023 13:32:00 GMT + + Abstract +

Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.

+

Download PDF

+

Cite

+
@phdthesis{bachmann2023,
+    author = {Bachmann, Tim},
+    year = {2023},
+    month = {05},
+    title = {Automated Planning using Property-Directed Reachability with Seed Heuristics},
+    doi = {10.13140/RG.2.2.11456.30727},
+    type = {Master's Thesis},
+    school = {University of Basel}
+}
+
+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + heuristic + pdr + planning-system + +
+ + <![CDATA[Weechat Notifications with ntfy.sh]]> + https://tiim.ch/blog/2023-03-28-weechat-notification-ntfy + https://tiim.ch/blog/2023-03-28-weechat-notification-ntfy + Tue, 28 Mar 2023 10:05:19 GMT + + In one of my last blog posts I set up WeeChat in docker, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the trigger plugin. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.

+

Also, I recently found the web service ntfy.sh. It sends push notifications whenever you send an HTTP post request to a certain URL. I already have ntfy.sh installed on my android phone, and I also found a minimal and lightweight desktop client.

+

I managed to set a WeeChat trigger up that fires every time I get mentioned (highlighted in WeeChat terminology), and a trigger that fires every time I get a private message. Both of those triggers execute the /exec command which runs an arbitrary shell command. The exec command runs the wget program to send a post request to the ntfy.sh server, which in turn sends a notification to all apps that subscribe to the same URL as the post request was sent. I would usually use the curl program for this instead of wget, but the docker default docker image doesn't contain a curl install.

+

Here you can see the two /trigger commands:

+

trigger on mention

+
/trigger addreplace notify_highlight print '' '${tg_highlight}' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data "${tg_message}" "-                   -header=Title: New highlight: ${buffer.full_name}" https://ntfy.sh/my_ntfy_topic_1234'
+
+

trigger on private message

+
/trigger addreplace notify_privmsg print '' '${tg_tag_notify} == private && ${buffer.notify} > 0' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data "${tg_message}" "--header=Title: New private message: ${buffer.full_name}" https://ntfy.sh/my_ntfy_topic_1234'
+
+

The trigger commands in detail

+

In case you don't just want to copy and paste some random command from the internet into your WeeChat (which you shouldn't do anyway), I will try to explain the trigger command that fires when you get mentioned in a message:

+

Let's first look at the trigger command itself: +/trigger addreplace <name> <hook> <argument> <condition> <variable-replace> <command> +We call the /trigger command with the addreplace subcommand. This subcommand will either register a new trigger or replace it if one with the same name already exists.

+
    +
  • name - This argument is self-explanatory, the name of the trigger. In our case I called it notify_highlight, but you could call it whatever you want.
  • +
  • hook - This argument specifies which hook or event the trigger should listen for. WeeChat is built as an event-driven platform, so pretty much anything from mouse movements to IRC messages are handled via events. In this case, we want to trigger on the print event, which is fired every time a new message gets received from IRC.
  • +
  • argument - The argument is needed for some hooks, but not for the print hook, so we are going to ignore that one for now and just set it to an empty string ''.
  • +
  • condition - The condition must evaluate to true for the trigger to fire. This is helpful because the print trigger fires for every new message, but we only want to be notified when the new message mentions our nick. The condition for this is ${tg_highlight}. You can find the list of variables that you can access with the command /trigger monitor, which prints all variables for every trigger that gets executed.
  • +
  • variable-replace - This took me a while to understand. This command is used to manipulate data and save it to a variable. The syntax is inspired by the sed command. Explaining it fully is out of the scope of this blog post, but you can take a look at the docs. In our example, we replace the whole content of the variable tg_message with the format string ${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor} which results in a sting like <tiim> Hello world!.
  • +
  • command - The last argument is the command that gets executed whenever this trigger fires. In our case, we use the /execute command, which starts the wget command which in turn sends a post request to ntfy.sh. Make sure you set the ntfy topic (the part after https://ntfy.sh/) to something private and long enough so that nobody else is going to guess it by accident.
  • +
+

Don't forget to subscribe to the ntfy topic on your phone or whatever device you want to receive the notification on.

+

The possibilities with the trigger plugin are endless, I hope this inspires you to build your own customizations using weechat.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + irc + ntfy.sh + weechat + wget + +
+ + <![CDATA[Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN]]> + https://tiim.ch/blog/2023-03-21-anyconnect-wsl2 + https://tiim.ch/blog/2023-03-21-anyconnect-wsl2 + Wed, 15 Mar 2023 15:22:04 GMT + + I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.

+

I will show you how I fixed this problem for me and explain what the commands I used do. This post is mostly for my reference, but I hope it helps anyone else as well.

+

Finding out what your problem is

+

Let's check first if we have internet access inside WSL2. For this run the ping command with an IP address as a destination:

+
ping 8.8.8.8
+
+

If you get something like this as the output, your internet connection is fine, and it's just the DNS nameserver addresses that are misconfigured, you can jump forward to Solution 2.

+
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
+64 bytes from 8.8.8.8: icmp_seq=1 ttl=108 time=4.53 ms
+64 bytes from 8.8.8.8: icmp_seq=2 ttl=108 time=3.94 ms
+64 bytes from 8.8.8.8: icmp_seq=3 ttl=108 time=3.97 ms
+64 bytes from 8.8.8.8: icmp_seq=4 ttl=108 time=3.78 ms
+64 bytes from 8.8.8.8: icmp_seq=5 ttl=108 time=3.77 ms
+64 bytes from 8.8.8.8: icmp_seq=6 ttl=108 time=3.76 ms
+64 bytes from 8.8.8.8: icmp_seq=7 ttl=108 time=3.81 ms
+
+

If you don't get any responses from the ping (i.e. no more output after the PING 8.8.8.8 (8.8.8.8) ... line), you need to configure the WSL and the VPN network adapter metric. Go to Solution 1.

+

To check if the DNS is working, we can again use the ping command, this time with a domain name:

+
ping google.com
+
+

If you get responses, the DNS and your internet connection are working! If not go to Section 2.

+

Solution 1: Fixing the Network Adapter

+

Run the following two commands in PowerShell as administrator:

+
Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match "Cisco AnyConnect"} | Set-NetIPInterface -InterfaceMetric 4000
+
+Get-NetIPInterface -InterfaceAlias "vEthernet (WSL)" | Set-NetIPInterface -InterfaceMetric 1
+
+

Let me explain what those two commands do. Both follow the same pattern of listing all network adapters, selecting a specific adapter from the list and setting its "metric".

+

You can imagine an adapter as a virtual network port on the back of your pc or laptop. But instead of sending packets through the wire, the driver for a specific port can do whatever it wants with those packets, in the case of a VPN, the packets get encrypted and forwarded to the internet via another adapter.

+

The InterfaceMetric is a value associated with each adapter that determines the order of those adapters. This allows windows to determine which adapter to prefer over another one.

+

By setting the interface metric of the Cisco adapter to 4000 and the metric of the WSL adapter to one, we allow the traffic from WSL to flow through the Cisco adapter. To be honest I do not exactly understand why this works but it does.

+

Solution 2: Registering the VPN DNS inside of WSL

+

Setting the DNS servers is, unfortunately, a little bit more involved than just running two commands, we need to edit the files /etc/wsl.conf and /etc/resolv.conf, and restart wsl in between. Let's get to it:

+

Edit the file /etc/wsl.conf inside of WSL2 using a text editor. I suggest doing this through the terminal since you need root permissions to do that:

+
sudo nano /etc/wsl.conf
+# feel free to use another editor such as vim or emacs
+
+

Most likely this file does not exist yet, otherwise, I suggest you create a backup of the original file to preserve the settings.

+

Add the following config settings into the file:

+
[network]
+generateResolvConf = false
+
+

This will instruct WSL to not override the /etc/resolv.conf file on every start-up. Save the file and restart WSL with the following command so that the changed config takes effect:

+
wsl.exe --shutdown
+
+

Now open a PowerShell terminal and list all network adapters with the following command:

+
ipconfig /all
+
+

Find the Cisco AnyConnect adapter and copy the IP addresses in the DNS-Server field. We will need those IPs in the next step.

+

Start WSL again and edit the /etc/resolv.conf file:

+
sudo nano /etc/resolv.conf
+
+

Most likely there is already something in this file, you can discard it. When undoing the changes, WSL will automatically regenerate this file anyway, so you don't need to back it up.

+

Delete all the contents and enter the IP addresses you noted down in the last step in the following format:

+
nameserver xxx.xxx.xxx.xxx
+
+

Put each address on a new line, preceded by the string nameserver. +Save the file and restart WSL with the same command as above:

+
wsl.exe --shutdown
+
+

Now open up WSL for the last time and set the immutable flag for the /etc/resolv.conf file:

+
chattr +i /etc/resolv.conf
+
+

And for the last time shut down WSL. Your DNS should now be working fine!

+

Undoing those changes

+

I did not have a need to undo the steps for Solution 1, and I'm pretty sure the metric resets after each system reboot anyway so there is not much to do.

+

To get DNS working again when not connected to the VPN run the following commands:

+
sudo chattr -i /etc/resolv.conf
+sudo rm /etc/resolv.conf
+sudo rm /etc/wsl.conf
+wsl.exe --shutdown
+
+

This will first clear the immutable flag off /etc/resolv.conf, and delete it. Next, it will delete /etc/wsl.conf if you have a backup of a previous wsl.conf file, you can replace it with that. At last, we shutdown WSL again for the changes to take effect.

+

Unfortunately, this is quite a procedure to get a VPN to work with WSL2, but I'm hopeful that this will soon not be necessairy anymore.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dns + networking + vpn + wsl + +
+ + <![CDATA["no such file or directory" after enabling CGO in Docker]]> + https://tiim.ch/blog/2023-01-24-no-such-file-or-directory-cgo + https://tiim.ch/blog/2023-01-24-no-such-file-or-directory-cgo + Tue, 24 Jan 2023 00:00:00 GMT + + Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message exec /app/indiego: no such file or directory. I had removed the CGO_ENABLE=0 variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the scratch image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.

+

To include libc into the container, I simply changed the base image from scratch to alpine, which includes libc. This makes the image slightly larger but this seemed way easier than trying to include libc directly.

+

As a bonus I got to delete the /usr/share/zoneinfo and ca-certificates.crt files, and rely on those provided by alpine.

+

You can see the commit to IndieGo here.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + cgo + docker + go +
+ + <![CDATA[Running the WeeChat IRC Client on a VPS in Docker]]> + https://tiim.ch/blog/2023-01-15-weechat-docker + https://tiim.ch/blog/2023-01-15-weechat-docker + Wed, 18 Jan 2023 11:34:27 GMT + + I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used HexChat in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.

+

The one client I have seen mentioned a lot is WeeChat (not to be confused with WeChat, the Chinese instant messenger). WeeChat runs in the terminal as a TUI and after a while of getting used to (and after enabling 'mouse mode') it seems intuitive enough.

+

The nice thing about WeeChat running not as a graphical application, is that it makes it possible to run on a server and access it from anywhere over ssh.

+
+INFO

Except on mobile devices, but weechat has mobile apps that can connect to it directly.

+
+

Since I pretty much host all my selfhosted software in docker on a VPS, I was looking if someone already published a docker image for WeeChat. There is a bunch of them, but only weechat/weechat (the official image) is still updated regularly. The docker hub page does not have any documentation, but I managed to find it in the weechat/weechat-container github repo.

+

As it says in the readme on github, you can start the container with

+
docker run -it weechat/weechat
+
+

which will run weechat directly in the foreground.

+
+Info

Don't skip the -it command line flags. The -i or --interactive keeps stdin open, which is required to send input to weechat. Weechat also closes immediately if the stdin gets closed, which took me a while to figure out. +The -t or --tty flag is required to provide a fake tty to the container. I don't really understand what that means but without this you won't see the user interface of weechat.

+
+

Running in the foreground is not really that helpful if we want to run weechat on a server, so we need to detach (let it run in the background) from the container with the -d or --detach flag. It also helps to specify a name for the container with the --name <name> argument, so we can quickly find the container again later. The docker command now looks like this:

+
docker run -it -d --name weechat weechat/weechat
+
+

When we run this command, we will notice that weechat is running in the background. To access it we can run docker attach weechat. To detach from weechat without exiting the container, we can press CTRL-p CTRL-q as described in the docker attach reference

+

I noticed that there are two versions of the weechat image: a debian version and an alpine linux version. Generally the Alpine Linux versions of containers are smaller than the Debian versions, so I decided to use the alpine version: weechat/weechat:latest-alpine.

+

With this we are practically done, but if we ever remove and restart the container, all of the chat logs and customisations to weechat will be gone. To prevent this we need to add the config and log files to a volume.

+

I generally use the folder ~/docker/(service) to point my docker volumes to, so I have a convenient place to inspect, modify and back up the data.

+

Let's create the folder and add the volume to the docker container. I also added the --restart unless-stopped flag to make sure the container gets restarted if it either exits for some reason of if docker restarts.

+
mkdir -p ~/docker/weechat/data
+mkdir -p ~/docker/weechat/config
+
+docker run -it -d --restart unless-stopped \
+    -v "~/docker/weechat/data:/home/user/.weechat" \
+    -v "~/docker/weechat/config:/home/user/.config/weechat" \
+    --name weechat weechat/weechat:latest-alpine`
+
+

Running this command on the server is all we need to have weechat running in docker.

+
+

But how do I quickly connect to weechat? Do I always have to first ssh into the server and then run docker attach?

+
+

Yes but, as almost always, we can simplify this with a bash script:

+
#!/usr/bin/env bash
+
+HOST=<ssh host>
+ssh -t "${HOST}" docker attach weechat
+
+

This bash script starts ssh with the -t flag which tells ssh that the command is interactive. +Copy this script into your ~/.local/bin folder and make it executable.

+
nano ~/.local/bin/weechat.sh
+chmod +x weechat.sh
+
+

And that's it! Running weechat.sh will open an ssh session to your server and attach to the weechat container. Happy Chatting!

+

If you liked this post, consider subscribing to my blog via RSS, or on social media. If you have any questions, feel free to contact me. I also usually hang out in ##tiim on irc.libera.chat. My name on IRC is tiim.

+
+Update 2022-01-18

I have found that at the beginning of a session, the input to weechat doesn't seem to work. Sometimes weechat refuses to let me type anything and/or doesn't recognize mouse events. +After a while of spamming keys and Alt-m (toggle mouse mode), it seems to fix itself most of the time. +I have no idea if thats a problem with weechat, with docker or with ssh, and so far have not found a solution for this. If you have the same problem or even know how to fix it, feel free to reach out.

+
+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + docker + irc + weechat + +
+ + <![CDATA[Hosting Images with Storj and Cloudflare]]> + https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting + https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting + Sat, 03 Dec 2022 13:37:33 GMT + + For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs TOS:

+
+

[...] Also, don't use Imgur to host image libraries you link to from elsewhere, content for your website, advertising, avatars, or anything else that turns us into your content delivery network.

+
+

Finally when I started indie-webifying my website, and was implementing the micropub protocol (which I will blog about at a later time), I decided that it was at the time to host the images on a platform that was meant to do that. I looked at a few storage providers such as cloudinary and S3 based object storage and landed on Storj.io, mostly because of the generous free tier, which should suffice for this little blog for quite a while.

+

One thing that bothered me slightly was that all storage providers I looked at charge for traffic. It's not the fact that it's an additional expense (if your not in the free tier anymore) that bothers me, but the fact that I don't have any control over how much this will cost me. In all likelihood this will never cost me anything since this blog has not much traffic, but if a post were to go viral (one can dream...), this could result in a surprise bill at the end of the month.

+

To help with the traffic costs I decided to try to use the free CDN functionality of Cloudflare to reduce the traffic to Storj. In this blog post I will describe how I did that.

+

Is this the right solution for you?

+

If you are in a similar situation as me, and just want to have somewhere to host your images for a personal website or to share images or screenshots as links while still having control over all your data, this could be a good solution.

+

If you want to build a robust image pipeline with resizing and image optimization, or you are building an enterprise website this is probably not the right way. You should take a look at cloudinary or one of the big cloud providers.

+

Prerequisites

+

To use Cloudflare as a CDN, you need to have Cloudflare setup as your DNS host for the domain you want to serve the images from. Even if you just want to use a subdomain like media.example.com, the whole example.com domain needs to be on cloudflare. For me this was not much of an issue, I followed the instructions from cloudflare and pointed the nameserver of my domain to cloudflare. Although I did have an issue during the migration, which resulted in my website being down for two hours. But I'm pretty sure this was caused by my previous nameserver provider.

+

Setting up Storj & Cloudflare

+

I assume you already have an account at storj.io. The next step is creating a bucket for your images. A bucket is just a place for your files and folders to live in storj, just like in any other S3 compatible storage provider. (Actually there are no folders in storj and other S3 services, the folders are just prefixes of the filenames). When creating a bucket, make sure you save the passphrase securely, such as in your password manager. Whenever storj asks you for the passphrase, make sure you don't let storj generate a new one! Every new passphrase will create access to a new bucket.

+

The next step is installing the uplink cli. Follow the quick start tutorial to get an access grant. Remember to use the same passphrase from above. Now follow the next quickstart tutorial to add the bucket to the uplink cli. The file accessgrant.txt in the tutorial only contains the access-grant string that you got from the last step.

+

Finally we want to share the bucket so the images can be accessed from the web. For this you can run the following command:

+
uplink share --dns <domain> sj://<bucket>/<prefix> --not-after=none
+
+

Replace <domain> with the domain you want to serve the images from. In my case I use media.tiim.ch. Then replace <bucket> with the name of your bucket and <prefix> with the prefix.

+

As mentioned above, you can think of a prefix as a folder. If you use for example media-site1 as a prefix, then every file in the "folder" media-site1 will be shared. This means you can use multiple prefixes to serve files for multiple websites in the same bucket.

+

You will get the following output:

+
[...]
+=========== DNS INFO =====================================================================
+Remember to update the $ORIGIN with your domain name. You may also change the $TTL.
+$ORIGIN example.com.
+$TTL    3600
+media.example.com           IN      CNAME   link.storjshare.io.
+txt-media.example.com       IN      TXT     storj-root:mybucket/myprefix
+txt-media.example.com       IN      TXT     storj-access:totallyrandomstringofnonsens
+
+

Create the DNS entries in Cloudflare with the values printed in the last three lines. Make sure you enable the proxy setting when entering the CNAME entry to enable Cloudflares CDN service.

+

And that's it. All files you put in the bucket with the correct prefix are now available under your domain! :)

+

If this blog post helped you, or you have some issues or thoughts on this, leave a comment via the comment box below or via webmention.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + cdn + cloudflare + indieweb + storj + +
+ + <![CDATA[IndieWebifying my Website Part 1 - Microformats and Webmentions]]> + https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1 + https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1 + Sat, 03 Dec 2022 20:56:54 GMT + + A few weeks ago, I stumbled on one of Jamie Tanna's blog posts about microformats2 by accident. That is when I first learned about the wonderful world of the IndieWeb. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?

+

The IndieWeb

+

I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.

+

The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.

+

The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.

+

The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.

+

Some of the standards in the IndieWeb include:

+
    +
  • Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.
  • +
  • Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.
  • +
  • IndieAuth, an OAuth2-based way to log in using only your domain name.
  • +
+

The implementation on my website

+

As explained in my earlier post First Go Project: A Jam-stack Commenting API, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.

+

Making the website machine-readable with Microformats

+

As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called h-entry. By adding the h-entry class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as p-name, p-author or dt-published.

+

While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.

+

Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.

+

Accepting comments and other interactions via Webmentions

+

The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.

+

In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.

+

Since I already have a small custom service running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions

+

It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database. +I found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.

+

I have tested my webmentions implementation using webmention.rocks, but I would appreciate it if you left me a mention as well 😃

+

Publishing short-form content such as replies, likes and bookmarks: A notes post type

+

The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called notes. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.

+

I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: full-rss.xml. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.

+

Notifying referenced websites: Sending Webmentions

+

Sending webmentions was easy compared to receiving webmentions:

+

On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.

+

Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library gocron for scheduling the regular intervals, gofeed for parsing the RSS feed and webmention for extracting links and sending webmentions.

+

In the future: IndieAuth

+

The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.

+

Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.

+ + + +]]>
+ hey@tiim.ch (Tim Bachmann) + go + indiego + indieweb + mf2 + tiim.ch + webmentions + +
+ + <![CDATA[SvelteKit Server-Side Rendering (SSR) with @urql/svelte]]> + https://tiim.ch/blog/2022-09-27-sveltekit-ssr-with-urql + https://tiim.ch/blog/2022-09-27-sveltekit-ssr-with-urql + Mon, 26 Sep 2022 00:00:00 GMT + + In this blog post, I will explain why server-side rendering with the urql GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.

+

Server-side rendering (SSR) is one of the great features of SvelteKit. I will try to keep this blog post short and will therefore not explain what server-side rendering is and why you should take advantage of it (you really should!). If you want to know more about SSR you can take a look at this article: A Deep Dive into Server-Side Rendering (SSR) in JavaScript.

+

Background - SSR in SvelteKit

+

SvelteKit implements SSR by providing a load function for every layout and page component. If a page or layout needs to perform some asynchronous operation, this should be done inside of this load function. SvelteKit executes this function asynchronously on the server side as well as on the client side and the return value of this function is assigned to the data prop of the associated component. Usually, this asynchronous operation is loading data from an external service, like in the case of this blog post a GraphQL server. +You can of course load data directly in the component, but SvelteKit will not wait for this to complete when doing SSR, and the resulting HTML will not include the loaded data.

+

Background - @urql/svelte

+

The urql library allows us to easily issue GraphQL queries and mutations. Some of the functionality it has to make our lives easier include:

+
    +
  • Reloading a query when a query variable changes
  • +
  • Reloading a query after a mutation that touches the same data as the query
  • +
+

We want to keep these features, even when using urql when doing SSR.

+

The Problem

+

When implementing SSR in my project, I ran into two problems. I couldn't find any documentation or any articles solving them, so I decided to write down my solutions to those problems in this blog post.

+

Problem 1 - Svelte and urql Reactivity

+

Let's say we have the following load function, which executes a GraphQL query to load a list of red cars:

+
// src/routes/car/+page.js
+
+/** @type {import('./$types').PageLoad} */
+export function load(event) {
+  const client = createClient({
+    url: config.url,
+    fetch: event.fetch,
+  });
+
+  const carColor = "red";
+
+  const cars = client
+    .query(carsQuery, {
+      color: carColor,
+    })
+    .toPromise()
+    .then((c) => c.data?.car);
+
+  return {
+    cars,
+  };
+}
+
+

This example uses the urql method client.query to start a query to get us a list of cars with a red colour (The GraphQL query is not shown but the exact query is not important for this example). +The client gets a special fetch function from the event which has a few nice properties, like preventing a second network request on the client side if that same request was just issued on the server-side.

+

Since the query code is now located in the load function and not in a svelte component, there is no way to easily change the carColor and have urql automatically reload the query. The only way to change the variable is to set the value as a query parameter and read that from the event argument. This however means that we have to refresh the whole page just to reload this query.

+

The other thing urql does for us, reloading the query when we do a mutation on the same data, will not work with the above code either.

+

The solution: A query in the load function and a query in the component

+

To fix those two drawbacks we have to add the same query as in the load function to our component code as well. Unfortunately, this means when a user loads the page, it sends a request from the client side, even though the same request got sent from the server side already.

+

I created a small wrapper function queryStoreInitialData that creates the query inside of the component and intelligently switches from the (possibly stale) data from the load function to the new data. Using this wrapper, the page or layout might look as follows:

+
<script>
+  import { queryStoreInitialData } from "@/lib/gql-client"; // The helper function mentioned above
+  import { getContextClient } from "@urql/svelte";
+  import { carsQuery } from "./query"; // The query
+
+  export let data;
+
+  $: gqlStore = queryStoreInitialData(
+    {
+      client: getContextClient(),
+      query: carsQuery,
+    },
+    data.cars
+  );
+  $: cars = $gqlStore?.data?.car;
+</script>
+
+<div>
+  <pre>
+    {JSON.stringify(cars, null, 2)}
+  </pre>
+</div>
+
+
    +
  1. The native queryStore function gets replaced with the wrapper function.
  2. +
  3. The initial value of the query is supplied to the wrapper
  4. +
+

Unfortunately, we can not return the query result from the load function directly like this:

+
const result = await client.query(cars, {}).toPromise();
+
+return {
+  cars: toInitialValue(result),
+};
+
+

This results in the following error:

+
Cannot stringify a function (data.events.operation.context.fetch)
+Error: Cannot stringify a function (data.events.operation.context.fetch)
+    at render_response (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/render.js:181:20)
+    at runMicrotasks (<anonymous>)
+    at processTicksAndRejections (node:internal/process/task_queues:96:5)
+    at async render_page (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/index.js:276:10)
+    at async resolve (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:232:17)
+    at async respond (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:284:20)
+    at async file:///app/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:406:22
+
+

This is because the query result contains data that is not serializable. +To fix this I created the toInitialValue function, which deletes all non-serializable elements from the result. The load function now looks like follows;

+
// src/routes/car/+page.js
+import { createServerClient, toInitialValue } from "@/lib/gql-client";
+import { parse } from "cookie";
+import { carsQuery } from "./query";
+
+/** @type {import('./$types').PageServerLoad} */
+export const load = async (event) => {
+  const client = createClient({
+    url: config.url,
+    fetch: event.fetch,
+  });
+
+  const result = await client.query(cars, {}).toPromise();
+
+  return {
+    cars: toInitialValue(result),
+  };
+};
+
+

Problem 2 - Authentication

+

We will look at the same load function as #Problem 1 - Svelte and urql Reactivity: the function creates a urql client with the fetch function from the event object and uses this client to send a query.

+

Sometimes however the GraphQL API requires authentication in the form of a cookie to allow access.

+

Unfortunately, the fetch function that we get from the load event will only pass the cookies on if the requested domain is the same as the base domain or a more specific subdomain of it. This means if your SvelteKit site runs on example.com and your GraphQL server runs on gql.example.com then the cookies will get forwarded and everything is fine. This however is, in my experience, often not the case. Either you might use an external service for your GraphQL API or you host it yourself and want to use its internal domain.

+

The only way to pass the cookies on to the GraphQL server, in this case, is by manually setting the cookie header when creating the urql client. This however forces us to use the server-only load function, as we do not have access to the cookie header in the normal load function.

+

The new code now looks like this:

+
// /src/routes/car/+page.server.js
+
+/** @type {import('./$types').PageServerLoad} */
+export function load(event) {
+  const client = createClient({
+    url: config.url,
+    fetch,
+    fetchOptions: {
+      credentials: "include",
+      headers: {
+        // inject the cookie header
+        // FIXME: change the cookie name
+        Cookie: `gql-session=${event.cookies.get("gql-session")}`,
+      },
+    },
+  });
+
+  const cars = client.query(carsQuery, {}).toPromise();
+
+  return {
+    cars: toInitialValue(result),
+  };
+}
+
+

To keep the size of the load functions across my codebase smaller I created a small wrapper function createServerClient:

+
// /src/routes/car/+page.server.js
+
+/** @type {import('./$types').PageServerLoad} */
+export function load(event) {
+  const client = createServerClient(event.cookies);
+
+  const cars = client.query(carsQuery, {}).toPromise();
+
+  return {
+    cars: toInitialValue(result),
+  };
+}
+
+

The Code

+

Below you can find the three functions createServerClient, queryStoreInitialData and toInitialValue that we used above:

+
// /src/lib/gql-client.js
+
+import { browser } from "$app/environment";
+import { urls } from "@/config";
+import { createClient, queryStore } from "@urql/svelte";
+import { derived, readable } from "svelte/store";
+
+/**
+ * Helper function to create an urql client for a server-side-only load function
+ *
+ *
+ * @param {import('@sveltejs/kit').Cookies} cookies
+ * @returns
+ */
+export function createServerClient(cookies) {
+  return createClient({
+    // FIXME: adjust your graphql url
+    url: urls.gql,
+    fetch,
+    // FIXME: if you don't need to authenticate, delete the following object:
+    fetchOptions: {
+      credentials: "include",
+      headers: {
+        // FIXME: if you want to set a cookie adjust the cookie name
+        Cookie: `gql-session=${cookies.get("gql-session")}`,
+      },
+    },
+  });
+}
+
+/**
+ * Helper method to send a GraphQL query but use the data from the SvelteKit load function initially.
+ *
+ *
+ * @param {any} queryArgs
+ * @param {any} initialValue
+ * @returns
+ */
+export function queryStoreInitialData(queryArgs, initialValue) {
+  if (!initialValue || (!initialValue.error && !initialValue.data)) {
+    throw new Error("No initial value from server");
+  }
+
+  let query = readable({ fetching: true });
+  if (browser) {
+    query = queryStore(queryArgs);
+  }
+
+  return derived(query, (value, set) => {
+    if (value.fetching) {
+      set({ ...initialValue, source: "server", fetching: true });
+    } else {
+      set({ ...value, source: "client" });
+    }
+  });
+}
+
+/**
+ * Make the result object of a urql query serialisable.
+ *
+ *
+ * @template T
+ * @param {Promise<import('@urql/svelte').OperationResult<T, any >>|import('@urql/svelte').OperationResult<T, any >} result
+ * @returns {Promise<{fetching:false, error: undefined | {name?: string, message?: string; graphQLErrors?: any[]; networkError?: Error; response?: any;}, data: T|undefined}>}
+ */
+export async function toInitialValue(result) {
+  const { error, data } = await result;
+
+  // required to turn class array into array of javascript objects
+  const errorObject = error ? {} : undefined;
+  if (errorObject) {
+    console.warn(error);
+    errorObject.graphQLErrors = error?.graphQLErrors?.map((e) => ({ ...e }));
+    errorObject.networkError = { ...error?.networkError };
+    errorObject.response = { value: "response omitted" };
+  }
+
+  return {
+    fetching: false,
+    error: { ...error, ...errorObject },
+    data,
+  };
+}
+
+

Link to the Gist

+

End remarks

+

Even though I think this solution is not too bad, I wish @urql/svelte would implement a better way to handle SSR with sveltekit. I posted a question on the urql GitHub discussions board, but I have not gotten any response yet.

+
+Info

This article was written with @svelte/kit version 1.0.0-next.499 and @urql/svelte version 3.0.1. +I will try to update this article as I update my codebase to newer versions.

+
+

If this post helped you, or you found a better or different way to solve SSR with urql, please let me know in the comments, write me an email or tag me on twitter @TiimB.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + graphql + ssr + sveltekit + urql + +
+ + <![CDATA[First Go Project: A Jam-stack Commenting API]]> + https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api + https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api + Wed, 23 Nov 2022 21:42:29 GMT + + I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on Github Pages, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.

+

I kept my eyes open for a service that I liked, but I did not want to just integrate any old service into my website, I did have some requirements:

+
    +
  • The service should not cost anything. I would rather host something myself than sign up for another subscription (because I'm already paying for a VPS anyway).
  • +
  • I want to control how the comments on my website are displayed. I quite like my website design and I don't want a generic comment box below my posts.
  • +
  • The service should respect the privacy of the people using my website.
  • +
  • There should be an option to comment without setting up an account with the service.
  • +
+

While looking around for how other people integrated comments into their static websites, I found a nice blog post from Average Linux User which compares a few popular commenting systems. +Unfortunately, most systems either are not very privacy-friendly, cost money or store the comments as comments on Github issues..? +After looking through the options I decided to use this opportunity to write my own commenting system and dabble with the Go programming language.

+

Writing a commenting API in Go

+

First thing first, if you want to take a look at the code, check out the Github repo.

+

I decided to write the commenting system in Go because I have been looking for an excuse to practice Go for a while, and this seemed like the perfect fit. It is a small CRUD app, consisting of a storage component, an API component and a small event component in the middle to easily compose the functionality I want.

+

Currently, it supports the following functionality:

+
    +
  • Listing all comments (optionally since a specified timestamp)
  • +
  • Listing all comments for a specified page (optionally since a specified timestamp)
  • +
  • Posting comments through the API
  • +
  • A simple admin dashboard that lists all comments and allows the admin to delete them
  • +
  • Email notifications when someone comments
  • +
  • Email notifications when someone replies to your comment
  • +
  • SQLite storage for comments
  • +
+

The code is built in a way to make it easy to customise the features. +For example to disable features like the email reply notifications you can just comment out the line in the main.go file that registers that hook.

+

To write custom hooks that get executed when a new comment gets submitted or one gets deleted, just implement the Handler interface and register it in the main method.

+

You can also easily add other storage options like databases or file storage by implementing the Store and SubscribtionStore interfaces.

+

Can it be used in production? 🚗💨

+

I currently use it on this website! Go test it out (I might delete the comments if they are rude though 🤔).

+

In all seriousness, I would not use it for a website where the comments are critical. But for a personal blog or similar, I don't see why not.

+

If you want to host your own version, there is a Dockerfile available. If you decide to integrate this into your website, please comment below, ping me @TiimB or shoot me an email hey@tiim.ch, I would love to check it out.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + go + indiego + project + tiim.ch + web-api + +
+ + <![CDATA[You should be using RSS]]> + https://tiim.ch/blog/2022-06-use-rss + https://tiim.ch/blog/2022-06-use-rss + Sun, 05 Jun 2022 00:00:00 GMT + + I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.

+

But there is another way! By subscribing to RSS feeds you are in control of what you are shown. Most websites, blogs, news sites and even social media sites provide RSS feeds to subscribe to. You get only the articles, videos or audio content you are subscribed to, without any algorithm messing with your attention.

+

But what exactly is an RSS feed?

+

RSS stands for "Really Simple Syndication", and it is a protocol for a website to provide a list of content. It is an old protocol, the first version was introduced in 1999, but it might be more useful nowadays than ever. +If you listen to podcasts, you are already familiar with RSS feeds: a podcast is an RSS feed which links to audio files instead of online articles. +An RSS feed is just an XML document which contains information about the feed and a list of content. +When you use an app to subscribe to an RSS feed, this app will just save the URL to the XML document and load it regularly to check if new content is available. You are completely in control of how often the feed is refreshed and what feeds you want to subscribe to. Some RSS reader apps also allow you to specify some rules for example about if you should be notified, based on the feed, the content or the tags.

+

How to subscribe to a feed?

+

Since an RSS feed is just an XML document, you don't technically have to subscribe to a feed to read it, you could just open the document and read the XML. But that would be painful. Luckily there are several plugins, apps and services that allow you to easily subscribe to and read RSS feeds.

+

If you want to start using RSS and are not sure if you will take the time to open a dedicated app, I would recommend using an RSS plugin for another software that you are using regularly. For example, the Thunderbird email client already has built-in RSS support. If you want to read to the feeds directly inside of your browser, you can use the feedbro extension for Chrome, Firefox, and other Chromium-based browsers. I use the Vivaldi browser which comes with an integrated RSS feed reader.

+

What if there is no RSS feed?

+

Unfortunately not every website offers an RSS feed. Although it might be worth it to hunt for them. Some websites offer an RSS feed but do not link to it anywhere. +If there is no feed, but a newsletter is offered, the service "Kill The Newsletter" will provide you with email addresses and a corresponding RSS URL to convert any newsletter to a feed. Another service to consider is FetchRSS. It turns any website into an RSS feed.

+

RSS Apps

+

If you want to have a dedicated app for your reading, you're in luck! There is a plethora of apps to choose from, all with different features and user interfaces. +There are three main types of apps: standalone apps, service-based apps, and self-hosted apps. Most apps are standalone, meaning they fetch the RSS feeds only when open, and don't sync to your other devices. The service-based apps rely on a cloud service which will fetch the feeds around the clock, even when all your devices are off. They can also send you a summary mail if you forget to check for some time and they can sync your subscriptions across all your devices. Unfortunately, most service-based apps only offer a limited experience for free. The last category is self-hosted apps. They are similar to the service based apps but instead of some company running the service, you have to provide a server for the service to run yourself.

+

I use a standalone app, because I do not want to rely on a service, but I also don't want to go through the hassle of setting up a self-hosted solution.

+

If you are still unsure what RSS app you could try out, I provided a list below. Make sure to add the RSS feed for my blog (https://tiim.ch/blog/rss.xml) to test it out 😉

+

Standalone Apps

+ +

Service-Based Apps

+ +

Self-hosted Apps

+ + + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + rss + software + +
+ + <![CDATA[How to set up an SSH Server on Windows with WSL]]> + https://tiim.ch/blog/2022-03-ssh-windows-wsl + https://tiim.ch/blog/2022-03-ssh-windows-wsl + Wed, 02 Mar 2022 00:00:00 GMT + + There are many guides on the internet showing how to set up an SSH server inside WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).

+

Installing the OpenSSH Server

+

Windows has been shipping with an OpenSSH client and server for a long time. They are not installed by default but can be activated either in the settings as described in the official docs or with the following PowerShell commands.

+

You will need to start PowerShell as Administrator

+

First, install the OpenSSH client and server.

+
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
+Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
+
+

Enable the SSH service and make sure the firewall rule is configured:

+
# Enable the service
+Start-Service sshd
+Set-Service -Name sshd -StartupType 'Automatic'
+
+# Confirm the firewall rule is configured. It should be created automatically by setup. Run the following to verify
+if (!(Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {
+    Write-Output "Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it..."
+    New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
+} else {
+    Write-Output "Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists."
+}
+
+

Congratulations, you have installed the SSH server on your Windows machine. And all without manually setting up a background service or modifying config files.

+

Setting WSL as Default Shell

+

To directly boot into WSL when connecting, we need to change the default shell from cmd.exe or PowerShell.exe to bash.exe, which in turn runs the default WSL distribution. This can be done with the PowerShell command:

+
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\WINDOWS\System32\bash.exe" -PropertyType String -Force
+
+

Note: even though the shell is running on the Linux side, the SSH server is still on windows. This means you have to use to windows username to log in, and the SCP command copies files relative to the user directory on windows.

+

Enable Key-based Authentication (non-Admin User)

+

Note: If the user account has Admin permissions, read the next chapter, otherwise continue reading.

+

Create the folder .ssh in the users home directory on windows: (e.g. C:\Users\<username>\.ssh). Run the following commands in PowerShell (not as administrator).

+
New-Item -Path ~\.ssh -ItmeType "directory"
+New-Item -Path ~\.ssh\authorized_keys
+
+

The file .ssh\autzorized_keys will contain a list of all public keys that shall be allowed to connect to the SSH server.

+

Copy the contents of your public key file (usually stored in ~/.ssh/id_rsa.pub) to the authorized_keys file. If a key is already present, paste your key on a new line.

+

Enable Key-based Authentication (Admin User)

+

If the user is in the Administrators group, it is not possible to have the authorized_keys file in the user directory for security purposes. +Instead, it needs to be located on the following path %ProgramData%\ssh\administrators_authorized_keys. A second requirement is that it is only accessible to Administrator users, to prevent a normal user from gaining admin permissions.

+

To create the file start PowerShell as administrator and run the following command.

+
New-Item -Path $env:programdata\ssh\administrators_authorized_keys
+
+

This will create the file with the correct permissions. Now open the file and paste your public key into it. The public key should be located at ~/.ssh/id_rsa.pub. If a key is already present, paste your key on a new line.

+

Verifying everything works

+

Verify that you can SSH into your machine by running the following inside WSL:

+
IP=$(cat /etc/resolv.conf | grep nameserver | cut -d " " -f2) # get the windows host ip address
+ssh <user>@$IP
+
+

Or from PowerShell and cmd:

+
ssh <user>@localhost
+
+

Drawbacks

+

There are some drawbacks to this approach. If you rely on some programs or scripts to work over SSH, this might not be the method for you. Most scripts expect a unix machine on the other end, or if they expect a windows machine they will most likely not be configured to deal with WSL.

+

If you however just want to connect to your pc to copy some files or change some settings this approach is perfectly fine.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + ssh + windows + wsl + +
+ + <![CDATA[How to Listen to Phone Audio on PC]]> + https://tiim.ch/blog/2022-02-phone-audio-to-pc + https://tiim.ch/blog/2022-02-phone-audio-to-pc + Sat, 12 Feb 2022 00:00:00 GMT + + Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.

+

TLDR:

+
    +
  • Either use Bluetooth Audio Receiver from the Microsoft Store to connect you phone via Bluetooth,
  • +
  • Or use an audio cable to connect the phone to the "line-in" on your PC.
  • +
+

Bluetooth (recommended)

+

Requirements: A PC with integrated Bluetooth or a Bluetooth dongle.

+

I recommend this approach more than the wired one because it is way less effort, you don't have to deal with a USB or lightning to audio dongle and in my opinion it is more reliable.

+

Pair your phone with your PC as normal, by opening the Bluetooth settings on your phone and on the PC and wait for the devices to show up. When you successfully paired the phone once you will not have to do this again. Now you need an app that will tell the phone that it can use the PC as a wireless speaker. The only app I found that will do this is the Bluetooth Audio Receiver app from the Windows Store. Install and and open it. You should see your phone on the list of Bluetooth devices on the app. Select it and click on the Open Connection button. It might take a moment but after it connected, you should hear all sounds from your phone on your PC.

+

Wired

+

Requirements:

+
    +
  • Male-to-Male audio cable (3.5mm audio jack).
  • +
  • A line-in port on your PC (usually blue audio jack on the back)
  • +
  • USB-C to audio jack adapter (Optional)
  • +
  • Lighting to audio jack adapter (Optional)
  • +
+

This approach works if your PC doesn't support Bluetooth, or if the Bluetooth connection drops for some reason. Connect the audio cable to the blue line-in jack on the back of the computer. Then, connect the phone to the other end of the audio cable. If your phone does not have an audio jack, use the adapter on the USB-C or Lightning port. If your PC detects that you connected a new line-in device, it might open the audio settings automatically. If not, right-click on the volume icon on the taskbar next to the clock and select Sounds. Navigate to the Input tab and double click on the Line-In entry (the one with a cable icon). Navigate to the Monitor tab and select the check box for "Use this device as a playback source". This will tell windows it should play all sounds received through this input directly to the speakers. Usually this is used to monitor microphones but it works for this use case too. You should now hear any sound from your phone through your PC headphones or speakers. Make sure you turn this checkbox off when you disconnect your phone. Otherwise you might hear a crackle or other sounds when the loose cable gets touched.

+

Photo by Lisa Fotios from Pexels

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + audio + bluetooth + how-to + software + windows + +
+ + <![CDATA[Modelling Git Operations as Planning Problems]]> + https://tiim.ch/blog/2021-01-git-operations-as-planning-problems + https://tiim.ch/blog/2021-01-git-operations-as-planning-problems + Mon, 18 Sep 2023 11:41:51 GMT + + Abstract +

Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.

+

Download Thesis

+

Cite

+
@thesis{bachmann2021,
+	title        = {Modelling Git Operations as Planning Problems},
+	author       = {Tim Bachmann},
+	year         = {2021},
+  month        = {01},
+	type         = {Bachelor's Thesis},
+	school       = {University of Basel},
+	doi          = {10.13140/RG.2.2.24784.17922}
+}
+
+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + git + pddl + planning-system + +
+ + <![CDATA[How to write optional filters in SQL]]> + https://tiim.ch/blog/2019-07-sql-optional-filters-coalesce + https://tiim.ch/blog/2019-07-sql-optional-filters-coalesce + Thu, 11 Jul 2019 00:00:00 GMT + + The problem +

Let's say you have a rest API with the following endpoint that returns all of the books in your database:

+
GET /book/
+
+

Your SQL query might look like something like this

+
SELECT *
+FROM books
+
+

Sometimes you want to only list books, for example, from a specific author. How do we do this in SQL?

+

Naive solution: String concatenation ✂

+

One way would be to concatenate your sql query something like this:

+
const arguments = [];
+const queryString = "SELECT * FROM books WHERE true";
+if (authorFilter != null) {
+  queryString += "AND author = ?";
+  arguments.push(authorFilter);
+}
+db.query(queryString, arguments);
+
+

I'm not much of a fan of manually concatenating strings.

+

The coalesce function 🌟

+

Most Databases have the function coalesce which accepts a variable amount of arguments and returns the first argument that is not null.

+
-- Examle
+SELECT coalesce(null, null, 'tiim.ch', null, '@TiimB') as example;
+
+-- Will return
+
+example
+---------
+tiim.ch
+
+

But how will this function help us?

+

Optional filters with the coalesce function

+
SELECT *
+FROM books
+WHERE
+  author = coalesce(?, author);
+
+

If the filter value is null the coalesce expression will resolve to author +and the comparison author = author will be true.

+

If on the other hand the value is set for example to Shakespeare then the author will be compared to Shakespeare.

+

I came across this way to implement optional filters only recently. If you have a more idiomatic way to do this let me know please ✨

+

If you liked this post please follow me on here or on Twitter under @TiimB 😎

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + quick-tip + sql + +
+ + <![CDATA[How I use Vue.js on GitHub Pages]]> + https://tiim.ch/blog/2019-05-vue-on-github-pages + https://tiim.ch/blog/2019-05-vue-on-github-pages + Sat, 04 May 2019 00:00:00 GMT + + I recently read the Article Serving Vue.js apps on GitHub Pages and it inspired me to write about what I'm doing differently.

+

If you want to see an example of this method in action, go check out my personal website on GitHub

+

I won't be explaining how to setup a Vue project. If you're looking for a Tutorial on that go check out the awesome Vue.js Guide.

+

So you have setup your awesome Vue project and want to host it on GitHub Pages. The way Muhammad explained it you would build the project using npm run build, commit the dist/ folder along with your source files and point GitHub to the dist folder. This might get quite messy because you either have commit messages with the sole purpose of uploading the dist folder or you commit the code changes at the same time which makes it hard to find the relevant changes if you ever want to look at your commits again.

+

So what can you do about this?

+

Git to the rescue, let's use a branch that contains all the build files.

+

Step 1 - keeping our working branch clean 🛀

+

To make sure that the branch we are working from stays clean of any build files we are gonna add a .gitignore file to the root.

+
# .gitignore
+dist/
+
+

Step 2 - adding a second branch 🌳

+

We are not goint to branch off master like how we would do it if we were to modify our code with the intention to merge it back to the main branch. Instead we are gonna create a squeaky clean new branch that will only ever hold the dist files. After all we will not ever need to merge these two branches together.

+

We do this by creating a new git repository inside the dist folder:

+
cd dist/
+git init
+git add .
+git commit -m 'Deploying my awesome vue app'
+
+

Step 3 - deploying 🚚

+

We are gonna force push our new git repository to a branch on GitHub. This might go against git best practices but since we won't ever checkout this branch we don't have to worry about that.

+
git push -f git@github.com:<username>/<repo>.git <branch>
+
+

⚠️ Make sure you double or tripple check your destination branch! You don't want to accidentally overwrite your working branch. Using the branch gh-pages will most likely be a good idea.

+

Step 4 - pointing GitHub to the right place 👈

+

Now we are almost done. The only thing left is telling GitHub where our assets live.

+

Go to your repo, on the top right navigate to Settings and scroll down to GitHub pages. Enable it and set your source branch to the branch you force pushed to, for example gh-pages.

+

Step 5 - automating everything 😴

+

If you don't mind doing this whole process (Step 2 and 3) every time you want to deploy you can stop now. If you're as lazy as me, here is the script I use to deploy with one command:

+
# deploy.sh
+
+#!/usr/bin/env sh
+
+# abort on errors
+set -e
+
+# build
+echo Linting..
+npm run lint
+echo Building. this may take a minute...
+npm run build
+
+# navigate into the build output directory
+cd dist
+
+# if you are deploying to a custom domain
+# echo 'example.com' > CNAME
+
+echo Deploying..
+git init
+git add -A
+git commit -m 'deploy'
+
+# deploy
+git push -f git@github.com:<username>/<repo>.git <branch>
+
+cd -
+
+
+

If your on windows look into the Windows Subsystem for Linus (WSL) it will be worth it.

+

If you are still reading, thank you very much. This is actually my first article and I'm really happy to hear about any opinions and criticisms. +Happy Coding ♥

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + github-pages + javascript + vue.js + +
+ + <![CDATA[The 3D Scanning Wiki]]> + https://tiim.ch/projects/3d-scanning-wiki + https://tiim.ch/projects/3d-scanning-wiki + Mon, 21 Nov 2022 12:45:23 GMT + A github hosed wiki for all things 3D scanning: photogrammetry, lidar, laser scanning and more. The page is a static website built from a github repository with markdown files.

+
+Deprecated

The 3D scanning wiki is now offline. But all pages are still available on github.

+
+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + markdown + sveltekit +
+ + <![CDATA[3D Scanned and Modeled Objects]]> + https://tiim.ch/projects/3d-scans + https://tiim.ch/projects/3d-scans + Wed, 08 Jun 2022 20:15:48 GMT + I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.

+

Example of a 3D scanned model: A pile of clothes on the floor

+

For 3D modeling I use the open source tool Blender and for 3D scanning my software of choice is Reality Capture.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + 3d + 3d-model + 3d-scanning +
+ + <![CDATA[Aqualetics Coach]]> + https://tiim.ch/projects/aqualetics-coach + https://tiim.ch/projects/aqualetics-coach + Wed, 08 Jun 2022 20:15:48 GMT + An internal web app for swim schools. Developed specifically for the "Kids" program of Swiss Aquatics. Live in production at the Aqualetics swim school since August 2019.

+

The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents. +The admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.

+

Screenshot of the coaches view

+

The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + docker + graphql + hasura + node + postgresql + vue +
+ + <![CDATA[IndieGo]]> + https://tiim.ch/projects/indiego + https://tiim.ch/projects/indiego + Wed, 02 Aug 2023 08:39:00 GMT + I blogged about creating a comment system for my website a while ago, +and later how I implemented webmentions into that same project. +Since then this little go program has grown quite a bit, and it has turned into a modular platform +that supports quite a few technologies:

+
    +
  • The basic commenting system
  • +
  • Sending and receiving webmentions
  • +
  • Micropub server implementation
  • +
  • IndieAuth (decentralized authentication standard based on OAuth)
  • +
  • Admin dashboard
  • +
  • Admin backup endpoint
  • +
+

Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and +comments through the fediverse show up back on my website.

+

The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core +of the application is only concerned with initializing those plugins.

+

If you have any questions, or want to run IndieGo yourself, don't hesitate to contact me.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + docker + go + golang + indieweb + sqlite + +
+ + <![CDATA[Lap Counter]]> + https://tiim.ch/projects/lap-counter + https://tiim.ch/projects/lap-counter + Wed, 08 Jun 2022 20:15:48 GMT + A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.

+

I built this little page to help me count laps for my swimmers for some time based test sets. The first version of the app just had a grid of static buttons, one for each athlete. I quickly found that it is very hard to keep track which laps I already counted. As a way to visualise when each button was last pressed, they change colour. The buttons start green when pressed and slowly turn red over time, based on the average duration of a lap.

+

The page works completely offline (after the page loads) and no data is sent to any server. It is also possible to export the data to a csv file.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + svelte + swim +
+ + <![CDATA[Lenex Splits Sheet Creator]]> + https://tiim.ch/projects/lenex-split-sheet + https://tiim.ch/projects/lenex-split-sheet + Wed, 08 Jun 2022 20:15:48 GMT + Generate useful splits sheets directly from your Lenex (.lxf, .lef) file that you used to sign up the athletes for a meet.

+

Screenshot of a split sheet

+

The split sheet creator is a quick and easy way to create a split sheet from your Lenex sign-up file. +The split sheet creator does not send your Lenex file to any servers. The file is opened directly in your browser and never leaves your computer!

+

I built this web app using my lenex javascript library.

+

What is a split sheet?

+

Swim coaches often write down the times of athletes after some fractions of a race (splits). Usually those splits are recorded every lap or every second lap.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + lenex + svelte + swim +
+ + <![CDATA[Android Markdown Widget]]> + https://tiim.ch/projects/markdown-widget + https://tiim.ch/projects/markdown-widget + Wed, 02 Aug 2023 08:59:00 GMT + A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.

+

Android widgets are handled by the operating system and only support a limited set of features for rendering. +To display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + android + dev + java + kotlin + markdown + widget +
+ + <![CDATA[My Cheat Sheets]]> + https://tiim.ch/projects/my-cheatsheets + https://tiim.ch/projects/my-cheatsheets + Wed, 08 Jun 2022 20:15:48 GMT + My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.

+

I currently do not update this repo anymore because i moved all cheat sheets into my Obsidian Vault. I am looking into a way to keep those two repositories in sync in the future.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + cheatsheet + dev + markdown +
+ + <![CDATA[Obsidian.md Fitbit Activity Script]]> + https://tiim.ch/projects/obsidian-fitbit-script + https://tiim.ch/projects/obsidian-fitbit-script + Wed, 08 Jun 2022 20:15:48 GMT + If you use Obsidian.md to track your daily activity and wear a fitbit, this script is for you! This is a user script for the Templater Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + fitbit + obsidian + plugin +
+ + <![CDATA[Pomo 🍅]]> + https://tiim.ch/projects/pomo + https://tiim.ch/projects/pomo + Thu, 03 Aug 2023 11:03:00 GMT + I created pomo as a way to keep me focused for working on my masters thesis, and at the same time +allowed me to learn the rust programming language.

+

Pomo is a simple pomodoro timer. It allows you to either specify the number of repetitions (pomodori), the duration of the pomodori and the duration of the breaks, or +you can stecify an end time, and let pomo calculate the durations and repetitions.

+

Pomo runs as a cli tool and stores the current state in a json file. All pomo executions excep pomo watch just +modify this json file and terminate. The watch command displays the current pomodoro timer, optionally writes the timer to a text file, +and watches for changes of the json file.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + cli + dev + rust +
+ + <![CDATA[Swim Club Birsfelden Website]]> + https://tiim.ch/projects/scbirs-website + https://tiim.ch/projects/scbirs-website + Wed, 08 Jun 2022 20:15:48 GMT + The website of the "ScBirs" swim club. This website serves as the center of all information distribution for the swimclub.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + php + wordpress +
+ + <![CDATA[TeamKit]]> + https://tiim.ch/projects/teamkit + https://tiim.ch/projects/teamkit + Sun, 27 Nov 2022 09:19:08 GMT + TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.

+

I built TeamKit because the swim club I am a part of needed an easy way for coaches to have an overview of their teams, handle attendance and track their own hours (not implemented yet). I tried a bunch of existing apps and services, but all of them were either too clunky for us or required the team members to sign up as well. This was a dealbreaker for us because we have a bunch of kids teams which are too young to sign up to websites, and because many of the parents are not very tech literate.

+

With TeamKit a coach can quickly add new team members to a team, create new members directly in the event view (useful for example a person that just wants to try out) without having to add more details than a name.

+

In the latest update, TeamKit now allows users to create notes on events. This is useful for planning, writing quick notes for an event or a practice session and for sharing information with other coaches.

+

If you are interested, TeamKit is currenlty free while it is still in beta.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + hasura + postgres + sveltekit +
+ + <![CDATA[WooCommerce Order Explorer]]> + https://tiim.ch/projects/woocommerce-order-explorer + https://tiim.ch/projects/woocommerce-order-explorer + Wed, 08 Jun 2022 20:15:48 GMT + A small client side only web app to browse all open orders on your WooCommerce store. All data is stored in the browser.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + dev + svelte + woocommerce + wordpress +
+ + <![CDATA[Blogroll and Links]]> + https://tiim.ch/pages/links + https://tiim.ch/pages/links + Mon, 31 Jul 2023 19:06:29 GMT + + I am planning to publish the list of blogs I subscribe to here in the future. But I still have not figured out how +to automatically export the list of feeds from Mozilla Thunderbird and import it here. Until then, this page consists of +a manual list of pages that I like.

+

Links

+
    +
  • Ye Olde Blogroll, a human curated list of personal blogs.
  • +
  • uses.tech, a list of personal websites that have a /uses page.
  • +
  • changelog.com/news, curated developer news as a podcast and as a newsletter. Unfortunately not available a RSS subscribable blog post.
  • +
+

You have some suggestions for other cool or useful websites or blogs? Let me know.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) +
+ + <![CDATA[/uses]]> + https://tiim.ch/pages/uses + https://tiim.ch/pages/uses + Sun, 30 Jul 2023 19:21:29 GMT + + This page lists most of the gear and tools I use on a daily basis. This page is inspired by @wesbos' uses.tech page.

+

Software

+

Editor and Terminal

+
    +
  • NeoVim, +I have recently switched to neovim from vscode. I planned to just give vim a spin for a few weeks, but somehow I don't seem to be able to go back 🤔.
  • +
  • I use the Nightfox.nvim theme. Before switching to neovim, I used the excelent Beyond the Horizon Dark VSCode Theme, which is based on the Horizon Dark colour scheme, which I kind of miss. This website uses this colour scheme too.
  • +
  • My favourite monospace font is currently Fira Code, which I use in the terminal, and therefore in the editor as well.
  • +
  • I use the Windows Terminal as my main terminal. I like it because it lets me easily switch between WSL2 and Powershell or CMD, it supports custom font, and it just looks really nice.
  • +
+

Note Taking

+
    +
  • Obsidian.md is the first note taking tool I actually use on a regular basis. I really enjoy the flexibility of it, especially regarding the quickly growing plugin ecosystem. A few of my favourite plugins are in no particular order: + +
  • +
+

Other Apps

+
    +
  • Firefox has been my default browser for many years now. I switched back to it after using chrome because I agree with its focus on privacy.
  • +
  • Mozilla Thunderbird, I used to grudgingly use thunderbird as my email program because I have not found anything better yet. However, in the recent months the changes to thunderbird make it way more enjoyable to use. I still miss a way to sync my settings and RSS feeds between my laptop and my desktop computer though.
  • +
  • I heavily rely on FancyZones of Windows PowerToys to arrange my windows in a tiled-ish way.
  • +
  • Microsoft To Do, I have recently been looking for a good and simple program to keep all my tasks. Microsoft To Do seems to fit this quite well and looks stunning.
  • +
  • Trello, just the easiest way to keep track of projects that consist of multiple tasks. I have a trello board for most of my side projects, as well as for private projects.
  • +
+

Tech Stack

+
    +
  • Svelte / Svelte kit is currently my favourite frontend web framework for highly dynamic content. I previously used Vue.js and React but I like the pragmatic way of svelte the most so far.
  • +
  • Hasura makes building web applications a breeze. It saves me from reimplementing the same fundamental things such as the GraphQL API, authorisation and admin dashboard for every project.
  • +
  • Docker in combination with Traefik makes hosting multiple applications on the same server a breeze.
  • +
  • For future projects that have a server side component, I am strongly considering using htmx for the frontend.
  • +
+

Gear

+

PC Gear

+
    +
  • Tascam TM-80 is the XLR microphone I am currently using. I got it quite recently after I got fed up with my ugly USB microphone.
  • +
  • As an audio interface I use the minimalist Audient evo 4.
  • +
  • Logitech c920 is my reliable webcam. I got it a year before the pandemic (thankfully!) and I am quite happy with it.
  • +
  • At work I use the Ultimate Hacking Keyboard This is my first real mechanical keyboard. I really like the split keyboard layout which makes typing much more ergonomic in my opinion. I use MX Brown switches.
  • +
  • At home I recently got the [Keychron K8 Pro] keyboard with Gateron G Pro Brown switches. I really love the sound and the feel of this keyboard.
  • +
  • The Logitech MX Master 3 mouse is the newest addition to my gear, and the first wireless mouse I own. I was sceptical about the battery duration at first but it really is not an issue for me.
  • +
  • Wacom Intuos Pen & Touch small was the drawing tablet I purchased a long time ago when I tried to get into drawing. Nowadays I use it mostly for writing on virtual whiteboards.
  • +
+

Multimedia Gear

+
    +
  • I take pictures with my trusty Panasonic Lumix G7. I really love this camera, it is still very good for the price and even log lenses are not bulky at all. Check out some of my pictures on my Flickr.
  • +
+
+

This page is a work in progress, if your are curious about anything else in my setup shoot me a toot @tiim@indieweb.social, or comment down below.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) +
+ + https://tiim.ch/mf2/2023/07/mteymz + https://tiim.ch/mf2/2023/07/mteymz + Sun, 02 Jul 2023 17:58:00 GMT + +

This post is in reply to "https://kevincox.ca/2023/06/27/decade-of-rss-via-email/"

+

Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds. +The syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + email + newsletter + rss +
+ + https://tiim.ch/mf2/2022/12/ota4mt + https://tiim.ch/mf2/2022/12/ota4mt + Fri, 09 Dec 2022 10:50:00 GMT + The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once.

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + photo + winter + +
+ + https://tiim.ch/mf2/2022/12/mte4nd + https://tiim.ch/mf2/2022/12/mte4nd + Sun, 04 Dec 2022 13:08:00 GMT + +

Liked https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==

+ + + +]]>
+ hey@tiim.ch (Tim Bachmann) + caddy + dev + golang +
+ + https://tiim.ch/mf2/2022/12/otewmj + https://tiim.ch/mf2/2022/12/otewmj + Thu, 01 Dec 2022 08:24:00 GMT + +

Liked https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html

+ + + +]]>
+ hey@tiim.ch (Tim Bachmann) + mastodon + moderation + twitter +
+ + https://tiim.ch/mf2/2022/11/ntc1nd + https://tiim.ch/mf2/2022/11/ntc1nd + Wed, 30 Nov 2022 06:35:00 GMT + +

Liked https://werd.io/2022/the-fediverse-and-the-indieweb

+ + + +]]>
+ hey@tiim.ch (Tim Bachmann) + fediverse + indieweb + mastodon +
+ + https://tiim.ch/mf2/2022/11/mji1md + https://tiim.ch/mf2/2022/11/mji1md + Tue, 22 Nov 2022 10:28:00 GMT + +

Liked https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/

+ + + +]]>
+ hey@tiim.ch (Tim Bachmann) + fediverse + mastodon + twitter +
+ + <![CDATA[adding Micropub support • AndreGarzia.com]]> + https://tiim.ch/mf2/2022/11/mjqymd + https://tiim.ch/mf2/2022/11/mjqymd + Mon, 21 Nov 2022 14:25:00 GMT + +

Liked https://andregarzia.com/2022/03/adding-micropub-support.html

+ + + +]]>
+ hey@tiim.ch (Tim Bachmann) + indieweb + micropub +
+ + <![CDATA[Hello Micropub]]> + https://tiim.ch/mf2/2022/11/ntuwnd + https://tiim.ch/mf2/2022/11/ntuwnd + Mon, 21 Nov 2022 13:55:00 GMT + If you can read this entry, my micropub implementation works! You can expect a blog post about it shortly :)

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + indieweb + micropub +
+ + <![CDATA[Test note]]> + https://tiim.ch/mf2/2022/hkhmlv + https://tiim.ch/mf2/2022/hkhmlv + Fri, 11 Nov 2022 14:43:58 GMT + This is a test note +

This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)

+

You can find a list of all my notes here

+ + +]]>
+ hey@tiim.ch (Tim Bachmann) + indieweb + mf2 +
+
+
\ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..123ad639 --- /dev/null +++ b/index.html @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + + Tim Bachmann + + +
+ + +
+
+ +

Hey, I'm Tim 😊

+

I'm a master graduate in computer science at the University of Basel, swim coach and swimmer in Switzerland.

+

🏊‍♂️💻🏋️‍♂️

+
Tiim's Profile
+ + +

Recent activity

+

Getting the Absolute Path of a Remote Directory in Ansible

+ 9/20/2023 +
Getting the Absolute Path of a Remote Directory in Ansible +

There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.

+
+
+ +

Pomo 🍅

+ 8/3/2023 +
+

I created pomo as a way to keep me focused for working on my masters thesis, and at the same time +allowed me to learn the rust programming language.

+
+
+ +

Blogroll and Links

+ 7/31/2023 +
+

List of cool blogs and other links I follow

+
+
+ +
+ + +

View all blog posts + | + View all microposts + | + View all projects

+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/manifest.json b/manifest.json new file mode 100644 index 00000000..94fd8d78 --- /dev/null +++ b/manifest.json @@ -0,0 +1,30 @@ +{ + "short_name": "tiim.ch", + "name": "Tim Bachmann", + "icons": [ + { + "src": "/swim-emoji.png", + "type": "image/png", + "sizes": "240x240" + } + ], + "start_url": "/", + "background_color": "#ffffff", + "display": "browser", + "scope": "/", + "theme_color": "#ffffff", + "shortcuts": [ + { + "name": "Projects", + "short_name": "Projects", + "description": "Projects & Utility Apps\u2699\uFE0F", + "url": "/projects" + }, + { + "name": "Blog", + "short_name": "Blog", + "description": "Blog\uD83D\uDCD6", + "url": "/blog" + } + ] +} diff --git a/mf2.html b/mf2.html new file mode 100644 index 00000000..4454b0bb --- /dev/null +++ b/mf2.html @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + + +
+ + +
+

Notes 📒

+

+ 12/9/2022 +
 title image +

The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once.

+
+
+ +

adding Micropub support • AndreGarzia.com

+ 11/21/2022 +
+

👍 Liked: https://andregarzia.com/2022/03/adding-micropub-support.html

+
+
+ +

Hello Micropub

+ 11/21/2022 +
+

If you can read this entry, my micropub implementation works! You can expect a blog post about it shortly :)

+
+
+ +

Test note

+ 11/11/2022 +
+

This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)

+
+
+ +
+ +
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/mf2/2022/11/mji1md.html b/mf2/2022/11/mji1md.html new file mode 100644 index 00000000..9a6c443d --- /dev/null +++ b/mf2/2022/11/mji1md.html @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + undefined - Tim Bachmann + + +
+ + +
+
+ +
+ +

👍Like:

+ + +

fediverse + mastodon + twitter +

+ +
by
+ published on +
+ + + + + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/mf2/2022/11/mji1md/__data.json b/mf2/2022/11/mji1md/__data.json new file mode 100644 index 00000000..386ad7cc --- /dev/null +++ b/mf2/2022/11/mji1md/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":18},{"html":2,"slug":3,"date":4,"content_tags":5,"like_of":9,"tags":11,"links":-1,"published":12,"type":13,"cover_image":-1,"description":14,"folder":15,"comments":16,"latestComment":17},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/\">https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/mji1md",["Date","2022-11-22T10:28:00.000Z"],[6,7,8],"twitter","fediverse","mastodon",{"url":10},"https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/",[7,8,6],true,"like","👍 Liked: https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/","mf2",[],"2023-09-02T19:26:59Z",{"html":19,"slug":20,"uuid":21,"date":22,"created":23,"published":12,"abstract":24,"tags":25,"links":-1,"type":26,"cover_image":-1,"description":27,"folder":28},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"article","","metadata"],"uses":{"params":["slug"]}}]} diff --git a/mf2/2022/11/mjqymd.html b/mf2/2022/11/mjqymd.html new file mode 100644 index 00000000..73c06c8f --- /dev/null +++ b/mf2/2022/11/mjqymd.html @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + adding Micropub support • AndreGarzia.com - Tim Bachmann + + +
+ + +
+
+ +
+ +

👍Like: adding Micropub support • AndreGarzia.com

+ + +

indieweb + micropub +

+ +
by
+ published on +
+ + + + + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/mf2/2022/11/mjqymd/__data.json b/mf2/2022/11/mjqymd/__data.json new file mode 100644 index 00000000..0580b14c --- /dev/null +++ b/mf2/2022/11/mjqymd/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":18},{"html":2,"slug":3,"title":4,"date":5,"content_tags":6,"like_of":9,"tags":11,"links":-1,"published":12,"type":13,"cover_image":-1,"description":14,"folder":15,"comments":16,"latestComment":17},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://andregarzia.com/2022/03/adding-micropub-support.html\">https://andregarzia.com/2022/03/adding-micropub-support.html\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/mjqymd","adding Micropub support • AndreGarzia.com",["Date","2022-11-21T14:25:00.000Z"],[7,8],"micropub","indieweb",{"url":10},"https://andregarzia.com/2022/03/adding-micropub-support.html",[8,7],true,"like","👍 Liked: https://andregarzia.com/2022/03/adding-micropub-support.html","mf2",[],"2023-09-02T19:26:59Z",{"html":19,"slug":20,"uuid":21,"date":22,"created":23,"published":12,"abstract":24,"tags":25,"links":-1,"type":26,"cover_image":-1,"description":27,"folder":28},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"article","","metadata"],"uses":{"params":["slug"]}}]} diff --git a/mf2/2022/11/ntc1nd.html b/mf2/2022/11/ntc1nd.html new file mode 100644 index 00000000..a58d8796 --- /dev/null +++ b/mf2/2022/11/ntc1nd.html @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + undefined - Tim Bachmann + + +
+ + +
+
+ +
+ +

👍Like:

+ + +

fediverse + indieweb + mastodon +

+ +
by
+ published on +
+ + + + + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/mf2/2022/11/ntc1nd/__data.json b/mf2/2022/11/ntc1nd/__data.json new file mode 100644 index 00000000..856fee46 --- /dev/null +++ b/mf2/2022/11/ntc1nd/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":37},{"html":2,"slug":3,"name":4,"date":5,"content_tags":6,"like_of":10,"raw_data":12,"tags":29,"links":-1,"published":31,"type":32,"cover_image":-1,"description":33,"folder":34,"comments":35,"latestComment":36},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://werd.io/2022/the-fediverse-and-the-indieweb\">https://werd.io/2022/the-fediverse-and-the-indieweb\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/ntc1nd","The fediverse and the indieweb",["Date","2022-11-30T06:35:00.000Z"],[7,8,9],"Indieweb","fediverse","mastodon",{"url":11},"https://werd.io/2022/the-fediverse-and-the-indieweb",{"items":13,"rels":27,"relurls":28},[14],{"id":15,"value":15,"html":15,"type":16,"properties":18,"shape":15,"coords":15,"children":26},"",[17],"h-entry",{"category":19,"like-of":20,"name":21,"post-status":22,"published":24},[7,8,9],[11],[4],[23],"published",[25],"2022-11-30T07:35:00+0100",[],{},{},[8,30,9],"indieweb",true,"like","👍 Liked: https://werd.io/2022/the-fediverse-and-the-indieweb","mf2",[],"2023-09-02T19:26:59Z",{"html":38,"slug":39,"uuid":40,"date":41,"created":42,"published":31,"abstract":43,"tags":44,"links":-1,"type":45,"cover_image":-1,"description":15,"folder":46},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"article","metadata"],"uses":{"params":["slug"]}}]} diff --git a/mf2/2022/11/ntuwnd.html b/mf2/2022/11/ntuwnd.html new file mode 100644 index 00000000..f6d6ba4b --- /dev/null +++ b/mf2/2022/11/ntuwnd.html @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Hello Micropub - Tim Bachmann + + +
+ + +
+
+ +
+ +

📔 Hello Micropub

+ + +

indieweb + micropub +

+ +
by
+ published on +
+ + +

If you can read this entry, my micropub implementation works! You can expect a blog post about it shortly :)

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/mf2/2022/11/ntuwnd/__data.json b/mf2/2022/11/ntuwnd/__data.json new file mode 100644 index 00000000..e6bac602 --- /dev/null +++ b/mf2/2022/11/ntuwnd/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":16},{"html":2,"slug":3,"title":4,"date":5,"content_tags":6,"abstract":2,"tags":9,"links":-1,"published":10,"type":11,"cover_image":-1,"description":12,"folder":13,"comments":14,"latestComment":15},"\u003Cp>If you can read this entry, my micropub implementation works! You can expect a blog post about it shortly :)\u003C/p>","mf2/2022/11/ntuwnd","Hello Micropub",["Date","2022-11-21T13:55:00.000Z"],[7,8],"indieweb","micropub",[7,8],true,"note","","mf2",[],"2023-09-02T19:26:59Z",{"html":17,"slug":18,"uuid":19,"date":20,"created":21,"published":10,"abstract":22,"tags":23,"links":-1,"type":24,"cover_image":-1,"description":12,"folder":25},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"article","metadata"],"uses":{"params":["slug"]}}]} diff --git a/mf2/2022/12/mte4nd.html b/mf2/2022/12/mte4nd.html new file mode 100644 index 00000000..e8c33e0e --- /dev/null +++ b/mf2/2022/12/mte4nd.html @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + undefined - Tim Bachmann + + +
+ + +
+
+ +
+ +

👍Like:

+ + +

caddy + dev + golang +

+ +
by
+ published on +
+ + + + + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/mf2/2022/12/mte4nd/__data.json b/mf2/2022/12/mte4nd/__data.json new file mode 100644 index 00000000..3b8d8b17 --- /dev/null +++ b/mf2/2022/12/mte4nd/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":37},{"html":2,"slug":3,"name":4,"date":5,"content_tags":6,"like_of":10,"raw_data":12,"tags":30,"links":-1,"published":31,"type":32,"cover_image":-1,"description":33,"folder":34,"comments":35,"latestComment":36},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==\">https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/12/mte4nd","How Caddy 2 works, a deep dive into the source",["Date","2022-12-04T13:08:00.000Z"],[7,8,9],"caddy","golang","dev",{"url":11},"https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==",{"items":13,"rels":28,"relurls":29},[14],{"id":15,"value":15,"html":15,"type":16,"properties":18,"shape":15,"coords":15,"children":27},"",[17],"h-entry",{"category":19,"like-of":20,"name":21,"post-status":23,"published":25},[7,8,9],[11],[22]," How Caddy 2 works, a deep dive into the source",[24],"published",[26],"2022-12-04T14:08:00+0100",[],{},{},[7,9,8],true,"like","👍 Liked: https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==","mf2",[],"2023-09-02T19:26:59Z",{"html":38,"slug":39,"uuid":40,"date":41,"created":42,"published":31,"abstract":43,"tags":44,"links":-1,"type":45,"cover_image":-1,"description":15,"folder":46},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"article","metadata"],"uses":{"params":["slug"]}}]} diff --git a/mf2/2022/12/ota4mt.html b/mf2/2022/12/ota4mt.html new file mode 100644 index 00000000..164aa6ce --- /dev/null +++ b/mf2/2022/12/ota4mt.html @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + undefined - Tim Bachmann + + +
+ + +
+
+ +
+
+ +

📔

+ + +

photo + winter +

+ +
by
+ published on +
+ + +

The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once.

+ +
+ + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/mf2/2022/12/ota4mt/__data.json b/mf2/2022/12/ota4mt/__data.json new file mode 100644 index 00000000..c2ef6a2c --- /dev/null +++ b/mf2/2022/12/ota4mt/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":37},{"html":2,"slug":3,"date":4,"content_tags":5,"photos":8,"raw_data":11,"abstract":2,"tags":30,"links":-1,"published":32,"type":33,"cover_image":10,"description":14,"folder":34,"comments":35,"latestComment":36},"\u003Cp>The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once.\u003C/p>","mf2/2022/12/ota4mt",["Date","2022-12-09T10:50:00.000Z"],[6,7],"Photo","winter",[9],{"url":10},"https://media.tiim.ch/47537749-f79a-4603-92a8-42c71d6b96ec.jpg",{"items":12,"rels":28,"relurls":29},[13],{"id":14,"value":14,"html":14,"type":15,"properties":17,"shape":14,"coords":14,"children":27},"",[16],"h-entry",{"category":18,"content":19,"mp-photo-alt":21,"post-status":23,"published":25},[6,7],[20],"The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once. ",[22],"A snowy field near Basel, Switzerland. ",[24],"published",[26],"2022-12-09T11:50:00+0100",[],{},{},[31,7],"photo",true,"note","mf2",[],"2023-09-02T19:26:59Z",{"html":38,"slug":39,"uuid":40,"date":41,"created":42,"published":32,"abstract":43,"tags":44,"links":-1,"type":45,"cover_image":-1,"description":14,"folder":46},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"article","metadata"],"uses":{"params":["slug"]}}]} diff --git a/mf2/2022/12/otewmj.html b/mf2/2022/12/otewmj.html new file mode 100644 index 00000000..3339be67 --- /dev/null +++ b/mf2/2022/12/otewmj.html @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + undefined - Tim Bachmann + + +
+ + +
+
+ +
+ +

👍Like:

+ + +

mastodon + moderation + twitter +

+ +
by
+ published on +
+ + + + + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/mf2/2022/12/otewmj/__data.json b/mf2/2022/12/otewmj/__data.json new file mode 100644 index 00000000..544e1555 --- /dev/null +++ b/mf2/2022/12/otewmj/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":37},{"html":2,"slug":3,"name":4,"date":5,"content_tags":6,"like_of":10,"raw_data":12,"tags":29,"links":-1,"published":31,"type":32,"cover_image":-1,"description":33,"folder":34,"comments":35,"latestComment":36},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html\">https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/12/otewmj","I Was Wrong About Mastodon",["Date","2022-12-01T08:24:00.000Z"],[7,8,9],"Mastodon","twitter","moderation",{"url":11},"https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html",{"items":13,"rels":27,"relurls":28},[14],{"id":15,"value":15,"html":15,"type":16,"properties":18,"shape":15,"coords":15,"children":26},"",[17],"h-entry",{"category":19,"like-of":20,"name":21,"post-status":22,"published":24},[7,8,9],[11],[4],[23],"published",[25],"2022-12-01T09:24:00+0100",[],{},{},[30,9,8],"mastodon",true,"like","👍 Liked: https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html","mf2",[],"2023-09-02T19:26:59Z",{"html":38,"slug":39,"uuid":40,"date":41,"created":42,"published":31,"abstract":43,"tags":44,"links":-1,"type":45,"cover_image":-1,"description":15,"folder":46},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"article","metadata"],"uses":{"params":["slug"]}}]} diff --git a/mf2/2022/hkhmlv.html b/mf2/2022/hkhmlv.html new file mode 100644 index 00000000..969c382a --- /dev/null +++ b/mf2/2022/hkhmlv.html @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Test note - Tim Bachmann + + +
+ + +
+
+ +
+ +

📔 Test note

+ + +

indieweb + mf2 +

+ +
by
+ published on +last updated on
+ + +

This is a test note

+

This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)

+

You can find a list of all my notes here

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/mf2/2022/hkhmlv/__data.json b/mf2/2022/hkhmlv/__data.json new file mode 100644 index 00000000..b2f4337f --- /dev/null +++ b/mf2/2022/hkhmlv/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":20},{"html":2,"slug":3,"uuid":4,"date":5,"created":6,"title":7,"published":8,"modified":9,"content_tags":10,"reply_to":13,"abstract":14,"tags":15,"links":-1,"type":16,"cover_image":-1,"description":17,"folder":11,"comments":18,"latestComment":19},"\u003Ch2>This is a test note\u003C/h2>\n\u003Cp>This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)\u003C/p>\n\u003Cp>You can find a list of all my notes \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">here\u003C/a>\u003C/p>","mf2/2022/hkhmlv","874d0a10-6de8-4816-9b69-5c2fbe782774",["Date","2022-11-11T14:43:58.000Z"],["Date","2022-11-11T14:43:58.000Z"],"Test note",true,["Date","2022-11-11T14:43:58.000Z"],[11,12],"mf2","indieweb","https://webmention.rocks/test/4","\u003Cp>This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)\u003C/p>",[12,11],"note","",[],"2023-09-02T19:26:59Z",{"html":21,"slug":22,"uuid":23,"date":24,"created":25,"published":8,"abstract":26,"tags":27,"links":-1,"type":28,"cover_image":-1,"description":17,"folder":29},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"article","metadata"],"uses":{"params":["slug"]}}]} diff --git a/mf2/2023/07/mteymz.html b/mf2/2023/07/mteymz.html new file mode 100644 index 00000000..d1f3b3ad --- /dev/null +++ b/mf2/2023/07/mteymz.html @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + undefined - Tim Bachmann + + +
+ + +
+
+ +
+ +

🗨️Reply:

+ + +

email + newsletter + rss +

+ +
by
+ published on +
+ + +
+

Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds. +The syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

1 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
  • + +

    Kevin

    + 2023-07-03 11:01:20 AM + + + +
    + +

    I definitely get that email-based feed reading is not for everyone, but I don't think we are that different here. My inbox is indeed reserved for "for things that require my attention". That is why only a very small number of feeds go to my inbox (like WebMention replies to my posts). The vast majority of feeds go into dedicated folders with no notifications. So in a way these folders are my feed reader. The only relation that they have to my "normal email workflow" is that the exist in the same app. When I say I get my feeds via email lots of people immediately picture having feeds show up in their regular email workflow and are (rightfully) mortified. I think this approach would work well for almost no one. I think it is very important to have separation by priority much like Thunderbird has a separate news feed rather than dumping the feeds into your inbox. The difference here is that the separation is different folders in the same IMAP account rather than a distinct news account.

    +
  • +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/mf2/2023/07/mteymz/__data.json b/mf2/2023/07/mteymz/__data.json new file mode 100644 index 00000000..2d957aa9 --- /dev/null +++ b/mf2/2023/07/mteymz/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"post":1,"about":44},{"html":2,"slug":3,"date":4,"content_tags":5,"in_reply_to":9,"raw_data":11,"abstract":29,"tags":30,"links":-1,"published":31,"type":32,"cover_image":-1,"description":33,"folder":34,"comments":35,"latestComment":43},"\u003Cdiv class=\"mf2\">\u003Cp>This post is in reply to \"\u003Ca class=\"u-in-reply-to\" href=\"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\">https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\u003C/a>\"\u003C/p>\u003C/div>\n\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>","mf2/2023/07/mteymz",["Date","2023-07-02T17:58:00.000Z"],[6,7,8],"rss","email","newsletter",{"url":10},"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/",{"items":12,"rels":27,"relurls":28},[13],{"id":14,"value":14,"html":14,"type":15,"properties":17,"shape":14,"coords":14,"children":26},"",[16],"h-entry",{"category":18,"content":19,"in-reply-to":21,"post-status":22,"published":24},[6,7,8],[20],"Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\n\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile. ",[10],[23],"published",[25],"2023-07-02T19:58:00+0200",[],{},{},"\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>",[7,8,6],true,"reply","💬 In reply to: https://kevincox.ca/2023/06/27/decade-of-rss-via-email/","mf2",[36],{"id":37,"type":38,"replyTo":14,"timestamp":39,"page":3,"url":40,"content":41,"name":42},"447820cb-5432-4f54-bd83-94e5674366e6","comment","2023-07-03T11:01:20Z","https://tiim.ch/mf2/2023/07/mteymz#447820cb-5432-4f54-bd83-94e5674366e6","I definitely get that email-based feed reading is not for everyone, but I don't think we are that different here. My inbox is indeed reserved for \"for things that require my attention\". That is why only a very small number of feeds go to my inbox (like WebMention replies to my posts). The vast majority of feeds go into dedicated folders with no notifications. So in a way these folders are my feed reader. The only relation that they have to my \"normal email workflow\" is that the exist in the same app. When I say I get my feeds via email lots of people immediately picture having feeds show up in their regular email workflow and are (rightfully) mortified. I think this approach would work well for almost no one. I think it is very important to have separation by priority much like Thunderbird has a separate news feed rather than dumping the feeds into your inbox. The difference here is that the separation is different folders in the same IMAP account rather than a distinct news account.","Kevin","2023-09-02T19:26:59Z",{"html":45,"slug":46,"uuid":47,"date":48,"created":49,"published":31,"abstract":50,"tags":51,"links":-1,"type":52,"cover_image":-1,"description":14,"folder":53},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"article","metadata"],"uses":{"params":["slug"]}}]} diff --git a/mf2/__data.json b/mf2/__data.json new file mode 100644 index 00000000..7ddfb986 --- /dev/null +++ b/mf2/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"notes":1},[2,45,75,105,134,162,172,184,192],{"html":3,"slug":4,"date":5,"content_tags":6,"in_reply_to":10,"raw_data":12,"abstract":30,"tags":31,"links":-1,"published":32,"type":33,"cover_image":-1,"description":34,"folder":35,"comments":36,"latestComment":44},"\u003Cdiv class=\"mf2\">\u003Cp>This post is in reply to \"\u003Ca class=\"u-in-reply-to\" href=\"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\">https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\u003C/a>\"\u003C/p>\u003C/div>\n\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>","mf2/2023/07/mteymz",["Date","2023-07-02T17:58:00.000Z"],[7,8,9],"rss","email","newsletter",{"url":11},"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/",{"items":13,"rels":28,"relurls":29},[14],{"id":15,"value":15,"html":15,"type":16,"properties":18,"shape":15,"coords":15,"children":27},"",[17],"h-entry",{"category":19,"content":20,"in-reply-to":22,"post-status":23,"published":25},[7,8,9],[21],"Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\n\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile. ",[11],[24],"published",[26],"2023-07-02T19:58:00+0200",[],{},{},"\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>",[8,9,7],true,"reply","💬 In reply to: https://kevincox.ca/2023/06/27/decade-of-rss-via-email/","mf2",[37],{"id":38,"type":39,"replyTo":15,"timestamp":40,"page":4,"url":41,"content":42,"name":43},"447820cb-5432-4f54-bd83-94e5674366e6","comment","2023-07-03T11:01:20Z","https://tiim.ch/mf2/2023/07/mteymz#447820cb-5432-4f54-bd83-94e5674366e6","I definitely get that email-based feed reading is not for everyone, but I don't think we are that different here. My inbox is indeed reserved for \"for things that require my attention\". That is why only a very small number of feeds go to my inbox (like WebMention replies to my posts). The vast majority of feeds go into dedicated folders with no notifications. So in a way these folders are my feed reader. The only relation that they have to my \"normal email workflow\" is that the exist in the same app. When I say I get my feeds via email lots of people immediately picture having feeds show up in their regular email workflow and are (rightfully) mortified. I think this approach would work well for almost no one. I think it is very important to have separation by priority much like Thunderbird has a separate news feed rather than dumping the feeds into your inbox. The difference here is that the separation is different folders in the same IMAP account rather than a distinct news account.","Kevin","2023-09-02T19:26:59Z",{"html":46,"slug":47,"date":48,"content_tags":49,"photos":52,"raw_data":55,"abstract":46,"tags":71,"links":-1,"published":32,"type":73,"cover_image":54,"description":15,"folder":35,"comments":74,"latestComment":44},"\u003Cp>The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once.\u003C/p>","mf2/2022/12/ota4mt",["Date","2022-12-09T10:50:00.000Z"],[50,51],"Photo","winter",[53],{"url":54},"https://media.tiim.ch/47537749-f79a-4603-92a8-42c71d6b96ec.jpg",{"items":56,"rels":69,"relurls":70},[57],{"id":15,"value":15,"html":15,"type":58,"properties":59,"shape":15,"coords":15,"children":68},[17],{"category":60,"content":61,"mp-photo-alt":63,"post-status":65,"published":66},[50,51],[62],"The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once. ",[64],"A snowy field near Basel, Switzerland. ",[24],[67],"2022-12-09T11:50:00+0100",[],{},{},[72,51],"photo","note",[],{"html":76,"slug":77,"name":78,"date":79,"content_tags":80,"like_of":84,"raw_data":86,"tags":101,"links":-1,"published":32,"type":102,"cover_image":-1,"description":103,"folder":35,"comments":104,"latestComment":44},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==\">https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/12/mte4nd","How Caddy 2 works, a deep dive into the source",["Date","2022-12-04T13:08:00.000Z"],[81,82,83],"caddy","golang","dev",{"url":85},"https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==",{"items":87,"rels":99,"relurls":100},[88],{"id":15,"value":15,"html":15,"type":89,"properties":90,"shape":15,"coords":15,"children":98},[17],{"category":91,"like-of":92,"name":93,"post-status":95,"published":96},[81,82,83],[85],[94]," How Caddy 2 works, a deep dive into the source",[24],[97],"2022-12-04T14:08:00+0100",[],{},{},[81,83,82],"like","👍 Liked: https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==",[],{"html":106,"slug":107,"name":108,"date":109,"content_tags":110,"like_of":114,"raw_data":116,"tags":130,"links":-1,"published":32,"type":102,"cover_image":-1,"description":132,"folder":35,"comments":133,"latestComment":44},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html\">https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/12/otewmj","I Was Wrong About Mastodon",["Date","2022-12-01T08:24:00.000Z"],[111,112,113],"Mastodon","twitter","moderation",{"url":115},"https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html",{"items":117,"rels":128,"relurls":129},[118],{"id":15,"value":15,"html":15,"type":119,"properties":120,"shape":15,"coords":15,"children":127},[17],{"category":121,"like-of":122,"name":123,"post-status":124,"published":125},[111,112,113],[115],[108],[24],[126],"2022-12-01T09:24:00+0100",[],{},{},[131,113,112],"mastodon","👍 Liked: https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html",[],{"html":135,"slug":136,"name":137,"date":138,"content_tags":139,"like_of":142,"raw_data":144,"tags":158,"links":-1,"published":32,"type":102,"cover_image":-1,"description":160,"folder":35,"comments":161,"latestComment":44},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://werd.io/2022/the-fediverse-and-the-indieweb\">https://werd.io/2022/the-fediverse-and-the-indieweb\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/ntc1nd","The fediverse and the indieweb",["Date","2022-11-30T06:35:00.000Z"],[140,141,131],"Indieweb","fediverse",{"url":143},"https://werd.io/2022/the-fediverse-and-the-indieweb",{"items":145,"rels":156,"relurls":157},[146],{"id":15,"value":15,"html":15,"type":147,"properties":148,"shape":15,"coords":15,"children":155},[17],{"category":149,"like-of":150,"name":151,"post-status":152,"published":153},[140,141,131],[143],[137],[24],[154],"2022-11-30T07:35:00+0100",[],{},{},[141,159,131],"indieweb","👍 Liked: https://werd.io/2022/the-fediverse-and-the-indieweb",[],{"html":163,"slug":164,"date":165,"content_tags":166,"like_of":167,"tags":169,"links":-1,"published":32,"type":102,"cover_image":-1,"description":170,"folder":35,"comments":171,"latestComment":44},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/\">https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/mji1md",["Date","2022-11-22T10:28:00.000Z"],[112,141,131],{"url":168},"https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/",[141,131,112],"👍 Liked: https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/",[],{"html":173,"slug":174,"title":175,"date":176,"content_tags":177,"like_of":179,"tags":181,"links":-1,"published":32,"type":102,"cover_image":-1,"description":182,"folder":35,"comments":183,"latestComment":44},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://andregarzia.com/2022/03/adding-micropub-support.html\">https://andregarzia.com/2022/03/adding-micropub-support.html\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/mjqymd","adding Micropub support • AndreGarzia.com",["Date","2022-11-21T14:25:00.000Z"],[178,159],"micropub",{"url":180},"https://andregarzia.com/2022/03/adding-micropub-support.html",[159,178],"👍 Liked: https://andregarzia.com/2022/03/adding-micropub-support.html",[],{"html":185,"slug":186,"title":187,"date":188,"content_tags":189,"abstract":185,"tags":190,"links":-1,"published":32,"type":73,"cover_image":-1,"description":15,"folder":35,"comments":191,"latestComment":44},"\u003Cp>If you can read this entry, my micropub implementation works! You can expect a blog post about it shortly :)\u003C/p>","mf2/2022/11/ntuwnd","Hello Micropub",["Date","2022-11-21T13:55:00.000Z"],[159,178],[159,178],[],{"html":193,"slug":194,"uuid":195,"date":196,"created":197,"title":198,"published":32,"modified":199,"content_tags":200,"reply_to":201,"abstract":202,"tags":203,"links":-1,"type":73,"cover_image":-1,"description":15,"folder":35,"comments":204,"latestComment":44},"\u003Ch2>This is a test note\u003C/h2>\n\u003Cp>This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)\u003C/p>\n\u003Cp>You can find a list of all my notes \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">here\u003C/a>\u003C/p>","mf2/2022/hkhmlv","874d0a10-6de8-4816-9b69-5c2fbe782774",["Date","2022-11-11T14:43:58.000Z"],["Date","2022-11-11T14:43:58.000Z"],"Test note",["Date","2022-11-11T14:43:58.000Z"],[35,159],"https://webmention.rocks/test/4","\u003Cp>This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)\u003C/p>",[159,35],[]],"uses":{}}]} diff --git a/pages/links.html b/pages/links.html new file mode 100644 index 00000000..663b278d --- /dev/null +++ b/pages/links.html @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Blogroll and Links - Tim Bachmann + + +
+ + +
+
+ +
+ +

Blogroll and Links

+ + +

+ +
by
+ published on +
+ + +

I am planning to publish the list of blogs I subscribe to here in the future. But I still have not figured out how +to automatically export the list of feeds from Mozilla Thunderbird and import it here. Until then, this page consists of +a manual list of pages that I like.

+

Links

+
    +
  • Ye Olde Blogroll, a human curated list of personal blogs.
  • +
  • uses.tech, a list of personal websites that have a /uses page.
  • +
  • changelog.com/news, curated developer news as a podcast and as a newsletter. Unfortunately not available a RSS subscribable blog post.
  • +
+

You have some suggestions for other cool or useful websites or blogs? Let me know.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/pages/links/__data.json b/pages/links/__data.json new file mode 100644 index 00000000..73e5a75a --- /dev/null +++ b/pages/links/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"page":1,"about":16},{"html":2,"slug":3,"uuid":4,"title":5,"published":6,"description":7,"date":8,"modified":9,"cover_image":-1,"abstract":10,"tags":11,"links":-1,"type":12,"folder":13,"comments":14,"latestComment":15},"\u003Cp>I am planning to publish the list of blogs I subscribe to here in the future. But I still have not figured out how\nto automatically export the list of feeds from Mozilla Thunderbird and import it here. Until then, this page consists of\na manual list of pages that I like.\u003C/p>\n\u003Ch2>Links\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://blogroll.org/\" rel=\"nofollow noopener noreferrer\">Ye Olde Blogroll\u003C/a>, a human curated list of personal blogs.\u003C/li>\n\u003Cli>\u003Ca href=\"https://uses.tech/\" rel=\"nofollow noopener noreferrer\">uses.tech\u003C/a>, a list of personal websites that have a \u003Ca href=\"https://tiim.ch/uses\" rel=\"nofollow noopener noreferrer\">\u003Ccode>/uses\u003C/code>\u003C/a> page.\u003C/li>\n\u003Cli>\u003Ca href=\"https://changelog.com/news\" rel=\"nofollow noopener noreferrer\">changelog.com/news\u003C/a>, curated developer news as a podcast and as a newsletter. Unfortunately not available a RSS subscribable blog post.\u003C/li>\n\u003C/ul>\n\u003Cp>\u003Cem>You have some suggestions for other cool or useful websites or blogs? \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">Let me know\u003C/a>.\u003C/em>\u003C/p>","pages/links","5d86f95b-751c-416d-bd24-e35401317494","Blogroll and Links",true,"List of cool blogs and other links I follow",["Date","2023-07-31T19:06:29.000Z"],null,"\u003Cp>I am planning to publish the list of blogs I subscribe to here in the future. But I still have not figured out how\nto automatically export the list of feeds from Mozilla Thunderbird and import it here. Until then, this page consists of\na manual list of pages that I like.\u003C/p>",[],"article","pages",[],"2023-09-02T19:26:59Z",{"html":17,"slug":18,"uuid":19,"date":20,"created":21,"published":6,"abstract":22,"tags":23,"links":-1,"type":12,"cover_image":-1,"description":24,"folder":25},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["page_slug"]}}]} diff --git a/pages/uses.html b/pages/uses.html new file mode 100644 index 00000000..690843f9 --- /dev/null +++ b/pages/uses.html @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + /uses - Tim Bachmann + + +
+ + +
+
+ +
+ +

/uses

+ + +

+ +
by
+ published on +last updated on
+ + +

This page lists most of the gear and tools I use on a daily basis. This page is inspired by @wesbos' uses.tech page.

+

Software

+

Editor and Terminal

+
    +
  • NeoVim, +I have recently switched to neovim from vscode. I planned to just give vim a spin for a few weeks, but somehow I don't seem to be able to go back 🤔.
  • +
  • I use the Nightfox.nvim theme. Before switching to neovim, I used the excelent Beyond the Horizon Dark VSCode Theme, which is based on the Horizon Dark colour scheme, which I kind of miss. This website uses this colour scheme too.
  • +
  • My favourite monospace font is currently Fira Code, which I use in the terminal, and therefore in the editor as well.
  • +
  • I use the Windows Terminal as my main terminal. I like it because it lets me easily switch between WSL2 and Powershell or CMD, it supports custom font, and it just looks really nice.
  • +
+

Note Taking

+
    +
  • Obsidian.md is the first note taking tool I actually use on a regular basis. I really enjoy the flexibility of it, especially regarding the quickly growing plugin ecosystem. A few of my favourite plugins are in no particular order: + +
  • +
+

Other Apps

+
    +
  • Firefox has been my default browser for many years now. I switched back to it after using chrome because I agree with its focus on privacy.
  • +
  • Mozilla Thunderbird, I used to grudgingly use thunderbird as my email program because I have not found anything better yet. However, in the recent months the changes to thunderbird make it way more enjoyable to use. I still miss a way to sync my settings and RSS feeds between my laptop and my desktop computer though.
  • +
  • I heavily rely on FancyZones of Windows PowerToys to arrange my windows in a tiled-ish way.
  • +
  • Microsoft To Do, I have recently been looking for a good and simple program to keep all my tasks. Microsoft To Do seems to fit this quite well and looks stunning.
  • +
  • Trello, just the easiest way to keep track of projects that consist of multiple tasks. I have a trello board for most of my side projects, as well as for private projects.
  • +
+

Tech Stack

+
    +
  • Svelte / Svelte kit is currently my favourite frontend web framework for highly dynamic content. I previously used Vue.js and React but I like the pragmatic way of svelte the most so far.
  • +
  • Hasura makes building web applications a breeze. It saves me from reimplementing the same fundamental things such as the GraphQL API, authorisation and admin dashboard for every project.
  • +
  • Docker in combination with Traefik makes hosting multiple applications on the same server a breeze.
  • +
  • For future projects that have a server side component, I am strongly considering using htmx for the frontend.
  • +
+

Gear

+

PC Gear

+
    +
  • Tascam TM-80 is the XLR microphone I am currently using. I got it quite recently after I got fed up with my ugly USB microphone.
  • +
  • As an audio interface I use the minimalist Audient evo 4.
  • +
  • Logitech c920 is my reliable webcam. I got it a year before the pandemic (thankfully!) and I am quite happy with it.
  • +
  • At work I use the Ultimate Hacking Keyboard This is my first real mechanical keyboard. I really like the split keyboard layout which makes typing much more ergonomic in my opinion. I use MX Brown switches.
  • +
  • At home I recently got the [Keychron K8 Pro] keyboard with Gateron G Pro Brown switches. I really love the sound and the feel of this keyboard.
  • +
  • The Logitech MX Master 3 mouse is the newest addition to my gear, and the first wireless mouse I own. I was sceptical about the battery duration at first but it really is not an issue for me.
  • +
  • Wacom Intuos Pen & Touch small was the drawing tablet I purchased a long time ago when I tried to get into drawing. Nowadays I use it mostly for writing on virtual whiteboards.
  • +
+

Multimedia Gear

+
    +
  • I take pictures with my trusty Panasonic Lumix G7. I really love this camera, it is still very good for the price and even log lenses are not bulky at all. Check out some of my pictures on my Flickr.
  • +
+
+

This page is a work in progress, if your are curious about anything else in my setup shoot me a toot @tiim@indieweb.social, or comment down below.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/pages/uses/__data.json b/pages/uses/__data.json new file mode 100644 index 00000000..6f30fa5e --- /dev/null +++ b/pages/uses/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"page":1,"about":17},{"html":2,"slug":3,"uuid":4,"title":5,"published":6,"created":7,"date":8,"description":9,"modified":10,"abstract":11,"tags":12,"links":-1,"type":13,"cover_image":-1,"folder":14,"comments":15,"latestComment":16},"\u003Cp>This page lists most of the gear and tools I use on a daily basis. This page is inspired by \u003Ca href=\"https://wesbos.com\" rel=\"nofollow noopener noreferrer\">@wesbos\u003C/a>' \u003Ca href=\"https://uses.tech/\" rel=\"nofollow noopener noreferrer\">uses.tech\u003C/a> page.\u003C/p>\n\u003Ch2>Software\u003C/h2>\n\u003Ch3>Editor and Terminal\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://neovim.io/\" rel=\"nofollow noopener noreferrer\">NeoVim\u003C/a>,\nI have recently switched to neovim from vscode. I planned to just give vim a spin for a few weeks, but somehow I don't seem to be able to go back 🤔.\u003C/li>\n\u003Cli>I use the \u003Ca href=\"https://github.com/EdenEast/nightfox.nvim\" rel=\"nofollow noopener noreferrer\">Nightfox.nvim\u003C/a> theme. Before switching to neovim, I used the excelent \u003Ca href=\"https://marketplace.visualstudio.com/items?itemName=shaneyu.beyond-the-horizon-theme-vscode\" rel=\"nofollow noopener noreferrer\">Beyond the Horizon Dark VSCode Theme\u003C/a>, which is based on the \u003Ca href=\"https://horizontheme.netlify.app/\" rel=\"nofollow noopener noreferrer\">Horizon Dark\u003C/a> colour scheme, which I kind of miss. This website uses this colour scheme too.\u003C/li>\n\u003Cli>My favourite monospace font is currently \u003Ca href=\"https://github.com/tonsky/FiraCode\" rel=\"nofollow noopener noreferrer\">Fira Code\u003C/a>, which I use in the terminal, and therefore in the editor as well.\u003C/li>\n\u003Cli>I use the \u003Ca href=\"https://github.com/microsoft/terminal\" rel=\"nofollow noopener noreferrer\">Windows Terminal\u003C/a> as my main terminal. I like it because it lets me easily switch between WSL2 and Powershell or CMD, it supports custom font, and it just looks really nice.\u003C/li>\n\u003C/ul>\n\u003Ch3>Note Taking\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://obsidian.md/\" rel=\"nofollow noopener noreferrer\">Obsidian.md\u003C/a> is the first note taking tool I actually use on a regular basis. I really enjoy the flexibility of it, especially regarding the quickly growing plugin ecosystem. A few of my favourite plugins are in no particular order:\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://silentvoid13.github.io/Templater/\" rel=\"nofollow noopener noreferrer\">Obsidian Templater\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://blacksmithgu.github.io/obsidian-dataview/\" rel=\"nofollow noopener noreferrer\">Obsidian Dataview\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://github.com/denolehov/obsidian-git\" rel=\"nofollow noopener noreferrer\">Obsidian Git\u003C/a>\u003C/li>\n\u003C/ul>\n\u003C/li>\n\u003C/ul>\n\u003Ch3>Other Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://www.mozilla.org/de/firefox/new/\" rel=\"nofollow noopener noreferrer\">Firefox\u003C/a> has been my default browser for many years now. I switched back to it after using chrome because I agree with its focus on privacy.\u003C/li>\n\u003Cli>Mozilla Thunderbird, I used to grudgingly use thunderbird as my email program because I have not found anything better yet. However, in the recent months the changes to thunderbird make it way more enjoyable to use. I still miss a way to sync my settings and RSS feeds between my laptop and my desktop computer though.\u003C/li>\n\u003Cli>I heavily rely on \u003Ca href=\"https://docs.microsoft.com/en-us/windows/powertoys/fancyzones\" rel=\"nofollow noopener noreferrer\">FancyZones\u003C/a> of Windows PowerToys to arrange my windows in a tiled-ish way.\u003C/li>\n\u003Cli>Microsoft To Do, I have recently been looking for a good and simple program to keep all my tasks. Microsoft To Do seems to fit this quite well and looks stunning.\u003C/li>\n\u003Cli>Trello, just the easiest way to keep track of projects that consist of multiple tasks. I have a trello board for most of my side projects, as well as for private projects.\u003C/li>\n\u003C/ul>\n\u003Ch2>Tech Stack\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://kit.svelte.dev/\" rel=\"nofollow noopener noreferrer\">Svelte / Svelte kit\u003C/a> is currently my favourite frontend web framework for highly dynamic content. I previously used \u003Ca href=\"https://vuejs.org/\" rel=\"nofollow noopener noreferrer\">Vue.js\u003C/a> and \u003Ca href=\"https://reactjs.org/\" rel=\"nofollow noopener noreferrer\">React\u003C/a> but I like the pragmatic way of svelte the most so far.\u003C/li>\n\u003Cli>\u003Ca href=\"https://hasura.io/\" rel=\"nofollow noopener noreferrer\">Hasura\u003C/a> makes building web applications a breeze. It saves me from reimplementing the same fundamental things such as the GraphQL API, authorisation and admin dashboard for every project.\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.docker.com/\" rel=\"nofollow noopener noreferrer\">Docker\u003C/a> in combination with \u003Ca href=\"https://doc.traefik.io/traefik/\" rel=\"nofollow noopener noreferrer\">Traefik\u003C/a> makes hosting multiple applications on the same server a breeze.\u003C/li>\n\u003Cli>For future projects that have a server side component, I am strongly considering using \u003Ca href=\"https://htmx.org/\" rel=\"nofollow noopener noreferrer\">htmx\u003C/a> for the frontend.\u003C/li>\n\u003C/ul>\n\u003Ch2>Gear\u003C/h2>\n\u003Ch3>PC Gear\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://www.tascam.eu/en/tm-80\" rel=\"nofollow noopener noreferrer\">Tascam TM-80\u003C/a> is the XLR microphone I am currently using. I got it quite recently after I got fed up with my ugly USB microphone.\u003C/li>\n\u003Cli>As an audio interface I use the minimalist \u003Ca href=\"https://evo.audio/products/audio-interfaces/evo-4/overview/\" rel=\"nofollow noopener noreferrer\">Audient evo 4\u003C/a>.\u003C/li>\n\u003Cli>Logitech c920 is my reliable webcam. I got it a year before the pandemic (thankfully!) and I am quite happy with it.\u003C/li>\n\u003Cli>At work I use the \u003Ca href=\"https://ultimatehackingkeyboard.com/\" rel=\"nofollow noopener noreferrer\">Ultimate Hacking Keyboard\u003C/a> This is my first real mechanical keyboard. I really like the split keyboard layout which makes typing much more ergonomic in my opinion. I use MX Brown switches.\u003C/li>\n\u003Cli>At home I recently got the [Keychron K8 Pro] keyboard with Gateron G Pro Brown switches. I really love the sound and the feel of this keyboard.\u003C/li>\n\u003Cli>The Logitech MX Master 3 mouse is the newest addition to my gear, and the first wireless mouse I own. I was sceptical about the battery duration at first but it really is not an issue for me.\u003C/li>\n\u003Cli>Wacom Intuos Pen & Touch small was the drawing tablet I purchased a long time ago when I tried to get into drawing. Nowadays I use it mostly for writing on virtual whiteboards.\u003C/li>\n\u003C/ul>\n\u003Ch3>Multimedia Gear\u003C/h3>\n\u003Cul>\n\u003Cli>I take pictures with my trusty \u003Cstrong>Panasonic Lumix G7\u003C/strong>. I really love this camera, it is still very good for the price and even log lenses are not bulky at all. Check out some of my pictures on my \u003Ca href=\"https://www.flickr.com/people/152309161@N02/\" rel=\"nofollow noopener noreferrer\">Flickr\u003C/a>.\u003C/li>\n\u003C/ul>\n\u003Chr>\n\u003Cp>\u003Cem>This page is a work in progress, if your are curious about anything else in my setup shoot me a toot \u003Ca href=\"https://indieweb.social/@tiim\" rel=\"nofollow noopener noreferrer\">@tiim@indieweb.social\u003C/a>, or comment down below.\u003C/em>\u003C/p>","pages/uses","c00df133-65ab-40c1-9fa9-376fe6dfae93","/uses",true,"2022-03-25T23:28:00.000Z",["Date","2022-03-26T00:00:00.000Z"],"List of software and gear I use on a regular basis.",["Date","2023-07-30T19:21:29.000Z"],"\u003Cp>This page lists most of the gear and tools I use on a daily basis. This page is inspired by \u003Ca href=\"https://wesbos.com\">@wesbos\u003C/a>' \u003Ca href=\"https://uses.tech/\">uses.tech\u003C/a> page.\u003C/p>",[],"article","pages",[],"2023-09-02T19:26:59Z",{"html":18,"slug":19,"uuid":20,"date":21,"created":22,"published":6,"abstract":23,"tags":24,"links":-1,"type":13,"cover_image":-1,"description":25,"folder":26},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"","metadata"],"uses":{"params":["page_slug"]}}]} diff --git a/projects.html b/projects.html new file mode 100644 index 00000000..55a6c0eb --- /dev/null +++ b/projects.html @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + Projects and Apps + + +
+ + +
+
+ +

Projects, Utility Apps & other Resources⚙️

+

This is a list of some of my current or past projects, as well as some + useful resources that I compiled. Go check them out! +

+

Projects

+

Pomo 🍅

+ 8/3/2023 +
+

I created pomo as a way to keep me focused for working on my masters thesis, and at the same time +allowed me to learn the rust programming language.

+
+
+ +

Swim Club Birsfelden Website

+ 3/5/2022 +
+

The website of the "ScBirs" swim club. This website serves as the center of all information distribution for the swimclub.

+
+
+ +

TeamKit

+ 11/27/2022 +
+

TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.

+
+
+ +
+ + +
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/3d-scanning-wiki.html b/projects/3d-scanning-wiki.html new file mode 100644 index 00000000..49575c28 --- /dev/null +++ b/projects/3d-scanning-wiki.html @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + The 3D Scanning Wiki - Tim Bachmann + + +
+ + +
+
+ +
+ +

The 3D Scanning Wiki

+ + +

dev + markdown + sveltekit +

+ +
by
+ published on +last updated on
+ + +

A github hosed wiki for all things 3D scanning: photogrammetry, lidar, laser scanning and more. The page is a static website built from a github repository with markdown files.

+
+Deprecated

The 3D scanning wiki is now offline. But all pages are still available on github.

+
+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/3d-scanning-wiki/__data.json b/projects/3d-scanning-wiki/__data.json new file mode 100644 index 00000000..7c1a8e30 --- /dev/null +++ b/projects/3d-scanning-wiki/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":24},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":14,"abstract":17,"tags":18,"type":19,"cover_image":-1,"description":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>A github hosed wiki for all things 3D scanning: \u003Ca href=\"https://3dscanning.wiki/Photogrammetry\" rel=\"nofollow noopener noreferrer\">photogrammetry\u003C/a>, \u003Ca href=\"https://3dscanning.wiki/Lidar\" rel=\"nofollow noopener noreferrer\">lidar\u003C/a>, laser scanning and more. The page is a static website built from a github repository with markdown files.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Deprecated\u003C/span>\u003Cp>The 3D scanning wiki is now offline. But all pages are still available on github.\u003C/p>\n\u003C/blockquote>","projects/3d-scanning-wiki","fd95701b-6d38-4ff0-85a1-d1f919bf9251","The 3D Scanning Wiki",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-11-21T12:45:23.000Z"],"Projects",true,[11,12,13],"sveltekit","markdown","dev",[15,16],"\u003Cp>\u003Ca href=\"https://3dscanning.wiki/\" rel=\"nofollow noopener noreferrer\">3D Scanning Wiki\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://github.com/3dscanningwiki\" rel=\"nofollow noopener noreferrer\">Github Organisation\u003C/a>\u003C/p>","\u003Cp>A github hosed wiki for all things 3D scanning: \u003Ca href=\"https://3dscanning.wiki/Photogrammetry\">photogrammetry\u003C/a>, \u003Ca href=\"https://3dscanning.wiki/Lidar\">lidar\u003C/a>, laser scanning and more. The page is a static website built from a github repository with markdown files.\u003C/p>",[13,12,11],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":25,"slug":26,"uuid":27,"date":28,"created":29,"published":9,"abstract":30,"tags":31,"links":-1,"type":19,"cover_image":-1,"description":20,"folder":32},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/3d-scans.html b/projects/3d-scans.html new file mode 100644 index 00000000..ad7c0ea0 --- /dev/null +++ b/projects/3d-scans.html @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 3D Scanned and Modeled Objects - Tim Bachmann + + +
+ + +
+
+ +
+ +

3D Scanned and Modeled Objects

+ + +

3d + 3d-model + 3d-scanning +

+ +
by
+ published on +last updated on
+ + +

I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.

+

Example of a 3D scanned model: A pile of clothes on the floor

+

For 3D modeling I use the open source tool Blender and for 3D scanning my software of choice is Reality Capture.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/3d-scans/__data.json b/projects/3d-scans/__data.json new file mode 100644 index 00000000..562abbe2 --- /dev/null +++ b/projects/3d-scans/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":27},{"html":2,"slug":3,"uuid":4,"title":5,"section":6,"date":7,"modified":8,"published":9,"content_tags":10,"links":14,"abstract":17,"tags":18,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":25,"latestComment":26},"\u003Cp>I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.\u003C/p>\n\u003Cp>\u003Cimg src=\"https://img2.cgtrader.com/items/3180107/fd6e03d4a5/pile-of-clothes-on-the-ground-3d-scanned-3d-model-low-poly-obj-fbx-blend-fbm.jpg\" alt=\"Example of a 3D scanned model: A pile of clothes on the floor\">\u003C/p>\n\u003Cp>For 3D modeling I use the open source tool \u003Ca href=\"https://www.blender.org/\" rel=\"nofollow noopener noreferrer\">Blender\u003C/a> and for 3D scanning my software of choice is \u003Ca href=\"https://www.capturingreality.com/\" rel=\"nofollow noopener noreferrer\">Reality Capture\u003C/a>.\u003C/p>","projects/3d-scans","01128b77-eace-4b25-a5b9-9cf36ea32b6a","3D Scanned and Modeled Objects","Projects",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],true,[11,12,13],"3D Scanning","3D model","3D",[15,16],"\u003Cp>\u003Ca href=\"https://www.cgtrader.com/tiim\" rel=\"nofollow noopener noreferrer\">CGTrader Profile\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://www.cgtrader.com/3d-models?author=tiim\" rel=\"nofollow noopener noreferrer\">All 3D Models\u003C/a>\u003C/p>","\u003Cp>I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.\u003C/p>",[19,20,21],"3d","3d-model","3d-scanning","article","","projects",[],"2023-09-02T19:26:59Z",{"html":28,"slug":29,"uuid":30,"date":31,"created":32,"published":9,"abstract":33,"tags":34,"links":-1,"type":22,"cover_image":-1,"description":23,"folder":35},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/__data.json b/projects/__data.json new file mode 100644 index 00000000..449430e1 --- /dev/null +++ b/projects/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"sections":1},[2],{"section":3,"values":4},"Projects",[5,27,47,68,87,103,117,134,148,164,178,192,205],{"html":6,"slug":7,"uuid":8,"title":9,"date":10,"modified":11,"section":3,"published":12,"content_tags":13,"links":17,"abstract":20,"tags":21,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":25,"latestComment":26},"\u003Cp>A github hosed wiki for all things 3D scanning: \u003Ca href=\"https://3dscanning.wiki/Photogrammetry\" rel=\"nofollow noopener noreferrer\">photogrammetry\u003C/a>, \u003Ca href=\"https://3dscanning.wiki/Lidar\" rel=\"nofollow noopener noreferrer\">lidar\u003C/a>, laser scanning and more. The page is a static website built from a github repository with markdown files.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Deprecated\u003C/span>\u003Cp>The 3D scanning wiki is now offline. But all pages are still available on github.\u003C/p>\n\u003C/blockquote>","projects/3d-scanning-wiki","fd95701b-6d38-4ff0-85a1-d1f919bf9251","The 3D Scanning Wiki",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-11-21T12:45:23.000Z"],true,[14,15,16],"sveltekit","markdown","dev",[18,19],"\u003Cp>\u003Ca href=\"https://3dscanning.wiki/\" rel=\"nofollow noopener noreferrer\">3D Scanning Wiki\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://github.com/3dscanningwiki\" rel=\"nofollow noopener noreferrer\">Github Organisation\u003C/a>\u003C/p>","\u003Cp>A github hosed wiki for all things 3D scanning: \u003Ca href=\"https://3dscanning.wiki/Photogrammetry\">photogrammetry\u003C/a>, \u003Ca href=\"https://3dscanning.wiki/Lidar\">lidar\u003C/a>, laser scanning and more. The page is a static website built from a github repository with markdown files.\u003C/p>",[16,15,14],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":28,"slug":29,"uuid":30,"title":31,"section":3,"date":32,"modified":33,"published":12,"content_tags":34,"links":38,"abstract":41,"tags":42,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":46,"latestComment":26},"\u003Cp>I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.\u003C/p>\n\u003Cp>\u003Cimg src=\"https://img2.cgtrader.com/items/3180107/fd6e03d4a5/pile-of-clothes-on-the-ground-3d-scanned-3d-model-low-poly-obj-fbx-blend-fbm.jpg\" alt=\"Example of a 3D scanned model: A pile of clothes on the floor\">\u003C/p>\n\u003Cp>For 3D modeling I use the open source tool \u003Ca href=\"https://www.blender.org/\" rel=\"nofollow noopener noreferrer\">Blender\u003C/a> and for 3D scanning my software of choice is \u003Ca href=\"https://www.capturingreality.com/\" rel=\"nofollow noopener noreferrer\">Reality Capture\u003C/a>.\u003C/p>","projects/3d-scans","01128b77-eace-4b25-a5b9-9cf36ea32b6a","3D Scanned and Modeled Objects",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[35,36,37],"3D Scanning","3D model","3D",[39,40],"\u003Cp>\u003Ca href=\"https://www.cgtrader.com/tiim\" rel=\"nofollow noopener noreferrer\">CGTrader Profile\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://www.cgtrader.com/3d-models?author=tiim\" rel=\"nofollow noopener noreferrer\">All 3D Models\u003C/a>\u003C/p>","\u003Cp>I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.\u003C/p>",[43,44,45],"3d","3d-model","3d-scanning",[],{"html":48,"slug":49,"uuid":50,"title":51,"date":52,"modified":53,"section":3,"published":12,"content_tags":54,"links":61,"abstract":65,"tags":66,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":67,"latestComment":26},"\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\" rel=\"nofollow noopener noreferrer\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>\n\u003Cp>The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents.\nThe admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/aqualetics-coach-screenshot.png\" alt=\"Screenshot of the coaches view\">\u003C/p>\n\u003Cp>The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.\u003C/p>","projects/aqualetics-coach","3210d289-1f9e-41b5-b1f9-d20f00f6a0c5","Aqualetics Coach",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[55,56,57,58,59,60,16],"node","vue","graphql","hasura","postgresql","docker",[62,63,64],"\u003Cp>\u003Ca href=\"https://sundrbi.ch/coach-application/\" rel=\"nofollow noopener noreferrer\">Overview\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch/2019/09/15/schwimmcoach-applikation-innovation/\" rel=\"nofollow noopener noreferrer\">Blog Post 🇩🇪\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch\" rel=\"nofollow noopener noreferrer\">Aqualetics Swim School\u003C/a>\u003C/p>","\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>",[16,60,57,58,55,59,56],[],{"html":69,"slug":70,"uuid":71,"title":72,"date":73,"modified":74,"section":3,"published":12,"cover_image":75,"content_tags":76,"links":81,"abstract":84,"tags":85,"type":22,"description":23,"folder":24,"comments":86,"latestComment":26},"\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>\n\u003Cul>\n\u003Cli>The basic commenting system\u003C/li>\n\u003Cli>Sending and receiving webmentions\u003C/li>\n\u003Cli>Micropub server implementation\u003C/li>\n\u003Cli>IndieAuth (decentralized authentication standard based on OAuth)\u003C/li>\n\u003Cli>Admin dashboard\u003C/li>\n\u003Cli>Admin backup endpoint\u003C/li>\n\u003C/ul>\n\u003Cp>Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and\ncomments through the fediverse show up back on my website.\u003C/p>\n\u003Cp>The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core\nof the application is only concerned with initializing those plugins.\u003C/p>\n\u003Cp>If you have any questions, or want to run IndieGo yourself, don't hesitate to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>.\u003C/p>","projects/indiego","0cf125b3-a99a-4996-8f84-ec5105d64c57","IndieGo",["Date","2023-08-02T08:39:00.000Z"],null,"/assets/2022-07-first-go-project-commenting-api.png",[77,78,79,60,80,16],"go","golang","indieweb","sqlite",[82,83],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">IndieGo Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://comments.tiim.ch\" rel=\"nofollow noopener noreferrer\">Admin Interface\u003C/a> - authentication required\u003C/p>","\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>",[16,60,77,78,79,80],[],{"html":88,"slug":89,"uuid":90,"title":91,"date":92,"modified":93,"section":3,"published":12,"content_tags":94,"links":97,"abstract":100,"tags":101,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":102,"latestComment":26},"\u003Cp>A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.\u003C/p>\n\u003Cp>I built this little page to help me count laps for my swimmers for some time based test sets. The first version of the app just had a grid of static buttons, one for each athlete. I quickly found that it is very hard to keep track which laps I already counted. As a way to visualise when each button was last pressed, they change colour. The buttons start green when pressed and slowly turn red over time, based on the average duration of a lap.\u003C/p>\n\u003Cp>The page works completely offline (after the page loads) and no data is sent to any server. It is also possible to export the data to a csv file.\u003C/p>","projects/lap-counter","871ebd58-0543-4d3a-9f3d-16cd85da9bf9","Lap Counter",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[95,96,16],"svelte","swim",[98,99],"\u003Cp>\u003Ca href=\"https://tiim.ch/lap-counter-js/\" rel=\"nofollow noopener noreferrer\">Open Lap Counter\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/laps-counter-8f0sb\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.\u003C/p>",[16,95,96],[],{"html":104,"slug":105,"uuid":106,"title":107,"date":108,"modified":109,"section":3,"published":12,"content_tags":110,"links":112,"abstract":114,"tags":115,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":116,"latestComment":26},"\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\" rel=\"nofollow noopener noreferrer\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/lenex-splits-sheet-creator.png\" alt=\"Screenshot of a split sheet\">\u003C/p>\n\u003Cp>The split sheet creator is a quick and easy way to create a split sheet from your Lenex sign-up file.\nThe split sheet creator does not send your Lenex file to any servers. The file is opened directly in your browser and never leaves your computer!\u003C/p>\n\u003Cp>I built this web app using my \u003Ca href=\"https://www.npmjs.com/package/js-lenex\" rel=\"nofollow noopener noreferrer\">lenex javascript library\u003C/a>.\u003C/p>\n\u003Ch2>What is a split sheet?\u003C/h2>\n\u003Cp>Swim coaches often write down the times of athletes after some fractions of a race (splits). Usually those splits are recorded every lap or every second lap.\u003C/p>","projects/lenex-split-sheet","a03b6eef-ffbe-428d-a559-42bd361ab88c","Lenex Splits Sheet Creator",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[96,111,95,16],"lenex",[113],"\u003Cp>\u003Ca href=\"https://tiim.ch/lenex-splits-sheet-creator/\" rel=\"nofollow noopener noreferrer\">Open Splits Sheet Creator\u003C/a>\u003C/p>","\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>",[16,111,95,96],[],{"html":118,"slug":119,"uuid":120,"title":121,"date":122,"modified":74,"section":3,"published":12,"content_tags":123,"links":128,"abstract":131,"tags":132,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":133,"latestComment":26},"\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>\n\u003Cp>Android widgets are handled by the operating system and only support a limited set of features for rendering.\nTo display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.\u003C/p>","projects/markdown-widget","c6a11779-cf98-4983-8744-9b1effae8d7a","Android Markdown Widget",["Date","2023-08-02T08:59:00.000Z"],[124,125,126,15,127,16],"android","java","kotlin","widget",[129,130],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Android-Markdown-Widget\" rel=\"nofollow noopener noreferrer\">Android Markdown Widget Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://f-droid.org/packages/ch.tiim.markdown_widget/\" rel=\"nofollow noopener noreferrer\">Download on F-Droid\u003C/a>\u003C/p>","\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>",[124,16,125,126,15,127],[],{"html":135,"slug":136,"uuid":137,"title":138,"date":139,"modified":140,"section":3,"published":12,"content_tags":141,"links":143,"abstract":145,"tags":146,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":147,"latestComment":26},"\u003Cp>My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.\u003C/p>\n\u003Cp>I currently do not update this repo anymore because i moved all cheat sheets into my \u003Ca href=\"https://tiim.ch/tags/obsidian\" rel=\"nofollow noopener noreferrer\">Obsidian Vault\u003C/a>. I am looking into a way to keep those two repositories in sync in the future.\u003C/p>","projects/my-cheatsheets","c6d2511a-5d5c-4957-b730-5536f949a1b4","My Cheat Sheets",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[15,16,142],"cheatsheet",[144],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/my-cheatsheets\" rel=\"nofollow noopener noreferrer\">Cheat Sheets on Github\u003C/a>\u003C/p>","\u003Cp>My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.\u003C/p>",[142,16,15],[],{"html":149,"slug":150,"uuid":151,"title":152,"date":153,"modified":154,"section":3,"published":12,"content_tags":155,"links":159,"abstract":161,"tags":162,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":163,"latestComment":26},"\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\" rel=\"nofollow noopener noreferrer\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\" rel=\"nofollow noopener noreferrer\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>","projects/obsidian-fitbit-script","f42e0cad-df0e-4862-8beb-352071f81890","Obsidian.md Fitbit Activity Script",["Date","2022-04-27T20:40:15.000Z"],["Date","2022-06-08T20:15:48.000Z"],[156,157,158,16],"obsidian","fitbit","plugin",[160],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Fitbit-Obsidian-Templater-Script\" rel=\"nofollow noopener noreferrer\">GitHub Repo\u003C/a>\u003C/p>","\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>",[16,157,156,158],[],{"html":165,"slug":166,"uuid":167,"title":168,"date":169,"modified":74,"section":3,"published":12,"content_tags":170,"links":173,"abstract":175,"tags":176,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":177,"latestComment":26},"\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>\n\u003Cp>Pomo is a simple pomodoro timer. It allows you to either specify the number of repetitions (pomodori), the duration of the pomodori and the duration of the breaks, or\nyou can stecify an end time, and let pomo calculate the durations and repetitions.\u003C/p>\n\u003Cp>Pomo runs as a cli tool and stores the current state in a json file. All pomo executions excep \u003Ccode>pomo watch\u003C/code> just\nmodify this json file and terminate. The watch command displays the current pomodoro timer, optionally writes the timer to a text file,\nand watches for changes of the json file.\u003C/p>","projects/pomo","bfa1a7fa-b8a3-469f-b8e8-727ab705cb93","Pomo 🍅",["Date","2023-08-03T11:03:00.000Z"],[171,172,16],"rust","cli",[174],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/pomo\" rel=\"nofollow noopener noreferrer\">pomo Github\u003C/a>\u003C/p>","\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>",[172,16,171],[],{"html":179,"slug":180,"uuid":181,"title":182,"date":183,"modified":184,"section":3,"published":12,"content_tags":185,"links":188,"abstract":179,"tags":190,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":191,"latestComment":26},"\u003Cp>The website of the \"ScBirs\" swim club. This website serves as the center of all information distribution for the swimclub.\u003C/p>","projects/scbirs-website","32787e66-2678-435f-be39-c27265bc9a6f","Swim Club Birsfelden Website",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[186,187,16],"wordpress","php",[189],"\u003Cp>\u003Ca href=\"https://scbirs.ch\" rel=\"nofollow noopener noreferrer\">Website\u003C/a>\u003C/p>",[16,187,186],[],{"html":193,"slug":194,"uuid":195,"title":196,"date":197,"modified":74,"section":3,"published":12,"content_tags":198,"links":200,"abstract":202,"tags":203,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":204,"latestComment":26},"\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>\n\u003Cp>I built TeamKit because the swim club I am a part of needed an easy way for coaches to have an overview of their teams, handle attendance and track their own hours (not implemented yet). I tried a bunch of existing apps and services, but all of them were either too clunky for us or required the team members to sign up as well. This was a dealbreaker for us because we have a bunch of kids teams which are too young to sign up to websites, and because many of the parents are not very tech literate.\u003C/p>\n\u003Cp>With TeamKit a coach can quickly add new team members to a team, create new members directly in the event view (useful for example a person that just wants to try out) without having to add more details than a name.\u003C/p>\n\u003Cp>In the latest update, TeamKit now allows users to create notes on events. This is useful for planning, writing quick notes for an event or a practice session and for sharing information with other coaches.\u003C/p>\n\u003Cp>If you are interested, TeamKit is currenlty free while it is still in beta.\u003C/p>","projects/teamkit","a3040709-cd2d-43cb-ab33-2061ba1ae061","TeamKit",["Date","2022-11-27T09:19:08.000Z"],[14,58,199,16],"postgres",[201],"\u003Cp>\u003Ca href=\"https://teamkit.cc\" rel=\"nofollow noopener noreferrer\">TeamKit\u003C/a>\u003C/p>","\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>",[16,58,199,14],[],{"html":206,"slug":207,"uuid":208,"title":209,"date":210,"modified":211,"section":3,"published":12,"content_tags":212,"links":214,"abstract":217,"tags":218,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":219,"latestComment":26},"\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\" rel=\"nofollow noopener noreferrer\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>","projects/woocommerce-order-explorer","8a52a1ad-a5cf-4191-947c-db6862746816","WooCommerce Order Explorer",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[95,186,213,16],"woocommerce",[215,216],"\u003Cp>\u003Ca href=\"https://tiim.ch/woocommerce-order-explorer-js/\" rel=\"nofollow noopener noreferrer\">Open Order Explorer\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/woo-commerce-order-explorer-js-vmu3h\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>",[16,95,213,186],[]],"uses":{}}]} diff --git a/projects/aqualetics-coach.html b/projects/aqualetics-coach.html new file mode 100644 index 00000000..c14b922c --- /dev/null +++ b/projects/aqualetics-coach.html @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Aqualetics Coach - Tim Bachmann + + +
+ + +
+
+ +
+ +

Aqualetics Coach

+ + +

dev + docker + graphql + hasura + node + postgresql + vue +

+ +
by
+ published on +last updated on
+ + +

An internal web app for swim schools. Developed specifically for the "Kids" program of Swiss Aquatics. Live in production at the Aqualetics swim school since August 2019.

+

The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents. +The admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.

+

Screenshot of the coaches view

+

The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/aqualetics-coach/__data.json b/projects/aqualetics-coach/__data.json new file mode 100644 index 00000000..f6bce253 --- /dev/null +++ b/projects/aqualetics-coach/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":29},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":18,"abstract":22,"tags":23,"type":24,"cover_image":-1,"description":25,"folder":26,"comments":27,"latestComment":28},"\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\" rel=\"nofollow noopener noreferrer\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>\n\u003Cp>The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents.\nThe admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/aqualetics-coach-screenshot.png\" alt=\"Screenshot of the coaches view\">\u003C/p>\n\u003Cp>The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.\u003C/p>","projects/aqualetics-coach","3210d289-1f9e-41b5-b1f9-d20f00f6a0c5","Aqualetics Coach",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[11,12,13,14,15,16,17],"node","vue","graphql","hasura","postgresql","docker","dev",[19,20,21],"\u003Cp>\u003Ca href=\"https://sundrbi.ch/coach-application/\" rel=\"nofollow noopener noreferrer\">Overview\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch/2019/09/15/schwimmcoach-applikation-innovation/\" rel=\"nofollow noopener noreferrer\">Blog Post 🇩🇪\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch\" rel=\"nofollow noopener noreferrer\">Aqualetics Swim School\u003C/a>\u003C/p>","\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>",[17,16,13,14,11,15,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":30,"slug":31,"uuid":32,"date":33,"created":34,"published":9,"abstract":35,"tags":36,"links":-1,"type":24,"cover_image":-1,"description":25,"folder":37},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/indiego.html b/projects/indiego.html new file mode 100644 index 00000000..cc22fa41 --- /dev/null +++ b/projects/indiego.html @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + IndieGo - Tim Bachmann + + +
+ + +
+
+ +
IndieGo +
+ +

IndieGo

+ + +

dev + docker + go + golang + indieweb + sqlite +

+ +
by
+ published on +
+ + +

I blogged about creating a comment system for my website a while ago, +and later how I implemented webmentions into that same project. +Since then this little go program has grown quite a bit, and it has turned into a modular platform +that supports quite a few technologies:

+
    +
  • The basic commenting system
  • +
  • Sending and receiving webmentions
  • +
  • Micropub server implementation
  • +
  • IndieAuth (decentralized authentication standard based on OAuth)
  • +
  • Admin dashboard
  • +
  • Admin backup endpoint
  • +
+

Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and +comments through the fediverse show up back on my website.

+

The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core +of the application is only concerned with initializing those plugins.

+

If you have any questions, or want to run IndieGo yourself, don't hesitate to contact me.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/indiego/__data.json b/projects/indiego/__data.json new file mode 100644 index 00000000..e44bb1c4 --- /dev/null +++ b/projects/indiego/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":28},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"cover_image":10,"content_tags":11,"links":18,"abstract":21,"tags":22,"type":23,"description":24,"folder":25,"comments":26,"latestComment":27},"\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>\n\u003Cul>\n\u003Cli>The basic commenting system\u003C/li>\n\u003Cli>Sending and receiving webmentions\u003C/li>\n\u003Cli>Micropub server implementation\u003C/li>\n\u003Cli>IndieAuth (decentralized authentication standard based on OAuth)\u003C/li>\n\u003Cli>Admin dashboard\u003C/li>\n\u003Cli>Admin backup endpoint\u003C/li>\n\u003C/ul>\n\u003Cp>Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and\ncomments through the fediverse show up back on my website.\u003C/p>\n\u003Cp>The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core\nof the application is only concerned with initializing those plugins.\u003C/p>\n\u003Cp>If you have any questions, or want to run IndieGo yourself, don't hesitate to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>.\u003C/p>","projects/indiego","0cf125b3-a99a-4996-8f84-ec5105d64c57","IndieGo",["Date","2023-08-02T08:39:00.000Z"],null,"Projects",true,"/assets/2022-07-first-go-project-commenting-api.png",[12,13,14,15,16,17],"go","golang","indieweb","docker","sqlite","dev",[19,20],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">IndieGo Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://comments.tiim.ch\" rel=\"nofollow noopener noreferrer\">Admin Interface\u003C/a> - authentication required\u003C/p>","\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>",[17,15,12,13,14,16],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":29,"slug":30,"uuid":31,"date":32,"created":33,"published":9,"abstract":34,"tags":35,"links":-1,"type":23,"cover_image":-1,"description":24,"folder":36},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/lap-counter.html b/projects/lap-counter.html new file mode 100644 index 00000000..ba32ecda --- /dev/null +++ b/projects/lap-counter.html @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Lap Counter - Tim Bachmann + + +
+ + +
+
+ +
+ +

Lap Counter

+ + +

dev + svelte + swim +

+ +
by
+ published on +last updated on
+ + +

A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.

+

I built this little page to help me count laps for my swimmers for some time based test sets. The first version of the app just had a grid of static buttons, one for each athlete. I quickly found that it is very hard to keep track which laps I already counted. As a way to visualise when each button was last pressed, they change colour. The buttons start green when pressed and slowly turn red over time, based on the average duration of a lap.

+

The page works completely offline (after the page loads) and no data is sent to any server. It is also possible to export the data to a csv file.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/lap-counter/__data.json b/projects/lap-counter/__data.json new file mode 100644 index 00000000..bee4c27d --- /dev/null +++ b/projects/lap-counter/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":24},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":14,"abstract":17,"tags":18,"type":19,"cover_image":-1,"description":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.\u003C/p>\n\u003Cp>I built this little page to help me count laps for my swimmers for some time based test sets. The first version of the app just had a grid of static buttons, one for each athlete. I quickly found that it is very hard to keep track which laps I already counted. As a way to visualise when each button was last pressed, they change colour. The buttons start green when pressed and slowly turn red over time, based on the average duration of a lap.\u003C/p>\n\u003Cp>The page works completely offline (after the page loads) and no data is sent to any server. It is also possible to export the data to a csv file.\u003C/p>","projects/lap-counter","871ebd58-0543-4d3a-9f3d-16cd85da9bf9","Lap Counter",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[11,12,13],"svelte","swim","dev",[15,16],"\u003Cp>\u003Ca href=\"https://tiim.ch/lap-counter-js/\" rel=\"nofollow noopener noreferrer\">Open Lap Counter\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/laps-counter-8f0sb\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.\u003C/p>",[13,11,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":25,"slug":26,"uuid":27,"date":28,"created":29,"published":9,"abstract":30,"tags":31,"links":-1,"type":19,"cover_image":-1,"description":20,"folder":32},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/lenex-split-sheet.html b/projects/lenex-split-sheet.html new file mode 100644 index 00000000..a7cca051 --- /dev/null +++ b/projects/lenex-split-sheet.html @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Lenex Splits Sheet Creator - Tim Bachmann + + +
+ + +
+
+ +
+ +

Lenex Splits Sheet Creator

+ + +

dev + lenex + svelte + swim +

+ +
by
+ published on +last updated on
+ + +

Generate useful splits sheets directly from your Lenex (.lxf, .lef) file that you used to sign up the athletes for a meet.

+

Screenshot of a split sheet

+

The split sheet creator is a quick and easy way to create a split sheet from your Lenex sign-up file. +The split sheet creator does not send your Lenex file to any servers. The file is opened directly in your browser and never leaves your computer!

+

I built this web app using my lenex javascript library.

+

What is a split sheet?

+

Swim coaches often write down the times of athletes after some fractions of a race (splits). Usually those splits are recorded every lap or every second lap.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/lenex-split-sheet/__data.json b/projects/lenex-split-sheet/__data.json new file mode 100644 index 00000000..c3f050be --- /dev/null +++ b/projects/lenex-split-sheet/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":24},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":15,"abstract":17,"tags":18,"type":19,"cover_image":-1,"description":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\" rel=\"nofollow noopener noreferrer\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/lenex-splits-sheet-creator.png\" alt=\"Screenshot of a split sheet\">\u003C/p>\n\u003Cp>The split sheet creator is a quick and easy way to create a split sheet from your Lenex sign-up file.\nThe split sheet creator does not send your Lenex file to any servers. The file is opened directly in your browser and never leaves your computer!\u003C/p>\n\u003Cp>I built this web app using my \u003Ca href=\"https://www.npmjs.com/package/js-lenex\" rel=\"nofollow noopener noreferrer\">lenex javascript library\u003C/a>.\u003C/p>\n\u003Ch2>What is a split sheet?\u003C/h2>\n\u003Cp>Swim coaches often write down the times of athletes after some fractions of a race (splits). Usually those splits are recorded every lap or every second lap.\u003C/p>","projects/lenex-split-sheet","a03b6eef-ffbe-428d-a559-42bd361ab88c","Lenex Splits Sheet Creator",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[11,12,13,14],"swim","lenex","svelte","dev",[16],"\u003Cp>\u003Ca href=\"https://tiim.ch/lenex-splits-sheet-creator/\" rel=\"nofollow noopener noreferrer\">Open Splits Sheet Creator\u003C/a>\u003C/p>","\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>",[14,12,13,11],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":25,"slug":26,"uuid":27,"date":28,"created":29,"published":9,"abstract":30,"tags":31,"links":-1,"type":19,"cover_image":-1,"description":20,"folder":32},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/markdown-widget.html b/projects/markdown-widget.html new file mode 100644 index 00000000..d8a6e096 --- /dev/null +++ b/projects/markdown-widget.html @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Android Markdown Widget - Tim Bachmann + + +
+ + +
+
+ +
+ +

Android Markdown Widget

+ + +

android + dev + java + kotlin + markdown + widget +

+ +
by
+ published on +
+ + +

A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.

+

Android widgets are handled by the operating system and only support a limited set of features for rendering. +To display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/markdown-widget/__data.json b/projects/markdown-widget/__data.json new file mode 100644 index 00000000..986e7325 --- /dev/null +++ b/projects/markdown-widget/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":27},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":17,"abstract":20,"tags":21,"type":22,"cover_image":-1,"description":23,"folder":24,"comments":25,"latestComment":26},"\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>\n\u003Cp>Android widgets are handled by the operating system and only support a limited set of features for rendering.\nTo display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.\u003C/p>","projects/markdown-widget","c6a11779-cf98-4983-8744-9b1effae8d7a","Android Markdown Widget",["Date","2023-08-02T08:59:00.000Z"],null,"Projects",true,[11,12,13,14,15,16],"android","java","kotlin","markdown","widget","dev",[18,19],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Android-Markdown-Widget\" rel=\"nofollow noopener noreferrer\">Android Markdown Widget Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://f-droid.org/packages/ch.tiim.markdown_widget/\" rel=\"nofollow noopener noreferrer\">Download on F-Droid\u003C/a>\u003C/p>","\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>",[11,16,12,13,14,15],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":28,"slug":29,"uuid":30,"date":31,"created":32,"published":9,"abstract":33,"tags":34,"links":-1,"type":22,"cover_image":-1,"description":23,"folder":35},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/my-cheatsheets.html b/projects/my-cheatsheets.html new file mode 100644 index 00000000..d7c66a72 --- /dev/null +++ b/projects/my-cheatsheets.html @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + My Cheat Sheets - Tim Bachmann + + +
+ + +
+
+ +
+ +

My Cheat Sheets

+ + +

cheatsheet + dev + markdown +

+ +
by
+ published on +last updated on
+ + +

My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.

+

I currently do not update this repo anymore because i moved all cheat sheets into my Obsidian Vault. I am looking into a way to keep those two repositories in sync in the future.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/my-cheatsheets/__data.json b/projects/my-cheatsheets/__data.json new file mode 100644 index 00000000..95614c05 --- /dev/null +++ b/projects/my-cheatsheets/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":23},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":14,"abstract":16,"tags":17,"type":18,"cover_image":-1,"description":19,"folder":20,"comments":21,"latestComment":22},"\u003Cp>My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.\u003C/p>\n\u003Cp>I currently do not update this repo anymore because i moved all cheat sheets into my \u003Ca href=\"https://tiim.ch/tags/obsidian\" rel=\"nofollow noopener noreferrer\">Obsidian Vault\u003C/a>. I am looking into a way to keep those two repositories in sync in the future.\u003C/p>","projects/my-cheatsheets","c6d2511a-5d5c-4957-b730-5536f949a1b4","My Cheat Sheets",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[11,12,13],"markdown","dev","cheatsheet",[15],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/my-cheatsheets\" rel=\"nofollow noopener noreferrer\">Cheat Sheets on Github\u003C/a>\u003C/p>","\u003Cp>My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.\u003C/p>",[13,12,11],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":24,"slug":25,"uuid":26,"date":27,"created":28,"published":9,"abstract":29,"tags":30,"links":-1,"type":18,"cover_image":-1,"description":19,"folder":31},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/obsidian-fitbit-script.html b/projects/obsidian-fitbit-script.html new file mode 100644 index 00000000..3afb6984 --- /dev/null +++ b/projects/obsidian-fitbit-script.html @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Obsidian.md Fitbit Activity Script - Tim Bachmann + + +
+ + +
+
+ +
+ +

Obsidian.md Fitbit Activity Script

+ + +

dev + fitbit + obsidian + plugin +

+ +
by
+ published on +last updated on
+ + +

If you use Obsidian.md to track your daily activity and wear a fitbit, this script is for you! This is a user script for the Templater Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/obsidian-fitbit-script/__data.json b/projects/obsidian-fitbit-script/__data.json new file mode 100644 index 00000000..1aee2a43 --- /dev/null +++ b/projects/obsidian-fitbit-script/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":24},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":15,"abstract":17,"tags":18,"type":19,"cover_image":-1,"description":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\" rel=\"nofollow noopener noreferrer\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\" rel=\"nofollow noopener noreferrer\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>","projects/obsidian-fitbit-script","f42e0cad-df0e-4862-8beb-352071f81890","Obsidian.md Fitbit Activity Script",["Date","2022-04-27T20:40:15.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[11,12,13,14],"obsidian","fitbit","plugin","dev",[16],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Fitbit-Obsidian-Templater-Script\" rel=\"nofollow noopener noreferrer\">GitHub Repo\u003C/a>\u003C/p>","\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>",[14,12,11,13],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":25,"slug":26,"uuid":27,"date":28,"created":29,"published":9,"abstract":30,"tags":31,"links":-1,"type":19,"cover_image":-1,"description":20,"folder":32},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/pomo.html b/projects/pomo.html new file mode 100644 index 00000000..2fa54513 --- /dev/null +++ b/projects/pomo.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Pomo 🍅 - Tim Bachmann + + +
+ + +
+
+ +
+ +

Pomo 🍅

+ + +

cli + dev + rust +

+ +
by
+ published on +
+ + +

I created pomo as a way to keep me focused for working on my masters thesis, and at the same time +allowed me to learn the rust programming language.

+

Pomo is a simple pomodoro timer. It allows you to either specify the number of repetitions (pomodori), the duration of the pomodori and the duration of the breaks, or +you can stecify an end time, and let pomo calculate the durations and repetitions.

+

Pomo runs as a cli tool and stores the current state in a json file. All pomo executions excep pomo watch just +modify this json file and terminate. The watch command displays the current pomodoro timer, optionally writes the timer to a text file, +and watches for changes of the json file.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/pomo/__data.json b/projects/pomo/__data.json new file mode 100644 index 00000000..a545fa7e --- /dev/null +++ b/projects/pomo/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":23},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":14,"abstract":16,"tags":17,"type":18,"cover_image":-1,"description":19,"folder":20,"comments":21,"latestComment":22},"\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>\n\u003Cp>Pomo is a simple pomodoro timer. It allows you to either specify the number of repetitions (pomodori), the duration of the pomodori and the duration of the breaks, or\nyou can stecify an end time, and let pomo calculate the durations and repetitions.\u003C/p>\n\u003Cp>Pomo runs as a cli tool and stores the current state in a json file. All pomo executions excep \u003Ccode>pomo watch\u003C/code> just\nmodify this json file and terminate. The watch command displays the current pomodoro timer, optionally writes the timer to a text file,\nand watches for changes of the json file.\u003C/p>","projects/pomo","bfa1a7fa-b8a3-469f-b8e8-727ab705cb93","Pomo 🍅",["Date","2023-08-03T11:03:00.000Z"],null,"Projects",true,[11,12,13],"rust","cli","dev",[15],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/pomo\" rel=\"nofollow noopener noreferrer\">pomo Github\u003C/a>\u003C/p>","\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>",[12,13,11],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":24,"slug":25,"uuid":26,"date":27,"created":28,"published":9,"abstract":29,"tags":30,"links":-1,"type":18,"cover_image":-1,"description":19,"folder":31},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/scbirs-website.html b/projects/scbirs-website.html new file mode 100644 index 00000000..cde8c6f4 --- /dev/null +++ b/projects/scbirs-website.html @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + Swim Club Birsfelden Website - Tim Bachmann + + +
+ + +
+
+ +
+ +

Swim Club Birsfelden Website

+ + +

dev + php + wordpress +

+ +
by
+ published on +last updated on
+ + +

The website of the "ScBirs" swim club. This website serves as the center of all information distribution for the swimclub.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/scbirs-website/__data.json b/projects/scbirs-website/__data.json new file mode 100644 index 00000000..e49441e6 --- /dev/null +++ b/projects/scbirs-website/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":22},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":14,"abstract":2,"tags":16,"type":17,"cover_image":-1,"description":18,"folder":19,"comments":20,"latestComment":21},"\u003Cp>The website of the \"ScBirs\" swim club. This website serves as the center of all information distribution for the swimclub.\u003C/p>","projects/scbirs-website","32787e66-2678-435f-be39-c27265bc9a6f","Swim Club Birsfelden Website",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[11,12,13],"wordpress","php","dev",[15],"\u003Cp>\u003Ca href=\"https://scbirs.ch\" rel=\"nofollow noopener noreferrer\">Website\u003C/a>\u003C/p>",[13,12,11],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":23,"slug":24,"uuid":25,"date":26,"created":27,"published":9,"abstract":28,"tags":29,"links":-1,"type":17,"cover_image":-1,"description":18,"folder":30},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/teamkit.html b/projects/teamkit.html new file mode 100644 index 00000000..2237fc9b --- /dev/null +++ b/projects/teamkit.html @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + TeamKit - Tim Bachmann + + +
+ + +
+
+ +
+ +

TeamKit

+ + +

dev + hasura + postgres + sveltekit +

+ +
by
+ published on +
+ + +

TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.

+

I built TeamKit because the swim club I am a part of needed an easy way for coaches to have an overview of their teams, handle attendance and track their own hours (not implemented yet). I tried a bunch of existing apps and services, but all of them were either too clunky for us or required the team members to sign up as well. This was a dealbreaker for us because we have a bunch of kids teams which are too young to sign up to websites, and because many of the parents are not very tech literate.

+

With TeamKit a coach can quickly add new team members to a team, create new members directly in the event view (useful for example a person that just wants to try out) without having to add more details than a name.

+

In the latest update, TeamKit now allows users to create notes on events. This is useful for planning, writing quick notes for an event or a practice session and for sharing information with other coaches.

+

If you are interested, TeamKit is currenlty free while it is still in beta.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/teamkit/__data.json b/projects/teamkit/__data.json new file mode 100644 index 00000000..c87fd91d --- /dev/null +++ b/projects/teamkit/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":24},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":15,"abstract":17,"tags":18,"type":19,"cover_image":-1,"description":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>\n\u003Cp>I built TeamKit because the swim club I am a part of needed an easy way for coaches to have an overview of their teams, handle attendance and track their own hours (not implemented yet). I tried a bunch of existing apps and services, but all of them were either too clunky for us or required the team members to sign up as well. This was a dealbreaker for us because we have a bunch of kids teams which are too young to sign up to websites, and because many of the parents are not very tech literate.\u003C/p>\n\u003Cp>With TeamKit a coach can quickly add new team members to a team, create new members directly in the event view (useful for example a person that just wants to try out) without having to add more details than a name.\u003C/p>\n\u003Cp>In the latest update, TeamKit now allows users to create notes on events. This is useful for planning, writing quick notes for an event or a practice session and for sharing information with other coaches.\u003C/p>\n\u003Cp>If you are interested, TeamKit is currenlty free while it is still in beta.\u003C/p>","projects/teamkit","a3040709-cd2d-43cb-ab33-2061ba1ae061","TeamKit",["Date","2022-11-27T09:19:08.000Z"],null,"Projects",true,[11,12,13,14],"sveltekit","hasura","postgres","dev",[16],"\u003Cp>\u003Ca href=\"https://teamkit.cc\" rel=\"nofollow noopener noreferrer\">TeamKit\u003C/a>\u003C/p>","\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>",[14,12,13,11],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":25,"slug":26,"uuid":27,"date":28,"created":29,"published":9,"abstract":30,"tags":31,"links":-1,"type":19,"cover_image":-1,"description":20,"folder":32},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/projects/woocommerce-order-explorer.html b/projects/woocommerce-order-explorer.html new file mode 100644 index 00000000..3e829a52 --- /dev/null +++ b/projects/woocommerce-order-explorer.html @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + WooCommerce Order Explorer - Tim Bachmann + + +
+ + +
+
+ +
+ +

WooCommerce Order Explorer

+ + +

dev + svelte + woocommerce + wordpress +

+ +
by
+ published on +last updated on
+ + +

A small client side only web app to browse all open orders on your WooCommerce store. All data is stored in the browser.

+ + + + + +
Photo of Tim Bachmann
+

Hi, my name is Tim Bachmann! +I'm a master graduate in computer science at University of Basel, swimmer and swim coach.

+

I am passionate about all things web development, swimming, personal knowledge management and much more. +If you liked this or any of my posts, feel free to follow me.

+
+
+ +
+
+

0 Comments and Interactions

+Leave a comment or interact with this page via + WebMention +
+ +
+ +
+
+
+
+ + + + +
+
+
    +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/projects/woocommerce-order-explorer/__data.json b/projects/woocommerce-order-explorer/__data.json new file mode 100644 index 00000000..979f451e --- /dev/null +++ b/projects/woocommerce-order-explorer/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"project":1,"about":25},{"html":2,"slug":3,"uuid":4,"title":5,"date":6,"modified":7,"section":8,"published":9,"content_tags":10,"links":15,"abstract":18,"tags":19,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\" rel=\"nofollow noopener noreferrer\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>","projects/woocommerce-order-explorer","8a52a1ad-a5cf-4191-947c-db6862746816","WooCommerce Order Explorer",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[11,12,13,14],"svelte","wordpress","woocommerce","dev",[16,17],"\u003Cp>\u003Ca href=\"https://tiim.ch/woocommerce-order-explorer-js/\" rel=\"nofollow noopener noreferrer\">Open Order Explorer\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/woo-commerce-order-explorer-js-vmu3h\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>",[14,11,13,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"date":29,"created":30,"published":9,"abstract":31,"tags":32,"links":-1,"type":20,"cover_image":-1,"description":21,"folder":33},"\u003Cp>Hi, my name is \u003Cspan class=\"p-name\">Tim Bachmann\u003C/span>!\nI'm a \u003Cspan class=\"p-role\">master graduate in computer science\u003C/span> at \u003Cspan class=\"p-org\">University of Basel\u003C/span>, swimmer and swim coach.\u003C/p>\n\u003Cp>I am passionate about all things web development, swimming, personal knowledge management and much more.\nIf you liked this or any of my posts, feel free to \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">follow me\u003C/a>.\u003C/p>","about","d8e56802-2847-4053-b213-9b004f1b965c",["Date","2022-10-15T00:00:00.000Z"],["Date","2022-10-15T19:47:09.652Z"],"\u003Cp>Hi, my name is {{name}}!\nI'm a {{role}} at {{org}}, swimmer and swim coach.\u003C/p>",[],"metadata"],"uses":{"params":["slug"]}}]} diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/swim-emoji.png b/swim-emoji.png new file mode 100644 index 00000000..943c45f6 Binary files /dev/null and b/swim-emoji.png differ diff --git a/tags.html b/tags.html new file mode 100644 index 00000000..e476a016 --- /dev/null +++ b/tags.html @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + +
+ + +
+

Tags 🏷️

+
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/3d-model.html b/tags/3d-model.html new file mode 100644 index 00000000..622dd543 --- /dev/null +++ b/tags/3d-model.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️3d-model - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: 3d-model

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/3d-model/__data.json b/tags/3d-model/__data.json new file mode 100644 index 00000000..07422cda --- /dev/null +++ b/tags/3d-model/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":28},[2],{"html":3,"slug":4,"uuid":5,"title":6,"section":7,"date":8,"modified":9,"published":10,"content_tags":11,"links":15,"abstract":18,"tags":19,"type":23,"cover_image":-1,"description":24,"folder":25,"comments":26,"latestComment":27},"\u003Cp>I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.\u003C/p>\n\u003Cp>\u003Cimg src=\"https://img2.cgtrader.com/items/3180107/fd6e03d4a5/pile-of-clothes-on-the-ground-3d-scanned-3d-model-low-poly-obj-fbx-blend-fbm.jpg\" alt=\"Example of a 3D scanned model: A pile of clothes on the floor\">\u003C/p>\n\u003Cp>For 3D modeling I use the open source tool \u003Ca href=\"https://www.blender.org/\" rel=\"nofollow noopener noreferrer\">Blender\u003C/a> and for 3D scanning my software of choice is \u003Ca href=\"https://www.capturingreality.com/\" rel=\"nofollow noopener noreferrer\">Reality Capture\u003C/a>.\u003C/p>","projects/3d-scans","01128b77-eace-4b25-a5b9-9cf36ea32b6a","3D Scanned and Modeled Objects","Projects",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],true,[12,13,14],"3D Scanning","3D model","3D",[16,17],"\u003Cp>\u003Ca href=\"https://www.cgtrader.com/tiim\" rel=\"nofollow noopener noreferrer\">CGTrader Profile\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://www.cgtrader.com/3d-models?author=tiim\" rel=\"nofollow noopener noreferrer\">All 3D Models\u003C/a>\u003C/p>","\u003Cp>I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.\u003C/p>",[20,21,22],"3d","3d-model","3d-scanning","article","","projects",[],"2023-09-02T19:26:59Z",{"tag":21}],"uses":{"params":["slug"]}}]} diff --git a/tags/3d-scanning.html b/tags/3d-scanning.html new file mode 100644 index 00000000..3b32ebd8 --- /dev/null +++ b/tags/3d-scanning.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️3d-scanning - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: 3d-scanning

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/3d-scanning/__data.json b/tags/3d-scanning/__data.json new file mode 100644 index 00000000..2e2c7172 --- /dev/null +++ b/tags/3d-scanning/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":28},[2],{"html":3,"slug":4,"uuid":5,"title":6,"section":7,"date":8,"modified":9,"published":10,"content_tags":11,"links":15,"abstract":18,"tags":19,"type":23,"cover_image":-1,"description":24,"folder":25,"comments":26,"latestComment":27},"\u003Cp>I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.\u003C/p>\n\u003Cp>\u003Cimg src=\"https://img2.cgtrader.com/items/3180107/fd6e03d4a5/pile-of-clothes-on-the-ground-3d-scanned-3d-model-low-poly-obj-fbx-blend-fbm.jpg\" alt=\"Example of a 3D scanned model: A pile of clothes on the floor\">\u003C/p>\n\u003Cp>For 3D modeling I use the open source tool \u003Ca href=\"https://www.blender.org/\" rel=\"nofollow noopener noreferrer\">Blender\u003C/a> and for 3D scanning my software of choice is \u003Ca href=\"https://www.capturingreality.com/\" rel=\"nofollow noopener noreferrer\">Reality Capture\u003C/a>.\u003C/p>","projects/3d-scans","01128b77-eace-4b25-a5b9-9cf36ea32b6a","3D Scanned and Modeled Objects","Projects",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],true,[12,13,14],"3D Scanning","3D model","3D",[16,17],"\u003Cp>\u003Ca href=\"https://www.cgtrader.com/tiim\" rel=\"nofollow noopener noreferrer\">CGTrader Profile\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://www.cgtrader.com/3d-models?author=tiim\" rel=\"nofollow noopener noreferrer\">All 3D Models\u003C/a>\u003C/p>","\u003Cp>I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.\u003C/p>",[20,21,22],"3d","3d-model","3d-scanning","article","","projects",[],"2023-09-02T19:26:59Z",{"tag":22}],"uses":{"params":["slug"]}}]} diff --git a/tags/3d.html b/tags/3d.html new file mode 100644 index 00000000..f08d84a1 --- /dev/null +++ b/tags/3d.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️3d - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: 3d

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/3d/__data.json b/tags/3d/__data.json new file mode 100644 index 00000000..c85a7c64 --- /dev/null +++ b/tags/3d/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":28},[2],{"html":3,"slug":4,"uuid":5,"title":6,"section":7,"date":8,"modified":9,"published":10,"content_tags":11,"links":15,"abstract":18,"tags":19,"type":23,"cover_image":-1,"description":24,"folder":25,"comments":26,"latestComment":27},"\u003Cp>I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.\u003C/p>\n\u003Cp>\u003Cimg src=\"https://img2.cgtrader.com/items/3180107/fd6e03d4a5/pile-of-clothes-on-the-ground-3d-scanned-3d-model-low-poly-obj-fbx-blend-fbm.jpg\" alt=\"Example of a 3D scanned model: A pile of clothes on the floor\">\u003C/p>\n\u003Cp>For 3D modeling I use the open source tool \u003Ca href=\"https://www.blender.org/\" rel=\"nofollow noopener noreferrer\">Blender\u003C/a> and for 3D scanning my software of choice is \u003Ca href=\"https://www.capturingreality.com/\" rel=\"nofollow noopener noreferrer\">Reality Capture\u003C/a>.\u003C/p>","projects/3d-scans","01128b77-eace-4b25-a5b9-9cf36ea32b6a","3D Scanned and Modeled Objects","Projects",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],true,[12,13,14],"3D Scanning","3D model","3D",[16,17],"\u003Cp>\u003Ca href=\"https://www.cgtrader.com/tiim\" rel=\"nofollow noopener noreferrer\">CGTrader Profile\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://www.cgtrader.com/3d-models?author=tiim\" rel=\"nofollow noopener noreferrer\">All 3D Models\u003C/a>\u003C/p>","\u003Cp>I like to 3d scan and model as a hobby. To keep me motivated, I decided to put the models for sale on cgTrader.\u003C/p>",[20,21,22],"3d","3d-model","3d-scanning","article","","projects",[],"2023-09-02T19:26:59Z",{"tag":20}],"uses":{"params":["slug"]}}]} diff --git a/tags/__data.json b/tags/__data.json new file mode 100644 index 00000000..373df561 --- /dev/null +++ b/tags/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"tags":1},[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182],{"tag":3},"3d",{"tag":5},"3d-model",{"tag":7},"3d-scanning",{"tag":9},"activitypub",{"tag":11},"android",{"tag":13},"ansible",{"tag":15},"audio",{"tag":17},"bash",{"tag":19},"bluetooth",{"tag":21},"caddy",{"tag":23},"cdn",{"tag":25},"cgo",{"tag":27},"cheatsheet",{"tag":29},"cli",{"tag":31},"cloudflare",{"tag":33},"dev",{"tag":35},"dns",{"tag":37},"docker",{"tag":39},"email",{"tag":41},"fediverse",{"tag":43},"fitbit",{"tag":45},"forum",{"tag":47},"git",{"tag":49},"github-pages",{"tag":51},"go",{"tag":53},"golang",{"tag":55},"graphql",{"tag":57},"hasura",{"tag":59},"heuristic",{"tag":61},"how-to",{"tag":63},"indiego",{"tag":65},"indieweb",{"tag":67},"irc",{"tag":69},"java",{"tag":71},"javascript",{"tag":73},"kotlin",{"tag":75},"lenex",{"tag":77},"linux",{"tag":79},"markdown",{"tag":81},"mastodon",{"tag":83},"mf2",{"tag":85},"micropub",{"tag":87},"moderation",{"tag":89},"networking",{"tag":91},"newsletter",{"tag":93},"node",{"tag":95},"ntfy.sh",{"tag":97},"obsidian",{"tag":99},"pddl",{"tag":101},"pdr",{"tag":103},"photo",{"tag":105},"php",{"tag":107},"planning-system",{"tag":109},"plugin",{"tag":111},"postgres",{"tag":113},"postgresql",{"tag":115},"project",{"tag":117},"quick-tip",{"tag":119},"reddit",{"tag":121},"rss",{"tag":123},"rust",{"tag":125},"software",{"tag":127},"sql",{"tag":129},"sqlite",{"tag":131},"ssh",{"tag":133},"ssr",{"tag":135},"storj",{"tag":137},"svelte",{"tag":139},"sveltekit",{"html":141,"slug":142,"uuid":143,"published":144,"date":145,"modified":146,"abstract":141,"tags":147,"links":-1,"type":148,"cover_image":-1,"description":149,"folder":150,"tag":151},"\u003Cp>🏊‍♀️ This is all the content related to my hobby swimming.\u003C/p>","tags/swim","456825d8-b2f6-4c1a-8976-3358ed9dacba",true,["Date","2022-06-03T00:00:56.000Z"],["Date","2022-06-08T22:15:48.000Z"],[],"article","","tags","swim",{"tag":153},"tiim.ch",{"tag":155},"twitter",{"tag":157},"urql",{"tag":159},"vpn",{"tag":161},"vue",{"tag":163},"vue.js",{"tag":165},"web-api",{"tag":167},"webmentions",{"tag":169},"weechat",{"tag":171},"wget",{"tag":173},"widget",{"tag":175},"windows",{"tag":177},"winter",{"tag":179},"woocommerce",{"tag":181},"wordpress",{"tag":183},"wsl"],"uses":{}}]} diff --git a/tags/activitypub.html b/tags/activitypub.html new file mode 100644 index 00000000..3bc2363a --- /dev/null +++ b/tags/activitypub.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️activitypub - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: activitypub

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/activitypub/__data.json b/tags/activitypub/__data.json new file mode 100644 index 00000000..ca7cc9a4 --- /dev/null +++ b/tags/activitypub/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\" rel=\"nofollow noopener noreferrer\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\" rel=\"nofollow noopener noreferrer\">Way back machine\u003C/a>.\u003C/p>\n\u003Cp>Another community I used to be really active in, was for a small indie roleplaying game called \u003Ca href=\"\">Illarion\u003C/a>. Again, the community relied heavily on a forum for communications. This time it was used for players to engage in \"out of character\" communication, as well as a way to simulate a metaphorical bullet board in the game town square where characters could leave notes for each other.\nSince the game was closely inspired by TTRPGs like D&D, the role-playing part was more important than the in-game mechanics. The forum allowed characters to interact with each other that were not online at the same time. Again, I got really invested in this community, even going so far as joining other guild-specific forums.\u003C/p>\n\u003Cp>I eventually moved on from both of those amazing communities, because my interests changed. I left the AutoHotkey community because I started to get more involved with other programming languages, and I left the Illarion community because I (with the support of my parents) was looking for a less time-intensive game. Unfortunately, I never happened to find another online community like those two ever again...\u003C/p>\n\u003Cp>Sometime later I joined Reddit and was amazed. It felt like a place where all communities come together on a single site. No need to check on multiple websites for new posts, everything neatly together in a single website, accessible on a single (third party) app. I remember wondering why people were still using forums when Reddit was so much simpler.\u003C/p>\n\u003Cp>Jumping to the present and I realize that I was wrong. Even though I am subscribed to a bunch of communities on Reddit, I barely comment on any posts and posted even less. While I am a community member on record, I do not feel like one. The wealth of communities, as well as the incentive to go on the front page to see the most popular posts of the whole site, made me want to open Reddit, but it did not give me the feeling of belonging. I rather felt like a spectator that from time to time gathers the courage to shout his own ideas into the ether.\u003C/p>\n\u003Cblockquote>\n\u003Cp>Side note: Discord comes much closer to the feeling of community. However, the nature of chat makes the interactions fleeting, being in a chat room with a few hundred other people, where every message is just a few sentences at most does not lead to the same connections. No one expects their message to be read again after a few days.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Now the company behind Reddit started to lose the goodwill of the users. While I don't think Reddit will die anytime soon, I think there are a lot of people looking for alternatives. And the best alternative to the website that killed forums is... forums.\u003C/p>\n\u003Cp>While forums largely still work the same as they did 15 years ago, there have been developments that might make them more feasible for our desire to have everything accessible on a single site or on a single app. Last time a social media company, Twitter, annoyed its user base, the fediverse, and more specifically Mastodon, started to go more mainstream. This time I hope there will be other projects that profit. I have heard people mentioning the projects Kbin and Lemmy, both forum-like platforms that implement the ActivityPub specification. Same as Mastodon, this means users are able to interact with users on other instances. Even further, this should also allow users of any federated social network, such as Mastodon, to post and comment on any federated forum. Even established forum software such as \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">Flarum\u003C/a> and \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">nodeBB\u003C/a> are considering adding federation support.\u003C/p>\n\u003Cp>I really hope that forums make a comeback, not only because of the nostalgia but also because to me it feels like a more sustainable way to build a community. And now with the possibility to federate via the fediverse, a forum doesn't have to be a walled garden of members any more. In the end, most importantly I hope people are still finding communities they can be as passionate about as I was, without any corporate overlords trying to keep their eyeballs on ads as long as possible.\u003C/p>","blog/2023-06-16-forums","624afba6-2962-4710-9bc7-686702cc9b55",["Date","2023-06-16T18:56:56.000Z"],["Date","2023-06-16T15:09:15.488Z"],[9],null,"Forums",true,"My experience of using forums in my teens, what changed after I started using reddit and my hopes for internet communities in the future.","https://media.tiim.ch/fe5de393-9773-4eaa-877a-decffbd706b4.webp","Stable Diffusion - bunch people talking to each other, social, speech bubbles, digital art, minimalistic",[16,17,18,19],"forum","fediverse","reddit","activitypub","\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\">Way back machine\u003C/a>.\u003C/p>",[19,17,16,18],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":19}],"uses":{"params":["slug"]}}]} diff --git a/tags/android.html b/tags/android.html new file mode 100644 index 00000000..237be8d7 --- /dev/null +++ b/tags/android.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️android - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: android

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/android/__data.json b/tags/android/__data.json new file mode 100644 index 00000000..d62a350f --- /dev/null +++ b/tags/android/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":28},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":18,"abstract":21,"tags":22,"type":23,"cover_image":-1,"description":24,"folder":25,"comments":26,"latestComment":27},"\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>\n\u003Cp>Android widgets are handled by the operating system and only support a limited set of features for rendering.\nTo display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.\u003C/p>","projects/markdown-widget","c6a11779-cf98-4983-8744-9b1effae8d7a","Android Markdown Widget",["Date","2023-08-02T08:59:00.000Z"],null,"Projects",true,[12,13,14,15,16,17],"android","java","kotlin","markdown","widget","dev",[19,20],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Android-Markdown-Widget\" rel=\"nofollow noopener noreferrer\">Android Markdown Widget Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://f-droid.org/packages/ch.tiim.markdown_widget/\" rel=\"nofollow noopener noreferrer\">Download on F-Droid\u003C/a>\u003C/p>","\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>",[12,17,13,14,15,16],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":12}],"uses":{"params":["slug"]}}]} diff --git a/tags/ansible.html b/tags/ansible.html new file mode 100644 index 00000000..4130b1d5 --- /dev/null +++ b/tags/ansible.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️ansible - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: ansible

+ +

Getting the Absolute Path of a Remote Directory in Ansible

+ 9/20/2023 +
Getting the Absolute Path of a Remote Directory in Ansible +

There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/ansible/__data.json b/tags/ansible/__data.json new file mode 100644 index 00000000..ddb7362a --- /dev/null +++ b/tags/ansible/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_image_txt":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>\n\u003Cp>Deleting with elevated permission on the command line is easy: The command \u003Ccode>sudo rm -rf ~/docker/myservice\u003C/code> performs the \u003Ccode>rm\u003C/code> operation as the root user. In bash, this will delete the \u003Ccode>docker/myservice\u003C/code> folder in the user's home directory, but when doing the equivalent in Ansible, this won't work!\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\"># This does not work!\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"~/docker/myservice\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>This code will try to delete the file \u003Ccode>/user/root/docker/myservice\u003C/code>, which is not what we wanted.\u003C/p>\n\u003Cp>The bash version works because the shell first resolves the tilde in the argument to the current users' directory before calling the sudo command. In Ansible, we first switch to the root user and only then the tilde is resolved: this time to the home directory of the root user.\u003C/p>\n\u003Cp>To circumvent this, we can manually resolve the path to an absolute path. Unfortunately, I have not found a straightforward way to do this in Ansible, however the bash command \u003Ccode>readlink -f <path>\u003C/code> does exactly this. To use it in Ansible, we can use the following configuration:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\">- name: Get absolute folder path\n ansible.builtin.command:\n cmd: \"readlink -f ~/docker/myservice\"\n register: folder_abs\n changed_when: False\n\n- name: Debug\n debug:\n msg: \"{{folder_abs.stdout}}\" # prints /user/tim/docker/myservice\n\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"{{folder_abs.stdout}}\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>With this Ansible script, we manually resolve the absolute path and use it to delete the folder using root permissions. If you know of an easier way to resolve to an absolute path, please let me know!\u003C/p>","blog/2023-09-20-ansible-absolute-path","ad58acaf-56b0-4bcf-9b72-d6c054fc48d4",["Date","2023-09-20T21:39:13.000Z"],["Date","2023-09-20T20:22:35.634Z"],null,"Getting the Absolute Path of a Remote Directory in Ansible",true,"There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.","https://media.tiim.ch/3c1246e4-3201-4df6-af87-6aa4ab98800e.webp","(stable doodle) server room, neon, cables",[15,16,17,18],"dev","ansible","linux","bash","\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>",[16,18,15,17],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":16}],"uses":{"params":["slug"]}}]} diff --git a/tags/audio.html b/tags/audio.html new file mode 100644 index 00000000..d9146916 --- /dev/null +++ b/tags/audio.html @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️audio - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: audio

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/audio/__data.json b/tags/audio/__data.json new file mode 100644 index 00000000..2c99ee1c --- /dev/null +++ b/tags/audio/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":23},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"date":8,"description":9,"cover_image":10,"content_tags":11,"abstract":17,"tags":18,"links":-1,"type":19,"folder":20,"comments":21,"latestComment":22},"\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>\n\u003Cp>\u003Cem>TLDR\u003C/em>:\u003C/p>\n\u003Cul>\n\u003Cli>Either use Bluetooth Audio Receiver from the Microsoft Store to connect you phone via Bluetooth,\u003C/li>\n\u003Cli>Or use an audio cable to connect the phone to the \"line-in\" on your PC.\u003C/li>\n\u003C/ul>\n\u003Ch2>Bluetooth (recommended)\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>: A PC with integrated Bluetooth or a Bluetooth dongle.\u003C/p>\n\u003Cp>I recommend this approach more than the wired one because it is way less effort, you don't have to deal with a USB or lightning to audio dongle and in my opinion it is more reliable.\u003C/p>\n\u003Cp>Pair your phone with your PC as normal, by opening the Bluetooth settings on your phone and on the PC and wait for the devices to show up. When you successfully paired the phone once you will not have to do this again. Now you need an app that will tell the phone that it can use the PC as a wireless speaker. The only app I found that will do this is the \u003Ca href=\"https://www.microsoft.com/de-de/p/bluetooth-audio-receiver/9n9wclwdqs5j\" rel=\"nofollow noopener noreferrer\">Bluetooth Audio Receiver\u003C/a> app from the Windows Store. Install and and open it. You should see your phone on the list of Bluetooth devices on the app. Select it and click on the \u003Ccode>Open Connection\u003C/code> button. It might take a moment but after it connected, you should hear all sounds from your phone on your PC.\u003C/p>\n\u003Ch2>Wired\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>:\u003C/p>\n\u003Cul>\n\u003Cli>Male-to-Male audio cable (3.5mm audio jack).\u003C/li>\n\u003Cli>A line-in port on your PC (usually blue audio jack on the back)\u003C/li>\n\u003Cli>USB-C to audio jack adapter (Optional)\u003C/li>\n\u003Cli>Lighting to audio jack adapter (Optional)\u003C/li>\n\u003C/ul>\n\u003Cp>This approach works if your PC doesn't support Bluetooth, or if the Bluetooth connection drops for some reason. Connect the audio cable to the blue line-in jack on the back of the computer. Then, connect the phone to the other end of the audio cable. If your phone does not have an audio jack, use the adapter on the USB-C or Lightning port. If your PC detects that you connected a new line-in device, it might open the audio settings automatically. If not, right-click on the volume icon on the taskbar next to the clock and select \u003Ccode>Sounds\u003C/code>. Navigate to the \u003Ccode>Input\u003C/code> tab and double click on the Line-In entry (the one with a cable icon). Navigate to the Monitor tab and select the check box for \"Use this device as a playback source\". This will tell windows it should play all sounds received through this input directly to the speakers. Usually this is used to monitor microphones but it works for this use case too. You should now hear any sound from your phone through your PC headphones or speakers. Make sure you turn this checkbox off when you disconnect your phone. Otherwise you might hear a crackle or other sounds when the loose cable gets touched.\u003C/p>\n\u003Cp>\u003Cem>Photo by Lisa Fotios from Pexels\u003C/em>\u003C/p>","blog/2022-02-phone-audio-to-pc","be57f2df-d58f-4b79-8a51-e20d482f46cf","How to Listen to Phone Audio on PC",true,["Date","2022-02-12T00:00:00.000Z"],"Learn how to connect your phone audio to your PC over wire or Bluetooth.","/assets/2022-02-phone-audio-to-pc.jpg",[12,13,14,15,16],"how-to","audio","windows","bluetooth","software","\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>",[13,15,12,16,14],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":13}],"uses":{"params":["slug"]}}]} diff --git a/tags/bash.html b/tags/bash.html new file mode 100644 index 00000000..1d9c3e55 --- /dev/null +++ b/tags/bash.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️bash - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: bash

+ +

Getting the Absolute Path of a Remote Directory in Ansible

+ 9/20/2023 +
Getting the Absolute Path of a Remote Directory in Ansible +

There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/bash/__data.json b/tags/bash/__data.json new file mode 100644 index 00000000..dae83f17 --- /dev/null +++ b/tags/bash/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_image_txt":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>\n\u003Cp>Deleting with elevated permission on the command line is easy: The command \u003Ccode>sudo rm -rf ~/docker/myservice\u003C/code> performs the \u003Ccode>rm\u003C/code> operation as the root user. In bash, this will delete the \u003Ccode>docker/myservice\u003C/code> folder in the user's home directory, but when doing the equivalent in Ansible, this won't work!\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\"># This does not work!\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"~/docker/myservice\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>This code will try to delete the file \u003Ccode>/user/root/docker/myservice\u003C/code>, which is not what we wanted.\u003C/p>\n\u003Cp>The bash version works because the shell first resolves the tilde in the argument to the current users' directory before calling the sudo command. In Ansible, we first switch to the root user and only then the tilde is resolved: this time to the home directory of the root user.\u003C/p>\n\u003Cp>To circumvent this, we can manually resolve the path to an absolute path. Unfortunately, I have not found a straightforward way to do this in Ansible, however the bash command \u003Ccode>readlink -f <path>\u003C/code> does exactly this. To use it in Ansible, we can use the following configuration:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\">- name: Get absolute folder path\n ansible.builtin.command:\n cmd: \"readlink -f ~/docker/myservice\"\n register: folder_abs\n changed_when: False\n\n- name: Debug\n debug:\n msg: \"{{folder_abs.stdout}}\" # prints /user/tim/docker/myservice\n\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"{{folder_abs.stdout}}\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>With this Ansible script, we manually resolve the absolute path and use it to delete the folder using root permissions. If you know of an easier way to resolve to an absolute path, please let me know!\u003C/p>","blog/2023-09-20-ansible-absolute-path","ad58acaf-56b0-4bcf-9b72-d6c054fc48d4",["Date","2023-09-20T21:39:13.000Z"],["Date","2023-09-20T20:22:35.634Z"],null,"Getting the Absolute Path of a Remote Directory in Ansible",true,"There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.","https://media.tiim.ch/3c1246e4-3201-4df6-af87-6aa4ab98800e.webp","(stable doodle) server room, neon, cables",[15,16,17,18],"dev","ansible","linux","bash","\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>",[16,18,15,17],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":18}],"uses":{"params":["slug"]}}]} diff --git a/tags/bluetooth.html b/tags/bluetooth.html new file mode 100644 index 00000000..38ec6542 --- /dev/null +++ b/tags/bluetooth.html @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️bluetooth - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: bluetooth

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/bluetooth/__data.json b/tags/bluetooth/__data.json new file mode 100644 index 00000000..6700acc4 --- /dev/null +++ b/tags/bluetooth/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":23},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"date":8,"description":9,"cover_image":10,"content_tags":11,"abstract":17,"tags":18,"links":-1,"type":19,"folder":20,"comments":21,"latestComment":22},"\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>\n\u003Cp>\u003Cem>TLDR\u003C/em>:\u003C/p>\n\u003Cul>\n\u003Cli>Either use Bluetooth Audio Receiver from the Microsoft Store to connect you phone via Bluetooth,\u003C/li>\n\u003Cli>Or use an audio cable to connect the phone to the \"line-in\" on your PC.\u003C/li>\n\u003C/ul>\n\u003Ch2>Bluetooth (recommended)\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>: A PC with integrated Bluetooth or a Bluetooth dongle.\u003C/p>\n\u003Cp>I recommend this approach more than the wired one because it is way less effort, you don't have to deal with a USB or lightning to audio dongle and in my opinion it is more reliable.\u003C/p>\n\u003Cp>Pair your phone with your PC as normal, by opening the Bluetooth settings on your phone and on the PC and wait for the devices to show up. When you successfully paired the phone once you will not have to do this again. Now you need an app that will tell the phone that it can use the PC as a wireless speaker. The only app I found that will do this is the \u003Ca href=\"https://www.microsoft.com/de-de/p/bluetooth-audio-receiver/9n9wclwdqs5j\" rel=\"nofollow noopener noreferrer\">Bluetooth Audio Receiver\u003C/a> app from the Windows Store. Install and and open it. You should see your phone on the list of Bluetooth devices on the app. Select it and click on the \u003Ccode>Open Connection\u003C/code> button. It might take a moment but after it connected, you should hear all sounds from your phone on your PC.\u003C/p>\n\u003Ch2>Wired\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>:\u003C/p>\n\u003Cul>\n\u003Cli>Male-to-Male audio cable (3.5mm audio jack).\u003C/li>\n\u003Cli>A line-in port on your PC (usually blue audio jack on the back)\u003C/li>\n\u003Cli>USB-C to audio jack adapter (Optional)\u003C/li>\n\u003Cli>Lighting to audio jack adapter (Optional)\u003C/li>\n\u003C/ul>\n\u003Cp>This approach works if your PC doesn't support Bluetooth, or if the Bluetooth connection drops for some reason. Connect the audio cable to the blue line-in jack on the back of the computer. Then, connect the phone to the other end of the audio cable. If your phone does not have an audio jack, use the adapter on the USB-C or Lightning port. If your PC detects that you connected a new line-in device, it might open the audio settings automatically. If not, right-click on the volume icon on the taskbar next to the clock and select \u003Ccode>Sounds\u003C/code>. Navigate to the \u003Ccode>Input\u003C/code> tab and double click on the Line-In entry (the one with a cable icon). Navigate to the Monitor tab and select the check box for \"Use this device as a playback source\". This will tell windows it should play all sounds received through this input directly to the speakers. Usually this is used to monitor microphones but it works for this use case too. You should now hear any sound from your phone through your PC headphones or speakers. Make sure you turn this checkbox off when you disconnect your phone. Otherwise you might hear a crackle or other sounds when the loose cable gets touched.\u003C/p>\n\u003Cp>\u003Cem>Photo by Lisa Fotios from Pexels\u003C/em>\u003C/p>","blog/2022-02-phone-audio-to-pc","be57f2df-d58f-4b79-8a51-e20d482f46cf","How to Listen to Phone Audio on PC",true,["Date","2022-02-12T00:00:00.000Z"],"Learn how to connect your phone audio to your PC over wire or Bluetooth.","/assets/2022-02-phone-audio-to-pc.jpg",[12,13,14,15,16],"how-to","audio","windows","bluetooth","software","\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>",[13,15,12,16,14],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":15}],"uses":{"params":["slug"]}}]} diff --git a/tags/caddy.html b/tags/caddy.html new file mode 100644 index 00000000..e359675a --- /dev/null +++ b/tags/caddy.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️caddy - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: caddy

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/caddy/__data.json b/tags/caddy/__data.json new file mode 100644 index 00000000..991f7130 --- /dev/null +++ b/tags/caddy/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":38},[2],{"html":3,"slug":4,"name":5,"date":6,"content_tags":7,"like_of":11,"raw_data":13,"tags":31,"links":-1,"published":32,"type":33,"cover_image":-1,"description":34,"folder":35,"comments":36,"latestComment":37},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==\">https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/12/mte4nd","How Caddy 2 works, a deep dive into the source",["Date","2022-12-04T13:08:00.000Z"],[8,9,10],"caddy","golang","dev",{"url":12},"https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==",{"items":14,"rels":29,"relurls":30},[15],{"id":16,"value":16,"html":16,"type":17,"properties":19,"shape":16,"coords":16,"children":28},"",[18],"h-entry",{"category":20,"like-of":21,"name":22,"post-status":24,"published":26},[8,9,10],[12],[23]," How Caddy 2 works, a deep dive into the source",[25],"published",[27],"2022-12-04T14:08:00+0100",[],{},{},[8,10,9],true,"like","👍 Liked: https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==","mf2",[],"2023-09-02T19:26:59Z",{"tag":8}],"uses":{"params":["slug"]}}]} diff --git a/tags/cdn.html b/tags/cdn.html new file mode 100644 index 00000000..1f518a54 --- /dev/null +++ b/tags/cdn.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️cdn - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: cdn

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/cdn/__data.json b/tags/cdn/__data.json new file mode 100644 index 00000000..5c8bddc1 --- /dev/null +++ b/tags/cdn/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":41},[2],{"html":3,"slug":4,"uuid":5,"date":6,"aliases":7,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_caption":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":25,"folder":26,"comments":27,"latestComment":40},"\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\" rel=\"nofollow noopener noreferrer\">TOS\u003C/a>:\u003C/p>\n\u003Cblockquote>\n\u003Cp>[...] Also, don't use Imgur to host image libraries you link to from elsewhere, content for your website, advertising, avatars, or anything else that turns us into your content delivery network.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Finally when I started \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">indie-webifying my website\u003C/a>, and was implementing the micropub protocol (which I will blog about at a later time), I decided that it was at the time to host the images on a platform that was meant to do that. I looked at a few storage providers such as cloudinary and S3 based object storage and landed on \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">Storj.io\u003C/a>, mostly because of the generous free tier, which should suffice for this little blog for quite a while.\u003C/p>\n\u003Cp>One thing that bothered me slightly was that all storage providers I looked at charge for traffic. It's not the fact that it's an additional expense (if your not in the free tier anymore) that bothers me, but the fact that I don't have any control over how much this will cost me. In all likelihood this will never cost me anything since this blog has not much traffic, but if a post were to go viral (one can dream...), this could result in a surprise bill at the end of the month.\u003C/p>\n\u003Cp>To help with the traffic costs I decided to try to use the free CDN functionality of Cloudflare to reduce the traffic to Storj. In this blog post I will describe how I did that.\u003C/p>\n\u003Ch2>Is this the right solution for you?\u003C/h2>\n\u003Cp>If you are in a similar situation as me, and just want to have somewhere to host your images for a personal website or to share images or screenshots as links while still having control over all your data, this could be a good solution.\u003C/p>\n\u003Cp>If you want to build a robust image pipeline with resizing and image optimization, or you are building an enterprise website this is probably not the right way. You should take a look at cloudinary or one of the big cloud providers.\u003C/p>\n\u003Ch2>Prerequisites\u003C/h2>\n\u003Cp>To use Cloudflare as a CDN, you need to have Cloudflare setup as your DNS host for the domain you want to serve the images from. Even if you just want to use a subdomain like \u003Ccode>media.example.com\u003C/code>, the whole \u003Ccode>example.com\u003C/code> domain needs to be on cloudflare. For me this was not much of an issue, I followed the instructions from cloudflare and pointed the nameserver of my domain to cloudflare. Although I did have an issue during the migration, which resulted in my website being down for two hours. But I'm pretty sure this was caused by my previous nameserver provider.\u003C/p>\n\u003Ch2>Setting up Storj & Cloudflare\u003C/h2>\n\u003Cp>I assume you already have an account at \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">storj.io\u003C/a>. The next step is creating a bucket for your images. A bucket is just a place for your files and folders to live in storj, just like in any other S3 compatible storage provider. (Actually there are no folders in storj and other S3 services, the folders are just prefixes of the filenames). When creating a bucket, make sure you save the passphrase securely, such as in your password manager. Whenever storj asks you for the passphrase, make sure you don't let storj generate a new one! Every new passphrase will create access to a new bucket.\u003C/p>\n\u003Cp>The next step is \u003Ca href=\"https://docs.storj.io/dcs/downloads/download-uplink-cli\" rel=\"nofollow noopener noreferrer\">installing the uplink cli\u003C/a>. Follow the quick start tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object\" rel=\"nofollow noopener noreferrer\">get an access grant\u003C/a>. Remember to use the same passphrase from above. Now follow the next quickstart tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object/set-up-uplink-cli\" rel=\"nofollow noopener noreferrer\">add the bucket to the uplink cli\u003C/a>. The file \u003Ccode>accessgrant.txt\u003C/code> in the tutorial only contains the access-grant string that you got from the last step.\u003C/p>\n\u003Cp>Finally we want to share the bucket so the images can be accessed from the web. For this you can run the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">uplink share --dns <domain> sj://<bucket>/<prefix> --not-after=none\n\u003C/code>\u003C/pre>\n\u003Cp>Replace \u003Ccode><domain>\u003C/code> with the domain you want to serve the images from. In my case I use \u003Ccode>media.tiim.ch\u003C/code>. Then replace \u003Ccode><bucket>\u003C/code> with the name of your bucket and \u003Ccode><prefix>\u003C/code> with the prefix.\u003C/p>\n\u003Cp>As mentioned above, you can think of a prefix as a folder. If you use for example \u003Ccode>media-site1\u003C/code> as a prefix, then every file in the \"folder\" \u003Ccode>media-site1\u003C/code> will be shared. This means you can use multiple prefixes to serve files for multiple websites in the same bucket.\u003C/p>\n\u003Cp>You will get the following output:\u003C/p>\n\u003Cpre>\u003Ccode>[...]\n=========== DNS INFO =====================================================================\nRemember to update the $ORIGIN with your domain name. You may also change the $TTL.\n$ORIGIN example.com.\n$TTL 3600\nmedia.example.com IN CNAME link.storjshare.io.\ntxt-media.example.com IN TXT storj-root:mybucket/myprefix\ntxt-media.example.com IN TXT storj-access:totallyrandomstringofnonsens\n\u003C/code>\u003C/pre>\n\u003Cp>Create the DNS entries in Cloudflare with the values printed in the last three lines. Make sure you enable the proxy setting when entering the CNAME entry to enable Cloudflares CDN service.\u003C/p>\n\u003Cp>And that's it. All files you put in the bucket with the correct prefix are now available under your domain! :)\u003C/p>\n\u003Cp>If this blog post helped you, or you have some issues or thoughts on this, leave a comment via the comment box below or via webmention.\u003C/p>","blog/2022-12-storj-cloudflare-image-hosting","6d5a964d-328e-43d7-9189-40280b012074",["Date","2022-12-03T13:37:33.000Z"],[8],null,"Hosting Images with Storj and Cloudflare",true,"Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.","https://media.tiim.ch/d280fad4-632a-4b5a-b6b2-6a5c0026b61c.jpg","Image generated by Dall-E: travel postcards scattered on grass, top down view, photoreal",[15,16,17,18],"CDN","IndieWeb","Cloudflare","Storj","\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\">TOS\u003C/a>:\u003C/p>",[21,22,23,24],"cdn","cloudflare","indieweb","storj","article","blog",[28,35],{"id":29,"type":30,"replyTo":31,"timestamp":32,"page":4,"url":33,"content":31,"name":34},"edeb5ddb-357a-41cf-b2b2-704df571d70c","webmention","","2023-01-11T06:03:38Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/1570290779419017216","Laura Forster",{"id":36,"type":30,"replyTo":31,"timestamp":37,"page":4,"url":38,"content":31,"name":39},"2331980c-acee-4549-a260-61b4119c63a9","2022-12-05T06:12:20Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/275384865","kevin","2023-09-02T19:26:59Z",{"tag":21}],"uses":{"params":["slug"]}}]} diff --git a/tags/cgo.html b/tags/cgo.html new file mode 100644 index 00000000..ade9835f --- /dev/null +++ b/tags/cgo.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️cgo - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: cgo

+ +

"no such file or directory" after enabling CGO in Docker

+ 1/24/2023 +
+

Quick fix for the "no such file or directory" error after enabling CGO, when running in a scratch docker image.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/cgo/__data.json b/tags/cgo/__data.json new file mode 100644 index 00000000..c92ee6f7 --- /dev/null +++ b/tags/cgo/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":24},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":-1,"cover_image_txt":13,"content_tags":14,"abstract":18,"tags":19,"links":-1,"type":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message \u003Ccode>exec /app/indiego: no such file or directory\u003C/code>. I had removed the \u003Ccode>CGO_ENABLE=0\u003C/code> variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the \u003Ccode>scratch\u003C/code> image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.\u003C/p>\n\u003Cp>To include libc into the container, I simply changed the base image from \u003Ccode>scratch\u003C/code> to \u003Ccode>alpine\u003C/code>, which includes libc. This makes the image slightly larger but this seemed way easier than trying to include libc directly.\u003C/p>\n\u003Cp>As a bonus I got to delete the \u003Ccode>/usr/share/zoneinfo\u003C/code> and \u003Ccode>ca-certificates.crt\u003C/code> files, and rely on those provided by alpine.\u003C/p>\n\u003Cp>You can see the commit to IndieGo \u003Ca href=\"https://github.com/Tiim/IndieGo/commit/63968814de7e39f295386bf398b583aa8bf0411c\" rel=\"nofollow noopener noreferrer\">here\u003C/a>.\u003C/p>","blog/2023-01-24-no-such-file-or-directory-cgo","dd580343-9e0f-4754-93dd-25667e6b5859",["Date","2023-01-24T00:00:00.000Z"],["Date","2023-01-24T20:54:11.330Z"],[9],null,"\"no such file or directory\" after enabling CGO in Docker",true,"Quick fix for the \"no such file or directory\" error after enabling CGO, when running in a scratch docker image.","",[15,16,17],"go","cgo","docker","\u003Cp>Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message \u003Ccode>exec /app/indiego: no such file or directory\u003C/code>. I had removed the \u003Ccode>CGO_ENABLE=0\u003C/code> variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the \u003Ccode>scratch\u003C/code> image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.\u003C/p>",[16,17,15],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":16}],"uses":{"params":["slug"]}}]} diff --git a/tags/cheatsheet.html b/tags/cheatsheet.html new file mode 100644 index 00000000..d65e25b0 --- /dev/null +++ b/tags/cheatsheet.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️cheatsheet - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: cheatsheet

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/cheatsheet/__data.json b/tags/cheatsheet/__data.json new file mode 100644 index 00000000..4270266e --- /dev/null +++ b/tags/cheatsheet/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":24},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":15,"abstract":17,"tags":18,"type":19,"cover_image":-1,"description":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.\u003C/p>\n\u003Cp>I currently do not update this repo anymore because i moved all cheat sheets into my \u003Ca href=\"https://tiim.ch/tags/obsidian\" rel=\"nofollow noopener noreferrer\">Obsidian Vault\u003C/a>. I am looking into a way to keep those two repositories in sync in the future.\u003C/p>","projects/my-cheatsheets","c6d2511a-5d5c-4957-b730-5536f949a1b4","My Cheat Sheets",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14],"markdown","dev","cheatsheet",[16],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/my-cheatsheets\" rel=\"nofollow noopener noreferrer\">Cheat Sheets on Github\u003C/a>\u003C/p>","\u003Cp>My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.\u003C/p>",[14,13,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":14}],"uses":{"params":["slug"]}}]} diff --git a/tags/cli.html b/tags/cli.html new file mode 100644 index 00000000..6e951e74 --- /dev/null +++ b/tags/cli.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️cli - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: cli

+ +

Pomo 🍅

+ 8/3/2023 +
+

I created pomo as a way to keep me focused for working on my masters thesis, and at the same time +allowed me to learn the rust programming language.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/cli/__data.json b/tags/cli/__data.json new file mode 100644 index 00000000..4e9c52a9 --- /dev/null +++ b/tags/cli/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":24},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":15,"abstract":17,"tags":18,"type":19,"cover_image":-1,"description":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>\n\u003Cp>Pomo is a simple pomodoro timer. It allows you to either specify the number of repetitions (pomodori), the duration of the pomodori and the duration of the breaks, or\nyou can stecify an end time, and let pomo calculate the durations and repetitions.\u003C/p>\n\u003Cp>Pomo runs as a cli tool and stores the current state in a json file. All pomo executions excep \u003Ccode>pomo watch\u003C/code> just\nmodify this json file and terminate. The watch command displays the current pomodoro timer, optionally writes the timer to a text file,\nand watches for changes of the json file.\u003C/p>","projects/pomo","bfa1a7fa-b8a3-469f-b8e8-727ab705cb93","Pomo 🍅",["Date","2023-08-03T11:03:00.000Z"],null,"Projects",true,[12,13,14],"rust","cli","dev",[16],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/pomo\" rel=\"nofollow noopener noreferrer\">pomo Github\u003C/a>\u003C/p>","\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>",[13,14,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":13}],"uses":{"params":["slug"]}}]} diff --git a/tags/cloudflare.html b/tags/cloudflare.html new file mode 100644 index 00000000..86d40fe2 --- /dev/null +++ b/tags/cloudflare.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️cloudflare - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: cloudflare

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/cloudflare/__data.json b/tags/cloudflare/__data.json new file mode 100644 index 00000000..6a9a51e9 --- /dev/null +++ b/tags/cloudflare/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":41},[2],{"html":3,"slug":4,"uuid":5,"date":6,"aliases":7,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_caption":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":25,"folder":26,"comments":27,"latestComment":40},"\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\" rel=\"nofollow noopener noreferrer\">TOS\u003C/a>:\u003C/p>\n\u003Cblockquote>\n\u003Cp>[...] Also, don't use Imgur to host image libraries you link to from elsewhere, content for your website, advertising, avatars, or anything else that turns us into your content delivery network.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Finally when I started \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">indie-webifying my website\u003C/a>, and was implementing the micropub protocol (which I will blog about at a later time), I decided that it was at the time to host the images on a platform that was meant to do that. I looked at a few storage providers such as cloudinary and S3 based object storage and landed on \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">Storj.io\u003C/a>, mostly because of the generous free tier, which should suffice for this little blog for quite a while.\u003C/p>\n\u003Cp>One thing that bothered me slightly was that all storage providers I looked at charge for traffic. It's not the fact that it's an additional expense (if your not in the free tier anymore) that bothers me, but the fact that I don't have any control over how much this will cost me. In all likelihood this will never cost me anything since this blog has not much traffic, but if a post were to go viral (one can dream...), this could result in a surprise bill at the end of the month.\u003C/p>\n\u003Cp>To help with the traffic costs I decided to try to use the free CDN functionality of Cloudflare to reduce the traffic to Storj. In this blog post I will describe how I did that.\u003C/p>\n\u003Ch2>Is this the right solution for you?\u003C/h2>\n\u003Cp>If you are in a similar situation as me, and just want to have somewhere to host your images for a personal website or to share images or screenshots as links while still having control over all your data, this could be a good solution.\u003C/p>\n\u003Cp>If you want to build a robust image pipeline with resizing and image optimization, or you are building an enterprise website this is probably not the right way. You should take a look at cloudinary or one of the big cloud providers.\u003C/p>\n\u003Ch2>Prerequisites\u003C/h2>\n\u003Cp>To use Cloudflare as a CDN, you need to have Cloudflare setup as your DNS host for the domain you want to serve the images from. Even if you just want to use a subdomain like \u003Ccode>media.example.com\u003C/code>, the whole \u003Ccode>example.com\u003C/code> domain needs to be on cloudflare. For me this was not much of an issue, I followed the instructions from cloudflare and pointed the nameserver of my domain to cloudflare. Although I did have an issue during the migration, which resulted in my website being down for two hours. But I'm pretty sure this was caused by my previous nameserver provider.\u003C/p>\n\u003Ch2>Setting up Storj & Cloudflare\u003C/h2>\n\u003Cp>I assume you already have an account at \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">storj.io\u003C/a>. The next step is creating a bucket for your images. A bucket is just a place for your files and folders to live in storj, just like in any other S3 compatible storage provider. (Actually there are no folders in storj and other S3 services, the folders are just prefixes of the filenames). When creating a bucket, make sure you save the passphrase securely, such as in your password manager. Whenever storj asks you for the passphrase, make sure you don't let storj generate a new one! Every new passphrase will create access to a new bucket.\u003C/p>\n\u003Cp>The next step is \u003Ca href=\"https://docs.storj.io/dcs/downloads/download-uplink-cli\" rel=\"nofollow noopener noreferrer\">installing the uplink cli\u003C/a>. Follow the quick start tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object\" rel=\"nofollow noopener noreferrer\">get an access grant\u003C/a>. Remember to use the same passphrase from above. Now follow the next quickstart tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object/set-up-uplink-cli\" rel=\"nofollow noopener noreferrer\">add the bucket to the uplink cli\u003C/a>. The file \u003Ccode>accessgrant.txt\u003C/code> in the tutorial only contains the access-grant string that you got from the last step.\u003C/p>\n\u003Cp>Finally we want to share the bucket so the images can be accessed from the web. For this you can run the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">uplink share --dns <domain> sj://<bucket>/<prefix> --not-after=none\n\u003C/code>\u003C/pre>\n\u003Cp>Replace \u003Ccode><domain>\u003C/code> with the domain you want to serve the images from. In my case I use \u003Ccode>media.tiim.ch\u003C/code>. Then replace \u003Ccode><bucket>\u003C/code> with the name of your bucket and \u003Ccode><prefix>\u003C/code> with the prefix.\u003C/p>\n\u003Cp>As mentioned above, you can think of a prefix as a folder. If you use for example \u003Ccode>media-site1\u003C/code> as a prefix, then every file in the \"folder\" \u003Ccode>media-site1\u003C/code> will be shared. This means you can use multiple prefixes to serve files for multiple websites in the same bucket.\u003C/p>\n\u003Cp>You will get the following output:\u003C/p>\n\u003Cpre>\u003Ccode>[...]\n=========== DNS INFO =====================================================================\nRemember to update the $ORIGIN with your domain name. You may also change the $TTL.\n$ORIGIN example.com.\n$TTL 3600\nmedia.example.com IN CNAME link.storjshare.io.\ntxt-media.example.com IN TXT storj-root:mybucket/myprefix\ntxt-media.example.com IN TXT storj-access:totallyrandomstringofnonsens\n\u003C/code>\u003C/pre>\n\u003Cp>Create the DNS entries in Cloudflare with the values printed in the last three lines. Make sure you enable the proxy setting when entering the CNAME entry to enable Cloudflares CDN service.\u003C/p>\n\u003Cp>And that's it. All files you put in the bucket with the correct prefix are now available under your domain! :)\u003C/p>\n\u003Cp>If this blog post helped you, or you have some issues or thoughts on this, leave a comment via the comment box below or via webmention.\u003C/p>","blog/2022-12-storj-cloudflare-image-hosting","6d5a964d-328e-43d7-9189-40280b012074",["Date","2022-12-03T13:37:33.000Z"],[8],null,"Hosting Images with Storj and Cloudflare",true,"Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.","https://media.tiim.ch/d280fad4-632a-4b5a-b6b2-6a5c0026b61c.jpg","Image generated by Dall-E: travel postcards scattered on grass, top down view, photoreal",[15,16,17,18],"CDN","IndieWeb","Cloudflare","Storj","\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\">TOS\u003C/a>:\u003C/p>",[21,22,23,24],"cdn","cloudflare","indieweb","storj","article","blog",[28,35],{"id":29,"type":30,"replyTo":31,"timestamp":32,"page":4,"url":33,"content":31,"name":34},"edeb5ddb-357a-41cf-b2b2-704df571d70c","webmention","","2023-01-11T06:03:38Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/1570290779419017216","Laura Forster",{"id":36,"type":30,"replyTo":31,"timestamp":37,"page":4,"url":38,"content":31,"name":39},"2331980c-acee-4549-a260-61b4119c63a9","2022-12-05T06:12:20Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/275384865","kevin","2023-09-02T19:26:59Z",{"tag":22}],"uses":{"params":["slug"]}}]} diff --git a/tags/dev.html b/tags/dev.html new file mode 100644 index 00000000..0e4e10dc --- /dev/null +++ b/tags/dev.html @@ -0,0 +1,355 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️dev - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: dev

+ +

Getting the Absolute Path of a Remote Directory in Ansible

+ 9/20/2023 +
Getting the Absolute Path of a Remote Directory in Ansible +

There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.

+
+
+ +

Automated Planning using Property-Directed Reachability with Seed Heuristics

+ 5/6/2023 +
Automated Planning using Property-Directed Reachability with Seed Heuristics +

Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.

+
+
+ +

You should be using RSS

+ 6/5/2022 +
You should be using RSS +

Decide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read.

+
+
+ +

How to set up an SSH Server on Windows with WSL

+ 3/2/2022 +
How to set up an SSH Server on Windows with WSL +

It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.

+
+
+ +

Modelling Git Operations as Planning Problems

+ 1/20/2021 +
Modelling Git Operations as Planning Problems +

Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.

+
+
+ +

Pomo 🍅

+ 8/3/2023 +
+

I created pomo as a way to keep me focused for working on my masters thesis, and at the same time +allowed me to learn the rust programming language.

+
+
+ +

Swim Club Birsfelden Website

+ 3/5/2022 +
+

The website of the "ScBirs" swim club. This website serves as the center of all information distribution for the swimclub.

+
+
+ +

TeamKit

+ 11/27/2022 +
+

TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/dev/__data.json b/tags/dev/__data.json new file mode 100644 index 00000000..1d887963 --- /dev/null +++ b/tags/dev/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":374},[2,25,42,66,102,120,135,153,171,192,210,226,240,257,271,287,301,315,328,343],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_image_txt":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>\n\u003Cp>Deleting with elevated permission on the command line is easy: The command \u003Ccode>sudo rm -rf ~/docker/myservice\u003C/code> performs the \u003Ccode>rm\u003C/code> operation as the root user. In bash, this will delete the \u003Ccode>docker/myservice\u003C/code> folder in the user's home directory, but when doing the equivalent in Ansible, this won't work!\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\"># This does not work!\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"~/docker/myservice\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>This code will try to delete the file \u003Ccode>/user/root/docker/myservice\u003C/code>, which is not what we wanted.\u003C/p>\n\u003Cp>The bash version works because the shell first resolves the tilde in the argument to the current users' directory before calling the sudo command. In Ansible, we first switch to the root user and only then the tilde is resolved: this time to the home directory of the root user.\u003C/p>\n\u003Cp>To circumvent this, we can manually resolve the path to an absolute path. Unfortunately, I have not found a straightforward way to do this in Ansible, however the bash command \u003Ccode>readlink -f <path>\u003C/code> does exactly this. To use it in Ansible, we can use the following configuration:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\">- name: Get absolute folder path\n ansible.builtin.command:\n cmd: \"readlink -f ~/docker/myservice\"\n register: folder_abs\n changed_when: False\n\n- name: Debug\n debug:\n msg: \"{{folder_abs.stdout}}\" # prints /user/tim/docker/myservice\n\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"{{folder_abs.stdout}}\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>With this Ansible script, we manually resolve the absolute path and use it to delete the folder using root permissions. If you know of an easier way to resolve to an absolute path, please let me know!\u003C/p>","blog/2023-09-20-ansible-absolute-path","ad58acaf-56b0-4bcf-9b72-d6c054fc48d4",["Date","2023-09-20T21:39:13.000Z"],["Date","2023-09-20T20:22:35.634Z"],null,"Getting the Absolute Path of a Remote Directory in Ansible",true,"There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.","https://media.tiim.ch/3c1246e4-3201-4df6-af87-6aa4ab98800e.webp","(stable doodle) server room, neon, cables",[15,16,17,18],"dev","ansible","linux","bash","\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>",[16,18,15,17],"article","blog",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"date":29,"created":30,"aliases":8,"title":31,"published":10,"modified":32,"description":33,"cover_image":34,"cover_image_txt":8,"content_tags":35,"abstract":39,"tags":40,"links":-1,"type":21,"folder":22,"comments":41,"latestComment":24},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>\n\u003Cp>\u003Ca href=\"https://www.researchgate.net/publication/373994137_Automated_Planning_using_Property-Directed_Reachability_with_Seed_Heuristics\" rel=\"nofollow noopener noreferrer\">Download PDF\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-bibtex\">@phdthesis{bachmann2023,\n author = {Bachmann, Tim},\n year = {2023},\n month = {05},\n title = {Automated Planning using Property-Directed Reachability with Seed Heuristics},\n doi = {10.13140/RG.2.2.11456.30727},\n type = {Master's Thesis},\n school = {University of Basel}\n}\n\u003C/code>\u003C/pre>","blog/2023-05-06-pdr-with-seed-heuristics","111e68c4-0285-4f21-ab36-4c1ce1989da1",["Date","2023-05-06T11:15:53.000Z"],["Date","2023-05-06T11:15:53.000Z"],"Automated Planning using Property-Directed Reachability with Seed Heuristics",["Date","2023-09-18T13:32:00.000Z"],"Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.","https://media.tiim.ch/023c1722-ac3d-45fd-b66c-9ff319dfc180.webp",[15,36,37,38],"planning-system","pdr","heuristic","\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>",[15,38,37,36],[],{"html":43,"slug":44,"uuid":45,"date":46,"created":47,"aliases":48,"title":49,"published":10,"modified":8,"description":50,"cover_image":51,"content_tags":52,"abstract":55,"tags":56,"links":-1,"type":21,"folder":22,"comments":57,"latestComment":24},"\u003Cp>I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.\u003C/p>\n\u003Cp>But there is another way! By subscribing to RSS feeds you are in control of what you are shown. Most websites, blogs, news sites and even social media sites provide RSS feeds to subscribe to. You get only the articles, videos or audio content you are subscribed to, without any algorithm messing with your attention.\u003C/p>\n\u003Ch2>But what exactly is an RSS feed?\u003C/h2>\n\u003Cp>RSS stands for \"Really Simple Syndication\", and it is a protocol for a website to provide a list of content. It is an old protocol, the first version was introduced in 1999, but it might be more useful nowadays than ever.\nIf you listen to podcasts, you are already familiar with RSS feeds: a podcast is an RSS feed which links to audio files instead of online articles.\nAn RSS feed is just an XML document which contains information about the feed and a list of content.\nWhen you use an app to subscribe to an RSS feed, this app will just save the URL to the XML document and load it regularly to check if new content is available. You are completely in control of how often the feed is refreshed and what feeds you want to subscribe to. Some RSS reader apps also allow you to specify some rules for example about if you should be notified, based on the feed, the content or the tags.\u003C/p>\n\u003Ch2>How to subscribe to a feed?\u003C/h2>\n\u003Cp>Since an RSS feed is just an XML document, you don't \u003Cem>technically\u003C/em> have to subscribe to a feed to read it, you \u003Cem>could\u003C/em> just open the document and read the XML. But that would be painful. Luckily there are several plugins, apps and services that allow you to easily subscribe to and read RSS feeds.\u003C/p>\n\u003Cp>If you want to start using RSS and are not sure if you will take the time to open a dedicated app, I would recommend using an RSS plugin for another software that you are using regularly. For example, the \u003Ca href=\"https://thunderbird.net/\" rel=\"nofollow noopener noreferrer\">Thunderbird\u003C/a> email client already has built-in RSS support. If you want to read to the feeds directly inside of your browser, you can use the \u003Ca href=\"https://nodetics.com/feedbro/\" rel=\"nofollow noopener noreferrer\">feedbro\u003C/a> extension for Chrome, Firefox, and other Chromium-based browsers. I use the \u003Ca href=\"https://vivaldi.com\" rel=\"nofollow noopener noreferrer\">Vivaldi\u003C/a> browser which comes with an integrated RSS feed reader.\u003C/p>\n\u003Ch2>What if there is no RSS feed?\u003C/h2>\n\u003Cp>Unfortunately not every website offers an RSS feed. Although it might be worth it to hunt for them. Some websites offer an RSS feed but do not link to it anywhere.\nIf there is no feed, but a newsletter is offered, the service \"\u003Ca href=\"https://kill-the-newsletter.com\" rel=\"nofollow noopener noreferrer\">Kill The Newsletter\u003C/a>\" will provide you with email addresses and a corresponding RSS URL to convert any newsletter to a feed. Another service to consider is \u003Ca href=\"http://fetchrss.com\" rel=\"nofollow noopener noreferrer\">FetchRSS\u003C/a>. It turns any website into an RSS feed.\u003C/p>\n\u003Ch2>RSS Apps\u003C/h2>\n\u003Cp>If you want to have a dedicated app for your reading, you're in luck! There is a plethora of apps to choose from, all with different features and user interfaces.\nThere are three main types of apps: standalone apps, service-based apps, and self-hosted apps. Most apps are standalone, meaning they fetch the RSS feeds only when open, and don't sync to your other devices. The service-based apps rely on a cloud service which will fetch the feeds around the clock, even when all your devices are off. They can also send you a summary mail if you forget to check for some time and they can sync your subscriptions across all your devices. Unfortunately, most service-based apps only offer a limited experience for free. The last category is self-hosted apps. They are similar to the service based apps but instead of some company running the service, you have to provide a server for the service to run yourself.\u003C/p>\n\u003Cp>I use a standalone app, because I do not want to rely on a service, but I also don't want to go through the hassle of setting up a self-hosted solution.\u003C/p>\n\u003Cp>If you are still unsure what RSS app you could try out, I provided a list below. Make sure to add the \u003Ca href=\"https://tiim.ch/blog/rss.xml\" rel=\"nofollow noopener noreferrer\">RSS feed for my blog\u003C/a> (\u003Ccode>https://tiim.ch/blog/rss.xml\u003C/code>) to test it out 😉\u003C/p>\n\u003Ch3>Standalone Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://thunderbird.net\" rel=\"nofollow noopener noreferrer\">Thunderbird\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://ravenreader.app\" rel=\"nofollow noopener noreferrer\">RavenReader\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://netnewswire.com\" rel=\"nofollow noopener noreferrer\">NetNewsWire\u003C/a> (Free, Integration with Services possible)\u003C/li>\n\u003Cli>\u003Ca href=\"https://vivaldi.com\" rel=\"nofollow noopener noreferrer\">Vivaldi Browser\u003C/a> (Free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://nodetics.com/feedbro/\" rel=\"nofollow noopener noreferrer\">feedbro browser extension\u003C/a> (Free)\u003C/li>\n\u003C/ul>\n\u003Ch3>Service-Based Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://feedreader.com\" rel=\"nofollow noopener noreferrer\">FeedReader\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://feeder.co\" rel=\"nofollow noopener noreferrer\">Feeder\u003C/a> (Freemium, 10 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.inoreader.com/pricing\" rel=\"nofollow noopener noreferrer\">Inoreader\u003C/a> (Freemium, Ads and 150 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://newsblur.com\" rel=\"nofollow noopener noreferrer\">NewsBlur\u003C/a> (Freemium, 64 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.feedspot.com\" rel=\"nofollow noopener noreferrer\">Feedspot\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://feedly.com\" rel=\"nofollow noopener noreferrer\">Feedly\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://feedbin.com\" rel=\"nofollow noopener noreferrer\">Feedbin\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://theoldreader.com\" rel=\"nofollow noopener noreferrer\">TheOldReader\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://bazqux.com\" rel=\"nofollow noopener noreferrer\">BazQux\u003C/a>\u003C/li>\n\u003C/ul>\n\u003Ch3>Self-hosted Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://www.commafeed.com/\" rel=\"nofollow noopener noreferrer\">CommaFeed\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://freshrss.org\" rel=\"nofollow noopener noreferrer\">FreshRSS\u003C/a> (Free, OSS)\u003C/li>\n\u003C/ul>","blog/2022-06-use-rss","ee633c0d-d668-48e5-af57-aaae9d243099",["Date","2022-06-05T00:00:00.000Z"],["Date","2022-06-04T22:01:15.123Z"],[8],"You should be using RSS","Decide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read.","https://i.imgur.com/t3mebu7.png",[53,15,54],"rss","software","\u003Cp>I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.\u003C/p>",[15,53,54],[58],{"id":59,"type":60,"replyTo":61,"timestamp":62,"page":44,"url":63,"content":64,"name":65},"31ea6d40-e8c2-4ada-ac16-028a3036d2de","webmention","","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/oatm9/","Liked\nYou should be using RSS\nPost detailsDecide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read. https://i.imgur.com/t3mebu7.png","Jamie Tanna",{"html":67,"slug":68,"uuid":69,"title":70,"published":10,"date":71,"description":72,"cover_image":73,"content_tags":74,"abstract":78,"tags":79,"links":-1,"type":21,"folder":22,"comments":83,"latestComment":24},"\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\" rel=\"nofollow noopener noreferrer\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\" rel=\"nofollow noopener noreferrer\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\" rel=\"nofollow noopener noreferrer\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\" rel=\"nofollow noopener noreferrer\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>\n\u003Ch2>Installing the OpenSSH Server\u003C/h2>\n\u003Cp>Windows has been shipping with an OpenSSH client and server for a long time. They are not installed by default but can be activated either in the settings as described \u003Ca href=\"https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse\" rel=\"nofollow noopener noreferrer\">in the official docs\u003C/a> or with the following PowerShell commands.\u003C/p>\n\u003Cp>\u003Cstrong>You will need to start PowerShell as Administrator\u003C/strong>\u003C/p>\n\u003Cp>First, install the OpenSSH client and server.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0\nAdd-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0\n\u003C/code>\u003C/pre>\n\u003Cp>Enable the SSH service and make sure the firewall rule is configured:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\"># Enable the service\nStart-Service sshd\nSet-Service -Name sshd -StartupType 'Automatic'\n\n# Confirm the firewall rule is configured. It should be created automatically by setup. Run the following to verify\nif (!(Get-NetFirewallRule -Name \"OpenSSH-Server-In-TCP\" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {\n Write-Output \"Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it...\"\n New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22\n} else {\n Write-Output \"Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists.\"\n}\n\u003C/code>\u003C/pre>\n\u003Cp>Congratulations, you have installed the SSH server on your Windows machine. And all without manually setting up a background service or modifying config files.\u003C/p>\n\u003Ch2>Setting WSL as Default Shell\u003C/h2>\n\u003Cp>To directly boot into WSL when connecting, we need to change the default shell from \u003Ccode>cmd.exe\u003C/code> or \u003Ccode>PowerShell.exe\u003C/code> to \u003Ccode>bash.exe\u003C/code>, which in turn runs the default WSL distribution. This can be done with the PowerShell command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-ItemProperty -Path \"HKLM:\\SOFTWARE\\OpenSSH\" -Name DefaultShell -Value \"C:\\WINDOWS\\System32\\bash.exe\" -PropertyType String -Force\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: even though the shell is running on the Linux side, the SSH server is still on windows. This means you have to use to windows username to log in, and the SCP command copies files relative to the user directory on windows.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (non-Admin User)\u003C/h2>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: If the user account has Admin permissions, read the next chapter, otherwise continue reading.\u003C/p>\n\u003Cp>Create the folder \u003Ccode>.ssh\u003C/code> in the users home directory on windows: (e.g. \u003Ccode>C:\\Users\\<username>\\.ssh\u003C/code>). Run the following commands in PowerShell (not as administrator).\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path ~\\.ssh -ItmeType \"directory\"\nNew-Item -Path ~\\.ssh\\authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>The file \u003Ccode>.ssh\\autzorized_keys\u003C/code> will contain a list of all public keys that shall be allowed to connect to the SSH server.\u003C/p>\n\u003Cp>Copy the contents of your public key file (usually stored in \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>) to the \u003Ccode>authorized_keys\u003C/code> file. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (Admin User)\u003C/h2>\n\u003Cp>If the user is in the Administrators group, it is not possible to have the \u003Ccode>authorized_keys\u003C/code> file in the user directory for security purposes.\nInstead, it needs to be located on the following path \u003Ccode>%ProgramData%\\ssh\\administrators_authorized_keys\u003C/code>. A second requirement is that it is only accessible to Administrator users, to prevent a normal user from gaining admin permissions.\u003C/p>\n\u003Cp>To create the file start PowerShell as administrator and run the following command.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path $env:programdata\\ssh\\administrators_authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>This will create the file with the correct permissions. Now open the file and paste your public key into it. The public key should be located at \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Verifying everything works\u003C/h2>\n\u003Cp>Verify that you can SSH into your machine by running the following inside WSL:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">IP=$(cat /etc/resolv.conf | grep nameserver | cut -d \" \" -f2) # get the windows host ip address\nssh <user>@$IP\n\u003C/code>\u003C/pre>\n\u003Cp>Or from PowerShell and cmd:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ssh <user>@localhost\n\u003C/code>\u003C/pre>\n\u003Ch2>Drawbacks\u003C/h2>\n\u003Cp>There are some drawbacks to this approach. If you rely on some programs or scripts to work over SSH, this might not be the method for you. Most scripts expect a unix machine on the other end, or if they expect a windows machine they will most likely not be configured to deal with WSL.\u003C/p>\n\u003Cp>If you however just want to connect to your pc to copy some files or change some settings this approach is perfectly fine.\u003C/p>","blog/2022-03-ssh-windows-wsl","03b5a86c-5f4d-4086-9f5f-e1e46b4bcf58","How to set up an SSH Server on Windows with WSL",["Date","2022-03-02T00:00:00.000Z"],"It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.","/assets/2022-03-ssh-windows-wsl.png",[75,76,77,15],"SSH","WSL","Windows","\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>",[15,80,81,82],"ssh","windows","wsl",[84,91,97],{"id":85,"type":86,"replyTo":87,"timestamp":88,"page":68,"url":89,"content":90,"name":61},"20f2c526-7466-4c21-83ac-51750f278328","comment","f7c1891b-e97b-4030-863e-19344ed84d32","2022-09-20T14:59:17Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#20f2c526-7466-4c21-83ac-51750f278328","Hi Tim, no problem. I had this error in \"Event Viewer > Applications and Services Logs > OpenSSH > Admin\" and figure it out that sshd seems to search the Administrators groups to operate, literal name and not properly localized by region.\n\nerroid:2 user:SYSTEM details:\"sshd: error: unable to resolve group administrators\"\n\nMaybe is not all the non-english windows with this problem, but I have it but after created the group works like a charm.",{"id":87,"type":86,"replyTo":92,"timestamp":93,"page":68,"url":94,"content":95,"name":96},"5de01bc5-b7c7-4522-9dfe-c67f103d4c03","2022-09-20T12:58:29Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#f7c1891b-e97b-4030-863e-19344ed84d32","Hi FinderX\n\nThanks for the heads up. I don't remember having to create a new user group, even though my system language is German. Maybe I just forgot about that though.","Tim",{"id":92,"type":86,"replyTo":61,"timestamp":98,"page":68,"url":99,"content":100,"name":101},"2022-09-20T07:50:46Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#5de01bc5-b7c7-4522-9dfe-c67f103d4c03","Hi!\nI add some roundabouts about admin-users, if your windows ssh server system language is NOT english, you must create 'Administrators' group (without quotes) in your language equivalent of \"Users and Local Groups > Groups\", if your server is a DC (Domain Controller) create it in your language equivalent of \"Active Directories Users and Computers\".\n\nCreate the user group with name Administrators, description whatever, ex. \"Dummy group for sshd to work correctly.\", and in Members add your language equivalent of the user Administrator.\n\nThis is optional but I suggest you change these settings in \"%programdata%\\ssh\\sshd_config\" after you successfully copy your public key to the ssh server :\n\nStrictModes yes\nPubkeyAuthentication yes\nPasswordAuthentication no\n\nYou can see the log activity in your language equivalent of \"Applications and Services Logs > OpenSSH > Admin or Operational\"\n\nBest Regards.","FinderX",{"html":103,"slug":104,"uuid":105,"title":106,"published":10,"description":107,"content_tags":108,"date":112,"modified":113,"cover_image":114,"abstract":115,"tags":116,"links":-1,"type":21,"folder":22,"comments":119,"latestComment":24},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>\n\u003Cp>\u003Ca href=\"https://tiim.ch/assets/2021-01-20-Thesis.pdf\" rel=\"nofollow noopener noreferrer\">Download Thesis\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode>@thesis{bachmann2021,\n\ttitle = {Modelling Git Operations as Planning Problems},\n\tauthor = {Tim Bachmann},\n\tyear = {2021},\n month = {01},\n\ttype = {Bachelor's Thesis},\n\tschool = {University of Basel},\n\tdoi = {10.13140/RG.2.2.24784.17922}\n}\n\u003C/code>\u003C/pre>","blog/2021-01-git-operations-as-planning-problems","dc6e6d10-c460-4d3c-8fe2-4ce7535b4af1","Modelling Git Operations as Planning Problems","Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.",[109,110,111,15],"Git","PDDL","Planning-System",["Date","2021-01-20T00:00:00.000Z"],["Date","2023-09-18T11:41:51.000Z"],"/assets/2021-01-git-operations-as-planning-problems.png","\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>",[15,117,118,36],"git","pddl",[],{"html":121,"slug":122,"uuid":123,"title":124,"published":10,"description":125,"content_tags":126,"date":129,"cover_image":130,"abstract":131,"tags":132,"links":-1,"type":21,"folder":22,"comments":134,"latestComment":24},"\u003Ch2>The problem\u003C/h2>\n\u003Cp>Let's say you have a rest API with the following endpoint that returns all of the books in your database:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-rest\">GET /book/\n\u003C/code>\u003C/pre>\n\u003Cp>Your SQL query might look like something like this\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT *\nFROM books\n\u003C/code>\u003C/pre>\n\u003Cp>Sometimes you want to only list books, for example, from a specific author. How do we do this in SQL?\u003C/p>\n\u003Ch2>Naive solution: String concatenation ✂\u003C/h2>\n\u003Cp>One way would be to concatenate your sql query something like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const arguments = [];\nconst queryString = \"SELECT * FROM books WHERE true\";\nif (authorFilter != null) {\n queryString += \"AND author = ?\";\n arguments.push(authorFilter);\n}\ndb.query(queryString, arguments);\n\u003C/code>\u003C/pre>\n\u003Cp>I'm not much of a fan of manually concatenating strings.\u003C/p>\n\u003Ch2>The coalesce function 🌟\u003C/h2>\n\u003Cp>Most Databases have the function \u003Ccode>coalesce\u003C/code> which accepts a variable amount of arguments and returns the first argument that is not null.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- Examle\nSELECT coalesce(null, null, 'tiim.ch', null, '@TiimB') as example;\n\n-- Will return\n\nexample\n---------\ntiim.ch\n\u003C/code>\u003C/pre>\n\u003Cp>But how will this function help us?\u003C/p>\n\u003Ch2>Optional filters with the coalesce function\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT *\nFROM books\nWHERE\n author = coalesce(?, author);\n\u003C/code>\u003C/pre>\n\u003Cp>If the filter value is null the coalesce expression will resolve to \u003Ccode>author\u003C/code>\nand the comparison \u003Ccode>author = author\u003C/code> will be true.\u003C/p>\n\u003Cp>If on the other hand the value is set for example to Shakespeare then the author will be compared to Shakespeare.\u003C/p>\n\u003Cp>I came across this way to implement optional filters only recently. If you have a more idiomatic way to do this let me know please ✨\u003C/p>\n\u003Cp>If you liked this post please follow me on here or on Twitter under \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> 😎\u003C/p>","blog/2019-07-sql-optional-filters-coalesce","899fb73c-a78e-4cd9-b712-1886715b2d56","How to write optional filters in SQL","A simple way to filter by optional values in SQL with the COALESCE function.",[127,128,15],"SQL","quick-tip",["Date","2019-07-11T00:00:00.000Z"],"/assets/2019-07-sql-optional-filters-coalesce.png","\u003Cp>Let's say you have a rest API with the following endpoint that returns all of the books in your database:\u003C/p>",[15,128,133],"sql",[],{"html":136,"slug":137,"uuid":138,"title":139,"published":10,"description":140,"content_tags":141,"date":145,"cover_image":146,"abstract":147,"tags":148,"links":-1,"type":21,"folder":22,"comments":152,"latestComment":24},"\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\" rel=\"nofollow noopener noreferrer\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>\n\u003Cp>If you want to see an example of this method in action, go check out my \u003Ca href=\"https://tiimb.work\" rel=\"nofollow noopener noreferrer\">personal website\u003C/a> on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>\u003C/p>\n\u003Cp>I won't be explaining how to setup a Vue project. If you're looking for a Tutorial on that go check out the awesome \u003Ca href=\"https://vuejs.org/v2/guide/\" rel=\"nofollow noopener noreferrer\">Vue.js Guide\u003C/a>.\u003C/p>\n\u003Cp>So you have setup your awesome Vue project and want to host it on GitHub Pages. The way Muhammad explained it you would build the project using \u003Ccode>npm run build\u003C/code>, commit the \u003Ccode>dist/\u003C/code> folder along with your source files and point GitHub to the dist folder. This might get quite messy because you either have commit messages with the sole purpose of uploading the dist folder or you commit the code changes at the same time which makes it hard to find the relevant changes if you ever want to look at your commits again.\u003C/p>\n\u003Cp>So what can you do about this?\u003C/p>\n\u003Cp>Git to the rescue, let's use a branch that contains all the build files.\u003C/p>\n\u003Ch2>Step 1 - keeping our working branch clean 🛀\u003C/h2>\n\u003Cp>To make sure that the branch we are working from stays clean of any build files we are gonna add a \u003Ccode>.gitignore\u003C/code> file to the root.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># .gitignore\ndist/\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 2 - adding a second branch 🌳\u003C/h2>\n\u003Cp>We are not goint to branch off master like how we would do it if we were to modify our code with the intention to merge it back to the main branch. Instead we are gonna create a squeaky clean new branch that will only ever hold the dist files. After all we will not ever need to merge these two branches together.\u003C/p>\n\u003Cp>We do this by creating a new git repository inside the dist folder:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">cd dist/\ngit init\ngit add .\ngit commit -m 'Deploying my awesome vue app'\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 3 - deploying 🚚\u003C/h2>\n\u003Cp>We are gonna force push our new git repository to a branch on GitHub. This might go against git best practices but since we won't ever checkout this branch we don't have to worry about that.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">git push -f git@github.com:<username>/<repo>.git <branch>\n\u003C/code>\u003C/pre>\n\u003Cp>⚠️ Make sure you double or tripple check your destination branch! You don't want to accidentally overwrite your working branch. Using the branch \u003Ccode>gh-pages\u003C/code> will most likely be a good idea.\u003C/p>\n\u003Ch2>Step 4 - pointing GitHub to the right place 👈\u003C/h2>\n\u003Cp>Now we are almost done. The only thing left is telling GitHub where our assets live.\u003C/p>\n\u003Cp>Go to your repo, on the top right navigate to \u003Ccode>Settings\u003C/code> and scroll down to GitHub pages. Enable it and set your source branch to the branch you force pushed to, for example \u003Ccode>gh-pages\u003C/code>.\u003C/p>\n\u003Ch2>Step 5 - automating everything 😴\u003C/h2>\n\u003Cp>If you don't mind doing this whole process (Step 2 and 3) every time you want to deploy you can stop now. If you're as lazy as me, here is the script I use to deploy with one command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># deploy.sh\n\n#!/usr/bin/env sh\n\n# abort on errors\nset -e\n\n# build\necho Linting..\nnpm run lint\necho Building. this may take a minute...\nnpm run build\n\n# navigate into the build output directory\ncd dist\n\n# if you are deploying to a custom domain\n# echo 'example.com' > CNAME\n\necho Deploying..\ngit init\ngit add -A\ngit commit -m 'deploy'\n\n# deploy\ngit push -f git@github.com:<username>/<repo>.git <branch>\n\ncd -\n\n\u003C/code>\u003C/pre>\n\u003Cp>If your on windows look into the Windows Subsystem for Linus (WSL) it will be worth it.\u003C/p>\n\u003Cp>If you are still reading, thank you very much. This is actually my first article and I'm really happy to hear about any opinions and criticisms.\nHappy Coding ♥\u003C/p>","blog/2019-05-vue-on-github-pages","96054292-eb45-4d8a-9aca-bb050175ff2a","How I use Vue.js on GitHub Pages","How to properly deploy a Vue.js app on GitHub Pages",[142,143,144,15],"GitHub Pages","Vue.js","Javascript",["Date","2019-05-04T00:00:00.000Z"],"/assets/2019-05-vue-on-github-pages.png","\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>",[15,149,150,151],"github-pages","javascript","vue.js",[],{"html":154,"slug":155,"uuid":156,"title":157,"date":158,"modified":159,"section":160,"published":10,"content_tags":161,"links":164,"abstract":167,"tags":168,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":170,"latestComment":24},"\u003Cp>A github hosed wiki for all things 3D scanning: \u003Ca href=\"https://3dscanning.wiki/Photogrammetry\" rel=\"nofollow noopener noreferrer\">photogrammetry\u003C/a>, \u003Ca href=\"https://3dscanning.wiki/Lidar\" rel=\"nofollow noopener noreferrer\">lidar\u003C/a>, laser scanning and more. The page is a static website built from a github repository with markdown files.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Deprecated\u003C/span>\u003Cp>The 3D scanning wiki is now offline. But all pages are still available on github.\u003C/p>\n\u003C/blockquote>","projects/3d-scanning-wiki","fd95701b-6d38-4ff0-85a1-d1f919bf9251","The 3D Scanning Wiki",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-11-21T12:45:23.000Z"],"Projects",[162,163,15],"sveltekit","markdown",[165,166],"\u003Cp>\u003Ca href=\"https://3dscanning.wiki/\" rel=\"nofollow noopener noreferrer\">3D Scanning Wiki\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://github.com/3dscanningwiki\" rel=\"nofollow noopener noreferrer\">Github Organisation\u003C/a>\u003C/p>","\u003Cp>A github hosed wiki for all things 3D scanning: \u003Ca href=\"https://3dscanning.wiki/Photogrammetry\">photogrammetry\u003C/a>, \u003Ca href=\"https://3dscanning.wiki/Lidar\">lidar\u003C/a>, laser scanning and more. The page is a static website built from a github repository with markdown files.\u003C/p>",[15,163,162],"projects",[],{"html":172,"slug":173,"uuid":174,"title":175,"date":176,"modified":177,"section":160,"published":10,"content_tags":178,"links":185,"abstract":189,"tags":190,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":191,"latestComment":24},"\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\" rel=\"nofollow noopener noreferrer\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>\n\u003Cp>The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents.\nThe admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/aqualetics-coach-screenshot.png\" alt=\"Screenshot of the coaches view\">\u003C/p>\n\u003Cp>The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.\u003C/p>","projects/aqualetics-coach","3210d289-1f9e-41b5-b1f9-d20f00f6a0c5","Aqualetics Coach",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[179,180,181,182,183,184,15],"node","vue","graphql","hasura","postgresql","docker",[186,187,188],"\u003Cp>\u003Ca href=\"https://sundrbi.ch/coach-application/\" rel=\"nofollow noopener noreferrer\">Overview\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch/2019/09/15/schwimmcoach-applikation-innovation/\" rel=\"nofollow noopener noreferrer\">Blog Post 🇩🇪\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch\" rel=\"nofollow noopener noreferrer\">Aqualetics Swim School\u003C/a>\u003C/p>","\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>",[15,184,181,182,179,183,180],[],{"html":193,"slug":194,"uuid":195,"title":196,"date":197,"modified":8,"section":160,"published":10,"cover_image":198,"content_tags":199,"links":204,"abstract":207,"tags":208,"type":21,"description":61,"folder":169,"comments":209,"latestComment":24},"\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>\n\u003Cul>\n\u003Cli>The basic commenting system\u003C/li>\n\u003Cli>Sending and receiving webmentions\u003C/li>\n\u003Cli>Micropub server implementation\u003C/li>\n\u003Cli>IndieAuth (decentralized authentication standard based on OAuth)\u003C/li>\n\u003Cli>Admin dashboard\u003C/li>\n\u003Cli>Admin backup endpoint\u003C/li>\n\u003C/ul>\n\u003Cp>Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and\ncomments through the fediverse show up back on my website.\u003C/p>\n\u003Cp>The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core\nof the application is only concerned with initializing those plugins.\u003C/p>\n\u003Cp>If you have any questions, or want to run IndieGo yourself, don't hesitate to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>.\u003C/p>","projects/indiego","0cf125b3-a99a-4996-8f84-ec5105d64c57","IndieGo",["Date","2023-08-02T08:39:00.000Z"],"/assets/2022-07-first-go-project-commenting-api.png",[200,201,202,184,203,15],"go","golang","indieweb","sqlite",[205,206],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">IndieGo Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://comments.tiim.ch\" rel=\"nofollow noopener noreferrer\">Admin Interface\u003C/a> - authentication required\u003C/p>","\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>",[15,184,200,201,202,203],[],{"html":211,"slug":212,"uuid":213,"title":214,"date":215,"modified":216,"section":160,"published":10,"content_tags":217,"links":220,"abstract":223,"tags":224,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":225,"latestComment":24},"\u003Cp>A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.\u003C/p>\n\u003Cp>I built this little page to help me count laps for my swimmers for some time based test sets. The first version of the app just had a grid of static buttons, one for each athlete. I quickly found that it is very hard to keep track which laps I already counted. As a way to visualise when each button was last pressed, they change colour. The buttons start green when pressed and slowly turn red over time, based on the average duration of a lap.\u003C/p>\n\u003Cp>The page works completely offline (after the page loads) and no data is sent to any server. It is also possible to export the data to a csv file.\u003C/p>","projects/lap-counter","871ebd58-0543-4d3a-9f3d-16cd85da9bf9","Lap Counter",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[218,219,15],"svelte","swim",[221,222],"\u003Cp>\u003Ca href=\"https://tiim.ch/lap-counter-js/\" rel=\"nofollow noopener noreferrer\">Open Lap Counter\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/laps-counter-8f0sb\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.\u003C/p>",[15,218,219],[],{"html":227,"slug":228,"uuid":229,"title":230,"date":231,"modified":232,"section":160,"published":10,"content_tags":233,"links":235,"abstract":237,"tags":238,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":239,"latestComment":24},"\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\" rel=\"nofollow noopener noreferrer\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/lenex-splits-sheet-creator.png\" alt=\"Screenshot of a split sheet\">\u003C/p>\n\u003Cp>The split sheet creator is a quick and easy way to create a split sheet from your Lenex sign-up file.\nThe split sheet creator does not send your Lenex file to any servers. The file is opened directly in your browser and never leaves your computer!\u003C/p>\n\u003Cp>I built this web app using my \u003Ca href=\"https://www.npmjs.com/package/js-lenex\" rel=\"nofollow noopener noreferrer\">lenex javascript library\u003C/a>.\u003C/p>\n\u003Ch2>What is a split sheet?\u003C/h2>\n\u003Cp>Swim coaches often write down the times of athletes after some fractions of a race (splits). Usually those splits are recorded every lap or every second lap.\u003C/p>","projects/lenex-split-sheet","a03b6eef-ffbe-428d-a559-42bd361ab88c","Lenex Splits Sheet Creator",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[219,234,218,15],"lenex",[236],"\u003Cp>\u003Ca href=\"https://tiim.ch/lenex-splits-sheet-creator/\" rel=\"nofollow noopener noreferrer\">Open Splits Sheet Creator\u003C/a>\u003C/p>","\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>",[15,234,218,219],[],{"html":241,"slug":242,"uuid":243,"title":244,"date":245,"modified":8,"section":160,"published":10,"content_tags":246,"links":251,"abstract":254,"tags":255,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":256,"latestComment":24},"\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>\n\u003Cp>Android widgets are handled by the operating system and only support a limited set of features for rendering.\nTo display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.\u003C/p>","projects/markdown-widget","c6a11779-cf98-4983-8744-9b1effae8d7a","Android Markdown Widget",["Date","2023-08-02T08:59:00.000Z"],[247,248,249,163,250,15],"android","java","kotlin","widget",[252,253],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Android-Markdown-Widget\" rel=\"nofollow noopener noreferrer\">Android Markdown Widget Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://f-droid.org/packages/ch.tiim.markdown_widget/\" rel=\"nofollow noopener noreferrer\">Download on F-Droid\u003C/a>\u003C/p>","\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>",[247,15,248,249,163,250],[],{"html":258,"slug":259,"uuid":260,"title":261,"date":262,"modified":263,"section":160,"published":10,"content_tags":264,"links":266,"abstract":268,"tags":269,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":270,"latestComment":24},"\u003Cp>My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.\u003C/p>\n\u003Cp>I currently do not update this repo anymore because i moved all cheat sheets into my \u003Ca href=\"https://tiim.ch/tags/obsidian\" rel=\"nofollow noopener noreferrer\">Obsidian Vault\u003C/a>. I am looking into a way to keep those two repositories in sync in the future.\u003C/p>","projects/my-cheatsheets","c6d2511a-5d5c-4957-b730-5536f949a1b4","My Cheat Sheets",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[163,15,265],"cheatsheet",[267],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/my-cheatsheets\" rel=\"nofollow noopener noreferrer\">Cheat Sheets on Github\u003C/a>\u003C/p>","\u003Cp>My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.\u003C/p>",[265,15,163],[],{"html":272,"slug":273,"uuid":274,"title":275,"date":276,"modified":277,"section":160,"published":10,"content_tags":278,"links":282,"abstract":284,"tags":285,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":286,"latestComment":24},"\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\" rel=\"nofollow noopener noreferrer\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\" rel=\"nofollow noopener noreferrer\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>","projects/obsidian-fitbit-script","f42e0cad-df0e-4862-8beb-352071f81890","Obsidian.md Fitbit Activity Script",["Date","2022-04-27T20:40:15.000Z"],["Date","2022-06-08T20:15:48.000Z"],[279,280,281,15],"obsidian","fitbit","plugin",[283],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Fitbit-Obsidian-Templater-Script\" rel=\"nofollow noopener noreferrer\">GitHub Repo\u003C/a>\u003C/p>","\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>",[15,280,279,281],[],{"html":288,"slug":289,"uuid":290,"title":291,"date":292,"modified":8,"section":160,"published":10,"content_tags":293,"links":296,"abstract":298,"tags":299,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":300,"latestComment":24},"\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>\n\u003Cp>Pomo is a simple pomodoro timer. It allows you to either specify the number of repetitions (pomodori), the duration of the pomodori and the duration of the breaks, or\nyou can stecify an end time, and let pomo calculate the durations and repetitions.\u003C/p>\n\u003Cp>Pomo runs as a cli tool and stores the current state in a json file. All pomo executions excep \u003Ccode>pomo watch\u003C/code> just\nmodify this json file and terminate. The watch command displays the current pomodoro timer, optionally writes the timer to a text file,\nand watches for changes of the json file.\u003C/p>","projects/pomo","bfa1a7fa-b8a3-469f-b8e8-727ab705cb93","Pomo 🍅",["Date","2023-08-03T11:03:00.000Z"],[294,295,15],"rust","cli",[297],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/pomo\" rel=\"nofollow noopener noreferrer\">pomo Github\u003C/a>\u003C/p>","\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>",[295,15,294],[],{"html":302,"slug":303,"uuid":304,"title":305,"date":306,"modified":307,"section":160,"published":10,"content_tags":308,"links":311,"abstract":302,"tags":313,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":314,"latestComment":24},"\u003Cp>The website of the \"ScBirs\" swim club. This website serves as the center of all information distribution for the swimclub.\u003C/p>","projects/scbirs-website","32787e66-2678-435f-be39-c27265bc9a6f","Swim Club Birsfelden Website",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[309,310,15],"wordpress","php",[312],"\u003Cp>\u003Ca href=\"https://scbirs.ch\" rel=\"nofollow noopener noreferrer\">Website\u003C/a>\u003C/p>",[15,310,309],[],{"html":316,"slug":317,"uuid":318,"title":319,"date":320,"modified":8,"section":160,"published":10,"content_tags":321,"links":323,"abstract":325,"tags":326,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":327,"latestComment":24},"\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>\n\u003Cp>I built TeamKit because the swim club I am a part of needed an easy way for coaches to have an overview of their teams, handle attendance and track their own hours (not implemented yet). I tried a bunch of existing apps and services, but all of them were either too clunky for us or required the team members to sign up as well. This was a dealbreaker for us because we have a bunch of kids teams which are too young to sign up to websites, and because many of the parents are not very tech literate.\u003C/p>\n\u003Cp>With TeamKit a coach can quickly add new team members to a team, create new members directly in the event view (useful for example a person that just wants to try out) without having to add more details than a name.\u003C/p>\n\u003Cp>In the latest update, TeamKit now allows users to create notes on events. This is useful for planning, writing quick notes for an event or a practice session and for sharing information with other coaches.\u003C/p>\n\u003Cp>If you are interested, TeamKit is currenlty free while it is still in beta.\u003C/p>","projects/teamkit","a3040709-cd2d-43cb-ab33-2061ba1ae061","TeamKit",["Date","2022-11-27T09:19:08.000Z"],[162,182,322,15],"postgres",[324],"\u003Cp>\u003Ca href=\"https://teamkit.cc\" rel=\"nofollow noopener noreferrer\">TeamKit\u003C/a>\u003C/p>","\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>",[15,182,322,162],[],{"html":329,"slug":330,"uuid":331,"title":332,"date":333,"modified":334,"section":160,"published":10,"content_tags":335,"links":337,"abstract":340,"tags":341,"type":21,"cover_image":-1,"description":61,"folder":169,"comments":342,"latestComment":24},"\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\" rel=\"nofollow noopener noreferrer\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>","projects/woocommerce-order-explorer","8a52a1ad-a5cf-4191-947c-db6862746816","WooCommerce Order Explorer",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[218,309,336,15],"woocommerce",[338,339],"\u003Cp>\u003Ca href=\"https://tiim.ch/woocommerce-order-explorer-js/\" rel=\"nofollow noopener noreferrer\">Open Order Explorer\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/woo-commerce-order-explorer-js-vmu3h\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>",[15,218,336,309],[],{"html":344,"slug":345,"name":346,"date":347,"content_tags":348,"like_of":350,"raw_data":352,"tags":369,"links":-1,"published":10,"type":370,"cover_image":-1,"description":371,"folder":372,"comments":373,"latestComment":24},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==\">https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/12/mte4nd","How Caddy 2 works, a deep dive into the source",["Date","2022-12-04T13:08:00.000Z"],[349,201,15],"caddy",{"url":351},"https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==",{"items":353,"rels":367,"relurls":368},[354],{"id":61,"value":61,"html":61,"type":355,"properties":357,"shape":61,"coords":61,"children":366},[356],"h-entry",{"category":358,"like-of":359,"name":360,"post-status":362,"published":364},[349,201,15],[351],[361]," How Caddy 2 works, a deep dive into the source",[363],"published",[365],"2022-12-04T14:08:00+0100",[],{},{},[349,15,201],"like","👍 Liked: https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==","mf2",[],{"tag":15}],"uses":{"params":["slug"]}}]} diff --git a/tags/dns.html b/tags/dns.html new file mode 100644 index 00000000..f05b4280 --- /dev/null +++ b/tags/dns.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️dns - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: dns

+ +

Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN

+ 3/15/2023 +
Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN +

I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/dns/__data.json b/tags/dns/__data.json new file mode 100644 index 00000000..32a3126e --- /dev/null +++ b/tags/dns/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>\n\u003Cp>I will show you how I fixed this problem for me and explain what the commands I used do. This post is mostly for my reference, but I hope it helps anyone else as well.\u003C/p>\n\u003Ch2>Finding out what your problem is\u003C/h2>\n\u003Cp>Let's check first if we have internet access inside WSL2. For this run the ping command with an IP address as a destination:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping 8.8.8.8\n\u003C/code>\u003C/pre>\n\u003Cp>If you get something like this as the output, your internet connection is fine, and it's just the DNS nameserver addresses that are misconfigured, you can jump forward to Solution 2.\u003C/p>\n\u003Cpre>\u003Ccode>PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n64 bytes from 8.8.8.8: icmp_seq=1 ttl=108 time=4.53 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=108 time=3.94 ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=108 time=3.97 ms\n64 bytes from 8.8.8.8: icmp_seq=4 ttl=108 time=3.78 ms\n64 bytes from 8.8.8.8: icmp_seq=5 ttl=108 time=3.77 ms\n64 bytes from 8.8.8.8: icmp_seq=6 ttl=108 time=3.76 ms\n64 bytes from 8.8.8.8: icmp_seq=7 ttl=108 time=3.81 ms\n\u003C/code>\u003C/pre>\n\u003Cp>If you don't get any responses from the ping (i.e. no more output after the \u003Ccode>PING 8.8.8.8 (8.8.8.8) ...\u003C/code> line), you need to configure the WSL and the VPN network adapter metric. Go to Solution 1.\u003C/p>\n\u003Cp>To check if the DNS is working, we can again use the ping command, this time with a domain name:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping google.com\n\u003C/code>\u003C/pre>\n\u003Cp>If you get responses, the DNS and your internet connection are working! If not go to Section 2.\u003C/p>\n\u003Ch2>Solution 1: Fixing the Network Adapter\u003C/h2>\n\u003Cp>Run the following two commands in PowerShell as administrator:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match \"Cisco AnyConnect\"} | Set-NetIPInterface -InterfaceMetric 4000\n\nGet-NetIPInterface -InterfaceAlias \"vEthernet (WSL)\" | Set-NetIPInterface -InterfaceMetric 1\n\u003C/code>\u003C/pre>\n\u003Cp>Let me explain what those two commands do. Both follow the same pattern of listing all network adapters, selecting a specific adapter from the list and setting its \"metric\".\u003C/p>\n\u003Cp>You can imagine an adapter as a virtual network port on the back of your pc or laptop. But instead of sending packets through the wire, the driver for a specific port can do whatever it wants with those packets, in the case of a VPN, the packets get encrypted and forwarded to the internet via another adapter.\u003C/p>\n\u003Cp>The \u003Ca href=\"https://learn.microsoft.com/en-us/windows-server/networking/technologies/network-subsystem/net-sub-interface-metric\" rel=\"nofollow noopener noreferrer\">InterfaceMetric\u003C/a> is a value associated with each adapter that determines the order of those adapters. This allows windows to determine which adapter to prefer over another one.\u003C/p>\n\u003Cp>By setting the interface metric of the Cisco adapter to 4000 and the metric of the WSL adapter to one, we allow the traffic from WSL to flow through the Cisco adapter. To be honest I do not exactly understand why this works but it does.\u003C/p>\n\u003Ch2>Solution 2: Registering the VPN DNS inside of WSL\u003C/h2>\n\u003Cp>Setting the DNS servers is, unfortunately, a little bit more involved than just running two commands, we need to edit the files \u003Ccode>/etc/wsl.conf\u003C/code> and \u003Ccode>/etc/resolv.conf\u003C/code>, and restart wsl in between. Let's get to it:\u003C/p>\n\u003Cp>Edit the file \u003Ccode>/etc/wsl.conf\u003C/code> inside of WSL2 using a text editor. I suggest doing this through the terminal since you need root permissions to do that:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/wsl.conf\n# feel free to use another editor such as vim or emacs\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely this file does not exist yet, otherwise, I suggest you create a backup of the original file to preserve the settings.\u003C/p>\n\u003Cp>Add the following config settings into the file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-ini\">[network]\ngenerateResolvConf = false\n\u003C/code>\u003C/pre>\n\u003Cp>This will instruct WSL to not override the \u003Ccode>/etc/resolv.conf\u003C/code> file on every start-up. Save the file and restart WSL with the following command so that the changed config takes effect:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open a PowerShell terminal and list all network adapters with the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ipconfig /all\n\u003C/code>\u003C/pre>\n\u003Cp>Find the Cisco AnyConnect adapter and copy the IP addresses in the DNS-Server field. We will need those IPs in the next step.\u003C/p>\n\u003Cp>Start WSL again and edit the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely there is already something in this file, you can discard it. When undoing the changes, WSL will automatically regenerate this file anyway, so you don't need to back it up.\u003C/p>\n\u003Cp>Delete all the contents and enter the IP addresses you noted down in the last step in the following format:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-resolv\">nameserver xxx.xxx.xxx.xxx\n\u003C/code>\u003C/pre>\n\u003Cp>Put each address on a new line, preceded by the string \u003Ccode>nameserver\u003C/code>.\nSave the file and restart WSL with the same command as above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open up WSL for the last time and set the immutable flag for the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">chattr +i /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>And for the last time shut down WSL. Your DNS should now be working fine!\u003C/p>\n\u003Ch2>Undoing those changes\u003C/h2>\n\u003Cp>I did not have a need to undo the steps for \u003Ccode>Solution 1\u003C/code>, and I'm pretty sure the metric resets after each system reboot anyway so there is not much to do.\u003C/p>\n\u003Cp>To get DNS working again when not connected to the VPN run the following commands:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo chattr -i /etc/resolv.conf\nsudo rm /etc/resolv.conf\nsudo rm /etc/wsl.conf\nwsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>This will first clear the immutable flag off \u003Ccode>/etc/resolv.conf\u003C/code>, and delete it. Next, it will delete \u003Ccode>/etc/wsl.conf\u003C/code> if you have a backup of a previous \u003Ccode>wsl.conf\u003C/code> file, you can replace it with that. At last, we shutdown WSL again for the changes to take effect.\u003C/p>\n\u003Cp>Unfortunately, this is quite a procedure to get a VPN to work with WSL2, but I'm hopeful that this will soon not be necessairy anymore.\u003C/p>","blog/2023-03-21-anyconnect-wsl2","c67bc4dc-4c96-41b1-afb5-15a99457dedf",["Date","2023-03-15T15:22:04.511Z"],["Date","2023-03-15T15:22:04.511Z"],[9],null,"Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN",true,"I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.","https://media.tiim.ch/66ca4290-3fc0-450f-977b-f00f888e4af3.webp","Stable Diffusion - Anything V3.0 - 1boy, hacker, in front of computer, back of head visible, vintage neon color scheme, terminal, big monitor",[16,17,18,19],"wsl","vpn","networking","dns","\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>",[19,18,17,16],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":19}],"uses":{"params":["slug"]}}]} diff --git a/tags/docker.html b/tags/docker.html new file mode 100644 index 00000000..213c7b2d --- /dev/null +++ b/tags/docker.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️docker - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: docker

+ +

"no such file or directory" after enabling CGO in Docker

+ 1/24/2023 +
+

Quick fix for the "no such file or directory" error after enabling CGO, when running in a scratch docker image.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/docker/__data.json b/tags/docker/__data.json new file mode 100644 index 00000000..c86a31c1 --- /dev/null +++ b/tags/docker/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":105},[2,24,65,88],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":-1,"cover_image_txt":13,"content_tags":14,"abstract":18,"tags":19,"links":-1,"type":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message \u003Ccode>exec /app/indiego: no such file or directory\u003C/code>. I had removed the \u003Ccode>CGO_ENABLE=0\u003C/code> variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the \u003Ccode>scratch\u003C/code> image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.\u003C/p>\n\u003Cp>To include libc into the container, I simply changed the base image from \u003Ccode>scratch\u003C/code> to \u003Ccode>alpine\u003C/code>, which includes libc. This makes the image slightly larger but this seemed way easier than trying to include libc directly.\u003C/p>\n\u003Cp>As a bonus I got to delete the \u003Ccode>/usr/share/zoneinfo\u003C/code> and \u003Ccode>ca-certificates.crt\u003C/code> files, and rely on those provided by alpine.\u003C/p>\n\u003Cp>You can see the commit to IndieGo \u003Ca href=\"https://github.com/Tiim/IndieGo/commit/63968814de7e39f295386bf398b583aa8bf0411c\" rel=\"nofollow noopener noreferrer\">here\u003C/a>.\u003C/p>","blog/2023-01-24-no-such-file-or-directory-cgo","dd580343-9e0f-4754-93dd-25667e6b5859",["Date","2023-01-24T00:00:00.000Z"],["Date","2023-01-24T20:54:11.330Z"],[9],null,"\"no such file or directory\" after enabling CGO in Docker",true,"Quick fix for the \"no such file or directory\" error after enabling CGO, when running in a scratch docker image.","",[15,16,17],"go","cgo","docker","\u003Cp>Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message \u003Ccode>exec /app/indiego: no such file or directory\u003C/code>. I had removed the \u003Ccode>CGO_ENABLE=0\u003C/code> variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the \u003Ccode>scratch\u003C/code> image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.\u003C/p>",[16,17,15],"article","blog",[],"2023-09-02T19:26:59Z",{"html":25,"slug":26,"uuid":27,"date":28,"created":29,"aliases":30,"title":31,"published":11,"modified":32,"description":33,"cover_image":34,"cover_image_txt":35,"content_tags":36,"abstract":39,"tags":40,"links":-1,"type":20,"folder":21,"comments":41,"latestComment":23},"\u003Cp>I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used \u003Ca href=\"https://hexchat.github.io/\" rel=\"nofollow noopener noreferrer\">HexChat\u003C/a> in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.\u003C/p>\n\u003Cp>The one client I have seen mentioned a lot is \u003Ca href=\"https://weechat.org/\" rel=\"nofollow noopener noreferrer\">WeeChat\u003C/a> (not to be confused with WeChat, the Chinese instant messenger). WeeChat runs in the terminal as a \u003Ca href=\"https://en.wikipedia.org/wiki/Text-based_user_interface\" rel=\"nofollow noopener noreferrer\">TUI\u003C/a> and after a while of getting used to (and after enabling 'mouse mode') it seems intuitive enough.\u003C/p>\n\u003Cp>The nice thing about WeeChat running not as a graphical application, is that it makes it possible to run on a server and access it from anywhere over ssh.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>INFO\u003C/span>\u003Cp>Except on mobile devices, but weechat has mobile apps that can connect to it directly.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Since I pretty much host all my selfhosted software in docker on a VPS, I was looking if someone already published a docker image for WeeChat. There is a bunch of them, but only \u003Ca href=\"https://hub.docker.com/r/weechat/weechat\" rel=\"nofollow noopener noreferrer\">weechat/weechat\u003C/a> (the official image) is still updated regularly. The docker hub page does not have any documentation, but I managed to find it in the \u003Ca href=\"https://github.com/weechat/weechat-container\" rel=\"nofollow noopener noreferrer\">weechat/weechat-container\u003C/a> github repo.\u003C/p>\n\u003Cp>As it says in the readme on github, you can start the container with\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">docker run -it weechat/weechat\n\u003C/code>\u003C/pre>\n\u003Cp>which will run weechat directly in the foreground.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>Don't skip the \u003Ccode>-it\u003C/code> command line flags. The \u003Ccode>-i\u003C/code> or \u003Ccode>--interactive\u003C/code> keeps stdin open, which is required to send input to weechat. Weechat also closes immediately if the stdin gets closed, which took me a while to figure out.\nThe \u003Ccode>-t\u003C/code> or \u003Ccode>--tty\u003C/code> flag is required to provide a fake tty to the container. I don't really understand what that means but without this you won't see the user interface of weechat.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Running in the foreground is not really that helpful if we want to run weechat on a server, so we need to detach (let it run in the background) from the container with the \u003Ccode>-d\u003C/code> or \u003Ccode>--detach\u003C/code> flag. It also helps to specify a name for the container with the \u003Ccode>--name <name>\u003C/code> argument, so we can quickly find the container again later. The docker command now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">docker run -it -d --name weechat weechat/weechat\n\u003C/code>\u003C/pre>\n\u003Cp>When we run this command, we will notice that weechat is running in the background. To access it we can run \u003Ccode>docker attach weechat\u003C/code>. To detach from weechat without exiting the container, we can press \u003Ccode>CTRL-p CTRL-q\u003C/code> as described in the \u003Ca href=\"https://docs.docker.com/engine/reference/commandline/attach/#description\" rel=\"nofollow noopener noreferrer\">docker attach reference\u003C/a>\u003C/p>\n\u003Cp>I noticed that there are two versions of the weechat image: a debian version and an alpine linux version. Generally the Alpine Linux versions of containers are smaller than the Debian versions, so I decided to use the alpine version: \u003Ccode>weechat/weechat:latest-alpine\u003C/code>.\u003C/p>\n\u003Cp>With this we are practically done, but if we ever remove and restart the container, all of the chat logs and customisations to weechat will be gone. To prevent this we need to add the config and log files to a volume.\u003C/p>\n\u003Cp>I generally use the folder \u003Ccode>~/docker/(service)\u003C/code> to point my docker volumes to, so I have a convenient place to inspect, modify and back up the data.\u003C/p>\n\u003Cp>Let's create the folder and add the volume to the docker container. I also added the \u003Ccode>--restart unless-stopped\u003C/code> flag to make sure the container gets restarted if it either exits for some reason of if docker restarts.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">mkdir -p ~/docker/weechat/data\nmkdir -p ~/docker/weechat/config\n\ndocker run -it -d --restart unless-stopped \\\n -v \"~/docker/weechat/data:/home/user/.weechat\" \\\n -v \"~/docker/weechat/config:/home/user/.config/weechat\" \\\n --name weechat weechat/weechat:latest-alpine`\n\u003C/code>\u003C/pre>\n\u003Cp>Running this command on the server is all we need to have weechat running in docker.\u003C/p>\n\u003Cblockquote>\n\u003Cp>But how do I quickly connect to weechat? Do I always have to first ssh into the server and then run docker attach?\u003C/p>\n\u003C/blockquote>\n\u003Cp>Yes but, as almost always, we can simplify this with a bash script:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-bash\">#!/usr/bin/env bash\n\nHOST=<ssh host>\nssh -t \"${HOST}\" docker attach weechat\n\u003C/code>\u003C/pre>\n\u003Cp>This bash script starts ssh with the \u003Ccode>-t\u003C/code> flag which tells ssh that the command is interactive.\nCopy this script into your \u003Ccode>~/.local/bin\u003C/code> folder and make it executable.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">nano ~/.local/bin/weechat.sh\nchmod +x weechat.sh\n\u003C/code>\u003C/pre>\n\u003Cp>And that's it! Running \u003Ccode>weechat.sh\u003C/code> will open an ssh session to your server and attach to the weechat container. Happy Chatting!\u003C/p>\n\u003Cp>If you liked this post, consider subscribing to my blog via \u003Ca href=\"https://tiim.ch/blog/rss.xml\" rel=\"nofollow noopener noreferrer\">RSS\u003C/a>, or on \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">social media\u003C/a>. If you have any questions, feel free to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>. I also usually hang out in \u003Ca href=\"irc://irc.libera.chat/##tiim\">\u003Ccode>##tiim\u003C/code> on irc.libera.chat\u003C/a>. My name on IRC is \u003Ccode>tiim\u003C/code>.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Update 2022-01-18\u003C/span>\u003Cp>I have found that at the beginning of a session, the input to weechat doesn't seem to work. Sometimes weechat refuses to let me type anything and/or doesn't recognize mouse events.\nAfter a while of spamming keys and \u003Ccode>Alt-m\u003C/code> (toggle mouse mode), it seems to fix itself most of the time.\nI have no idea if thats a problem with weechat, with docker or with ssh, and so far have not found a solution for this. If you have the same problem or even know how to fix it, feel free to reach out.\u003C/p>\n\u003C/blockquote>","blog/2023-01-15-weechat-docker","889ff4db-3ccb-4ab1-9676-a2b0ea8f19eb",["Date","2023-01-15T00:00:00.000Z"],["Date","2023-01-15T00:17:07.000Z"],[9],"Running the WeeChat IRC Client on a VPS in Docker",["Date","2023-01-18T11:34:27.000Z"],"Walkthrough on how to setup the WeeChat IRC client in docker.","https://media.tiim.ch/a28c65a1-ed95-43d3-af87-a2ad222bee7f.jpg","Stable Diffusion - anime landscape, pastel colors, thick outlines, forest, mountains, golden light",[37,38,17],"irc","weechat","\u003Cp>I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used \u003Ca href=\"https://hexchat.github.io/\">HexChat\u003C/a> in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.\u003C/p>",[17,37,38],[42,49,54,59],{"id":43,"type":44,"replyTo":13,"timestamp":45,"page":26,"url":46,"content":47,"name":48},"52b7b3e6-e233-4379-8f0c-3332aed562a6","webmention","2023-03-28T10:10:56Z","https://tiim.ch/blog/2023-03-28-weechat-notification-ntfy","Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.","Tim Bachmann",{"id":50,"type":44,"replyTo":13,"timestamp":51,"page":26,"url":52,"content":13,"name":53},"f6f58ebf-a68f-415e-baed-cb8bf38189fd","2023-03-02T00:05:49Z","https://brid.gy/like/twitter/tiimb/1614403118258601987/1557313575072546816","DM Cyber Security",{"id":55,"type":44,"replyTo":13,"timestamp":56,"page":26,"url":57,"content":13,"name":58},"45d40e9f-6498-4432-bdb0-01210e55d092","2023-01-25T18:20:43Z","https://brid.gy/like/twitter/tiimb/1614403118258601987/8717982","Christopher Scott",{"id":60,"type":44,"replyTo":13,"timestamp":61,"page":26,"url":62,"content":63,"name":64},"979c42d0-a8fc-4a52-a85d-bedb672fb144","2023-01-25T09:22:51Z","https://brid.gy/repost/twitter/tiimb/1614403118258601987/1618133427139792898","New blog post: A walkthrough on how to set up the WeeChat IRC client in docker. #irc #docker @WeeChatClient\ntiim.ch/blog/2023-01-1…","WeeChat",{"html":66,"slug":67,"uuid":68,"title":69,"date":70,"modified":71,"section":72,"published":11,"content_tags":73,"links":80,"abstract":84,"tags":85,"type":20,"cover_image":-1,"description":13,"folder":86,"comments":87,"latestComment":23},"\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\" rel=\"nofollow noopener noreferrer\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>\n\u003Cp>The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents.\nThe admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/aqualetics-coach-screenshot.png\" alt=\"Screenshot of the coaches view\">\u003C/p>\n\u003Cp>The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.\u003C/p>","projects/aqualetics-coach","3210d289-1f9e-41b5-b1f9-d20f00f6a0c5","Aqualetics Coach",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",[74,75,76,77,78,17,79],"node","vue","graphql","hasura","postgresql","dev",[81,82,83],"\u003Cp>\u003Ca href=\"https://sundrbi.ch/coach-application/\" rel=\"nofollow noopener noreferrer\">Overview\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch/2019/09/15/schwimmcoach-applikation-innovation/\" rel=\"nofollow noopener noreferrer\">Blog Post 🇩🇪\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch\" rel=\"nofollow noopener noreferrer\">Aqualetics Swim School\u003C/a>\u003C/p>","\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>",[79,17,76,77,74,78,75],"projects",[],{"html":89,"slug":90,"uuid":91,"title":92,"date":93,"modified":9,"section":72,"published":11,"cover_image":94,"content_tags":95,"links":99,"abstract":102,"tags":103,"type":20,"description":13,"folder":86,"comments":104,"latestComment":23},"\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>\n\u003Cul>\n\u003Cli>The basic commenting system\u003C/li>\n\u003Cli>Sending and receiving webmentions\u003C/li>\n\u003Cli>Micropub server implementation\u003C/li>\n\u003Cli>IndieAuth (decentralized authentication standard based on OAuth)\u003C/li>\n\u003Cli>Admin dashboard\u003C/li>\n\u003Cli>Admin backup endpoint\u003C/li>\n\u003C/ul>\n\u003Cp>Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and\ncomments through the fediverse show up back on my website.\u003C/p>\n\u003Cp>The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core\nof the application is only concerned with initializing those plugins.\u003C/p>\n\u003Cp>If you have any questions, or want to run IndieGo yourself, don't hesitate to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>.\u003C/p>","projects/indiego","0cf125b3-a99a-4996-8f84-ec5105d64c57","IndieGo",["Date","2023-08-02T08:39:00.000Z"],"/assets/2022-07-first-go-project-commenting-api.png",[15,96,97,17,98,79],"golang","indieweb","sqlite",[100,101],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">IndieGo Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://comments.tiim.ch\" rel=\"nofollow noopener noreferrer\">Admin Interface\u003C/a> - authentication required\u003C/p>","\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>",[79,17,15,96,97,98],[],{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/email.html b/tags/email.html new file mode 100644 index 00000000..1f27a3a3 --- /dev/null +++ b/tags/email.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️email - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: email

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/email/__data.json b/tags/email/__data.json new file mode 100644 index 00000000..1f0ae02d --- /dev/null +++ b/tags/email/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":45},[2],{"html":3,"slug":4,"date":5,"content_tags":6,"in_reply_to":10,"raw_data":12,"abstract":30,"tags":31,"links":-1,"published":32,"type":33,"cover_image":-1,"description":34,"folder":35,"comments":36,"latestComment":44},"\u003Cdiv class=\"mf2\">\u003Cp>This post is in reply to \"\u003Ca class=\"u-in-reply-to\" href=\"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\">https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\u003C/a>\"\u003C/p>\u003C/div>\n\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>","mf2/2023/07/mteymz",["Date","2023-07-02T17:58:00.000Z"],[7,8,9],"rss","email","newsletter",{"url":11},"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/",{"items":13,"rels":28,"relurls":29},[14],{"id":15,"value":15,"html":15,"type":16,"properties":18,"shape":15,"coords":15,"children":27},"",[17],"h-entry",{"category":19,"content":20,"in-reply-to":22,"post-status":23,"published":25},[7,8,9],[21],"Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\n\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile. ",[11],[24],"published",[26],"2023-07-02T19:58:00+0200",[],{},{},"\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>",[8,9,7],true,"reply","💬 In reply to: https://kevincox.ca/2023/06/27/decade-of-rss-via-email/","mf2",[37],{"id":38,"type":39,"replyTo":15,"timestamp":40,"page":4,"url":41,"content":42,"name":43},"447820cb-5432-4f54-bd83-94e5674366e6","comment","2023-07-03T11:01:20Z","https://tiim.ch/mf2/2023/07/mteymz#447820cb-5432-4f54-bd83-94e5674366e6","I definitely get that email-based feed reading is not for everyone, but I don't think we are that different here. My inbox is indeed reserved for \"for things that require my attention\". That is why only a very small number of feeds go to my inbox (like WebMention replies to my posts). The vast majority of feeds go into dedicated folders with no notifications. So in a way these folders are my feed reader. The only relation that they have to my \"normal email workflow\" is that the exist in the same app. When I say I get my feeds via email lots of people immediately picture having feeds show up in their regular email workflow and are (rightfully) mortified. I think this approach would work well for almost no one. I think it is very important to have separation by priority much like Thunderbird has a separate news feed rather than dumping the feeds into your inbox. The difference here is that the separation is different folders in the same IMAP account rather than a distinct news account.","Kevin","2023-09-02T19:26:59Z",{"tag":8}],"uses":{"params":["slug"]}}]} diff --git a/tags/fediverse.html b/tags/fediverse.html new file mode 100644 index 00000000..0be4f1cf --- /dev/null +++ b/tags/fediverse.html @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️fediverse - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: fediverse

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/fediverse/__data.json b/tags/fediverse/__data.json new file mode 100644 index 00000000..402095ea --- /dev/null +++ b/tags/fediverse/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":70},[2,26,59],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\" rel=\"nofollow noopener noreferrer\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\" rel=\"nofollow noopener noreferrer\">Way back machine\u003C/a>.\u003C/p>\n\u003Cp>Another community I used to be really active in, was for a small indie roleplaying game called \u003Ca href=\"\">Illarion\u003C/a>. Again, the community relied heavily on a forum for communications. This time it was used for players to engage in \"out of character\" communication, as well as a way to simulate a metaphorical bullet board in the game town square where characters could leave notes for each other.\nSince the game was closely inspired by TTRPGs like D&D, the role-playing part was more important than the in-game mechanics. The forum allowed characters to interact with each other that were not online at the same time. Again, I got really invested in this community, even going so far as joining other guild-specific forums.\u003C/p>\n\u003Cp>I eventually moved on from both of those amazing communities, because my interests changed. I left the AutoHotkey community because I started to get more involved with other programming languages, and I left the Illarion community because I (with the support of my parents) was looking for a less time-intensive game. Unfortunately, I never happened to find another online community like those two ever again...\u003C/p>\n\u003Cp>Sometime later I joined Reddit and was amazed. It felt like a place where all communities come together on a single site. No need to check on multiple websites for new posts, everything neatly together in a single website, accessible on a single (third party) app. I remember wondering why people were still using forums when Reddit was so much simpler.\u003C/p>\n\u003Cp>Jumping to the present and I realize that I was wrong. Even though I am subscribed to a bunch of communities on Reddit, I barely comment on any posts and posted even less. While I am a community member on record, I do not feel like one. The wealth of communities, as well as the incentive to go on the front page to see the most popular posts of the whole site, made me want to open Reddit, but it did not give me the feeling of belonging. I rather felt like a spectator that from time to time gathers the courage to shout his own ideas into the ether.\u003C/p>\n\u003Cblockquote>\n\u003Cp>Side note: Discord comes much closer to the feeling of community. However, the nature of chat makes the interactions fleeting, being in a chat room with a few hundred other people, where every message is just a few sentences at most does not lead to the same connections. No one expects their message to be read again after a few days.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Now the company behind Reddit started to lose the goodwill of the users. While I don't think Reddit will die anytime soon, I think there are a lot of people looking for alternatives. And the best alternative to the website that killed forums is... forums.\u003C/p>\n\u003Cp>While forums largely still work the same as they did 15 years ago, there have been developments that might make them more feasible for our desire to have everything accessible on a single site or on a single app. Last time a social media company, Twitter, annoyed its user base, the fediverse, and more specifically Mastodon, started to go more mainstream. This time I hope there will be other projects that profit. I have heard people mentioning the projects Kbin and Lemmy, both forum-like platforms that implement the ActivityPub specification. Same as Mastodon, this means users are able to interact with users on other instances. Even further, this should also allow users of any federated social network, such as Mastodon, to post and comment on any federated forum. Even established forum software such as \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">Flarum\u003C/a> and \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">nodeBB\u003C/a> are considering adding federation support.\u003C/p>\n\u003Cp>I really hope that forums make a comeback, not only because of the nostalgia but also because to me it feels like a more sustainable way to build a community. And now with the possibility to federate via the fediverse, a forum doesn't have to be a walled garden of members any more. In the end, most importantly I hope people are still finding communities they can be as passionate about as I was, without any corporate overlords trying to keep their eyeballs on ads as long as possible.\u003C/p>","blog/2023-06-16-forums","624afba6-2962-4710-9bc7-686702cc9b55",["Date","2023-06-16T18:56:56.000Z"],["Date","2023-06-16T15:09:15.488Z"],[9],null,"Forums",true,"My experience of using forums in my teens, what changed after I started using reddit and my hopes for internet communities in the future.","https://media.tiim.ch/fe5de393-9773-4eaa-877a-decffbd706b4.webp","Stable Diffusion - bunch people talking to each other, social, speech bubbles, digital art, minimalistic",[16,17,18,19],"forum","fediverse","reddit","activitypub","\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\">Way back machine\u003C/a>.\u003C/p>",[19,17,16,18],"article","blog",[],"2023-09-02T19:26:59Z",{"html":27,"slug":28,"name":29,"date":30,"content_tags":31,"like_of":34,"raw_data":36,"tags":53,"links":-1,"published":11,"type":55,"cover_image":-1,"description":56,"folder":57,"comments":58,"latestComment":25},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://werd.io/2022/the-fediverse-and-the-indieweb\">https://werd.io/2022/the-fediverse-and-the-indieweb\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/ntc1nd","The fediverse and the indieweb",["Date","2022-11-30T06:35:00.000Z"],[32,17,33],"Indieweb","mastodon",{"url":35},"https://werd.io/2022/the-fediverse-and-the-indieweb",{"items":37,"rels":51,"relurls":52},[38],{"id":39,"value":39,"html":39,"type":40,"properties":42,"shape":39,"coords":39,"children":50},"",[41],"h-entry",{"category":43,"like-of":44,"name":45,"post-status":46,"published":48},[32,17,33],[35],[29],[47],"published",[49],"2022-11-30T07:35:00+0100",[],{},{},[17,54,33],"indieweb","like","👍 Liked: https://werd.io/2022/the-fediverse-and-the-indieweb","mf2",[],{"html":60,"slug":61,"date":62,"content_tags":63,"like_of":65,"tags":67,"links":-1,"published":11,"type":55,"cover_image":-1,"description":68,"folder":57,"comments":69,"latestComment":25},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/\">https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/mji1md",["Date","2022-11-22T10:28:00.000Z"],[64,17,33],"twitter",{"url":66},"https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/",[17,33,64],"👍 Liked: https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/",[],{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/fitbit.html b/tags/fitbit.html new file mode 100644 index 00000000..b9e33d3c --- /dev/null +++ b/tags/fitbit.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️fitbit - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: fitbit

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/fitbit/__data.json b/tags/fitbit/__data.json new file mode 100644 index 00000000..70e64e0c --- /dev/null +++ b/tags/fitbit/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":16,"abstract":18,"tags":19,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\" rel=\"nofollow noopener noreferrer\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\" rel=\"nofollow noopener noreferrer\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>","projects/obsidian-fitbit-script","f42e0cad-df0e-4862-8beb-352071f81890","Obsidian.md Fitbit Activity Script",["Date","2022-04-27T20:40:15.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14,15],"obsidian","fitbit","plugin","dev",[17],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Fitbit-Obsidian-Templater-Script\" rel=\"nofollow noopener noreferrer\">GitHub Repo\u003C/a>\u003C/p>","\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>",[15,13,12,14],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":13}],"uses":{"params":["slug"]}}]} diff --git a/tags/forum.html b/tags/forum.html new file mode 100644 index 00000000..71df83e3 --- /dev/null +++ b/tags/forum.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️forum - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: forum

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/forum/__data.json b/tags/forum/__data.json new file mode 100644 index 00000000..81ff2263 --- /dev/null +++ b/tags/forum/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\" rel=\"nofollow noopener noreferrer\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\" rel=\"nofollow noopener noreferrer\">Way back machine\u003C/a>.\u003C/p>\n\u003Cp>Another community I used to be really active in, was for a small indie roleplaying game called \u003Ca href=\"\">Illarion\u003C/a>. Again, the community relied heavily on a forum for communications. This time it was used for players to engage in \"out of character\" communication, as well as a way to simulate a metaphorical bullet board in the game town square where characters could leave notes for each other.\nSince the game was closely inspired by TTRPGs like D&D, the role-playing part was more important than the in-game mechanics. The forum allowed characters to interact with each other that were not online at the same time. Again, I got really invested in this community, even going so far as joining other guild-specific forums.\u003C/p>\n\u003Cp>I eventually moved on from both of those amazing communities, because my interests changed. I left the AutoHotkey community because I started to get more involved with other programming languages, and I left the Illarion community because I (with the support of my parents) was looking for a less time-intensive game. Unfortunately, I never happened to find another online community like those two ever again...\u003C/p>\n\u003Cp>Sometime later I joined Reddit and was amazed. It felt like a place where all communities come together on a single site. No need to check on multiple websites for new posts, everything neatly together in a single website, accessible on a single (third party) app. I remember wondering why people were still using forums when Reddit was so much simpler.\u003C/p>\n\u003Cp>Jumping to the present and I realize that I was wrong. Even though I am subscribed to a bunch of communities on Reddit, I barely comment on any posts and posted even less. While I am a community member on record, I do not feel like one. The wealth of communities, as well as the incentive to go on the front page to see the most popular posts of the whole site, made me want to open Reddit, but it did not give me the feeling of belonging. I rather felt like a spectator that from time to time gathers the courage to shout his own ideas into the ether.\u003C/p>\n\u003Cblockquote>\n\u003Cp>Side note: Discord comes much closer to the feeling of community. However, the nature of chat makes the interactions fleeting, being in a chat room with a few hundred other people, where every message is just a few sentences at most does not lead to the same connections. No one expects their message to be read again after a few days.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Now the company behind Reddit started to lose the goodwill of the users. While I don't think Reddit will die anytime soon, I think there are a lot of people looking for alternatives. And the best alternative to the website that killed forums is... forums.\u003C/p>\n\u003Cp>While forums largely still work the same as they did 15 years ago, there have been developments that might make them more feasible for our desire to have everything accessible on a single site or on a single app. Last time a social media company, Twitter, annoyed its user base, the fediverse, and more specifically Mastodon, started to go more mainstream. This time I hope there will be other projects that profit. I have heard people mentioning the projects Kbin and Lemmy, both forum-like platforms that implement the ActivityPub specification. Same as Mastodon, this means users are able to interact with users on other instances. Even further, this should also allow users of any federated social network, such as Mastodon, to post and comment on any federated forum. Even established forum software such as \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">Flarum\u003C/a> and \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">nodeBB\u003C/a> are considering adding federation support.\u003C/p>\n\u003Cp>I really hope that forums make a comeback, not only because of the nostalgia but also because to me it feels like a more sustainable way to build a community. And now with the possibility to federate via the fediverse, a forum doesn't have to be a walled garden of members any more. In the end, most importantly I hope people are still finding communities they can be as passionate about as I was, without any corporate overlords trying to keep their eyeballs on ads as long as possible.\u003C/p>","blog/2023-06-16-forums","624afba6-2962-4710-9bc7-686702cc9b55",["Date","2023-06-16T18:56:56.000Z"],["Date","2023-06-16T15:09:15.488Z"],[9],null,"Forums",true,"My experience of using forums in my teens, what changed after I started using reddit and my hopes for internet communities in the future.","https://media.tiim.ch/fe5de393-9773-4eaa-877a-decffbd706b4.webp","Stable Diffusion - bunch people talking to each other, social, speech bubbles, digital art, minimalistic",[16,17,18,19],"forum","fediverse","reddit","activitypub","\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\">Way back machine\u003C/a>.\u003C/p>",[19,17,16,18],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":16}],"uses":{"params":["slug"]}}]} diff --git a/tags/git.html b/tags/git.html new file mode 100644 index 00000000..ff731706 --- /dev/null +++ b/tags/git.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️git - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: git

+ +

Modelling Git Operations as Planning Problems

+ 1/20/2021 +
Modelling Git Operations as Planning Problems +

Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/git/__data.json b/tags/git/__data.json new file mode 100644 index 00000000..c1d6e1d2 --- /dev/null +++ b/tags/git/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"description":8,"content_tags":9,"date":14,"modified":15,"cover_image":16,"abstract":17,"tags":18,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>\n\u003Cp>\u003Ca href=\"https://tiim.ch/assets/2021-01-20-Thesis.pdf\" rel=\"nofollow noopener noreferrer\">Download Thesis\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode>@thesis{bachmann2021,\n\ttitle = {Modelling Git Operations as Planning Problems},\n\tauthor = {Tim Bachmann},\n\tyear = {2021},\n month = {01},\n\ttype = {Bachelor's Thesis},\n\tschool = {University of Basel},\n\tdoi = {10.13140/RG.2.2.24784.17922}\n}\n\u003C/code>\u003C/pre>","blog/2021-01-git-operations-as-planning-problems","dc6e6d10-c460-4d3c-8fe2-4ce7535b4af1","Modelling Git Operations as Planning Problems",true,"Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.",[10,11,12,13],"Git","PDDL","Planning-System","dev",["Date","2021-01-20T00:00:00.000Z"],["Date","2023-09-18T11:41:51.000Z"],"/assets/2021-01-git-operations-as-planning-problems.png","\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>",[13,19,20,21],"git","pddl","planning-system","article","blog",[],"2023-09-02T19:26:59Z",{"tag":19}],"uses":{"params":["slug"]}}]} diff --git a/tags/github-pages.html b/tags/github-pages.html new file mode 100644 index 00000000..c76f6025 --- /dev/null +++ b/tags/github-pages.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️github-pages - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: github-pages

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/github-pages/__data.json b/tags/github-pages/__data.json new file mode 100644 index 00000000..349f8fbb --- /dev/null +++ b/tags/github-pages/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"description":8,"content_tags":9,"date":14,"cover_image":15,"abstract":16,"tags":17,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\" rel=\"nofollow noopener noreferrer\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>\n\u003Cp>If you want to see an example of this method in action, go check out my \u003Ca href=\"https://tiimb.work\" rel=\"nofollow noopener noreferrer\">personal website\u003C/a> on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>\u003C/p>\n\u003Cp>I won't be explaining how to setup a Vue project. If you're looking for a Tutorial on that go check out the awesome \u003Ca href=\"https://vuejs.org/v2/guide/\" rel=\"nofollow noopener noreferrer\">Vue.js Guide\u003C/a>.\u003C/p>\n\u003Cp>So you have setup your awesome Vue project and want to host it on GitHub Pages. The way Muhammad explained it you would build the project using \u003Ccode>npm run build\u003C/code>, commit the \u003Ccode>dist/\u003C/code> folder along with your source files and point GitHub to the dist folder. This might get quite messy because you either have commit messages with the sole purpose of uploading the dist folder or you commit the code changes at the same time which makes it hard to find the relevant changes if you ever want to look at your commits again.\u003C/p>\n\u003Cp>So what can you do about this?\u003C/p>\n\u003Cp>Git to the rescue, let's use a branch that contains all the build files.\u003C/p>\n\u003Ch2>Step 1 - keeping our working branch clean 🛀\u003C/h2>\n\u003Cp>To make sure that the branch we are working from stays clean of any build files we are gonna add a \u003Ccode>.gitignore\u003C/code> file to the root.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># .gitignore\ndist/\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 2 - adding a second branch 🌳\u003C/h2>\n\u003Cp>We are not goint to branch off master like how we would do it if we were to modify our code with the intention to merge it back to the main branch. Instead we are gonna create a squeaky clean new branch that will only ever hold the dist files. After all we will not ever need to merge these two branches together.\u003C/p>\n\u003Cp>We do this by creating a new git repository inside the dist folder:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">cd dist/\ngit init\ngit add .\ngit commit -m 'Deploying my awesome vue app'\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 3 - deploying 🚚\u003C/h2>\n\u003Cp>We are gonna force push our new git repository to a branch on GitHub. This might go against git best practices but since we won't ever checkout this branch we don't have to worry about that.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">git push -f git@github.com:<username>/<repo>.git <branch>\n\u003C/code>\u003C/pre>\n\u003Cp>⚠️ Make sure you double or tripple check your destination branch! You don't want to accidentally overwrite your working branch. Using the branch \u003Ccode>gh-pages\u003C/code> will most likely be a good idea.\u003C/p>\n\u003Ch2>Step 4 - pointing GitHub to the right place 👈\u003C/h2>\n\u003Cp>Now we are almost done. The only thing left is telling GitHub where our assets live.\u003C/p>\n\u003Cp>Go to your repo, on the top right navigate to \u003Ccode>Settings\u003C/code> and scroll down to GitHub pages. Enable it and set your source branch to the branch you force pushed to, for example \u003Ccode>gh-pages\u003C/code>.\u003C/p>\n\u003Ch2>Step 5 - automating everything 😴\u003C/h2>\n\u003Cp>If you don't mind doing this whole process (Step 2 and 3) every time you want to deploy you can stop now. If you're as lazy as me, here is the script I use to deploy with one command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># deploy.sh\n\n#!/usr/bin/env sh\n\n# abort on errors\nset -e\n\n# build\necho Linting..\nnpm run lint\necho Building. this may take a minute...\nnpm run build\n\n# navigate into the build output directory\ncd dist\n\n# if you are deploying to a custom domain\n# echo 'example.com' > CNAME\n\necho Deploying..\ngit init\ngit add -A\ngit commit -m 'deploy'\n\n# deploy\ngit push -f git@github.com:<username>/<repo>.git <branch>\n\ncd -\n\n\u003C/code>\u003C/pre>\n\u003Cp>If your on windows look into the Windows Subsystem for Linus (WSL) it will be worth it.\u003C/p>\n\u003Cp>If you are still reading, thank you very much. This is actually my first article and I'm really happy to hear about any opinions and criticisms.\nHappy Coding ♥\u003C/p>","blog/2019-05-vue-on-github-pages","96054292-eb45-4d8a-9aca-bb050175ff2a","How I use Vue.js on GitHub Pages",true,"How to properly deploy a Vue.js app on GitHub Pages",[10,11,12,13],"GitHub Pages","Vue.js","Javascript","dev",["Date","2019-05-04T00:00:00.000Z"],"/assets/2019-05-vue-on-github-pages.png","\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>",[13,18,19,20],"github-pages","javascript","vue.js","article","blog",[],"2023-09-02T19:26:59Z",{"tag":18}],"uses":{"params":["slug"]}}]} diff --git a/tags/go.html b/tags/go.html new file mode 100644 index 00000000..c87900d4 --- /dev/null +++ b/tags/go.html @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️go - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: go

+ +

"no such file or directory" after enabling CGO in Docker

+ 1/24/2023 +
+

Quick fix for the "no such file or directory" error after enabling CGO, when running in a scratch docker image.

+
+
+ +

First Go Project: A Jam-stack Commenting API

+ 7/12/2022 +
First Go Project: A Jam-stack Commenting API +

I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/go/__data.json b/tags/go/__data.json new file mode 100644 index 00000000..f2afc2ac --- /dev/null +++ b/tags/go/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":188},[2,24,92,170],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":-1,"cover_image_txt":13,"content_tags":14,"abstract":18,"tags":19,"links":-1,"type":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message \u003Ccode>exec /app/indiego: no such file or directory\u003C/code>. I had removed the \u003Ccode>CGO_ENABLE=0\u003C/code> variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the \u003Ccode>scratch\u003C/code> image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.\u003C/p>\n\u003Cp>To include libc into the container, I simply changed the base image from \u003Ccode>scratch\u003C/code> to \u003Ccode>alpine\u003C/code>, which includes libc. This makes the image slightly larger but this seemed way easier than trying to include libc directly.\u003C/p>\n\u003Cp>As a bonus I got to delete the \u003Ccode>/usr/share/zoneinfo\u003C/code> and \u003Ccode>ca-certificates.crt\u003C/code> files, and rely on those provided by alpine.\u003C/p>\n\u003Cp>You can see the commit to IndieGo \u003Ca href=\"https://github.com/Tiim/IndieGo/commit/63968814de7e39f295386bf398b583aa8bf0411c\" rel=\"nofollow noopener noreferrer\">here\u003C/a>.\u003C/p>","blog/2023-01-24-no-such-file-or-directory-cgo","dd580343-9e0f-4754-93dd-25667e6b5859",["Date","2023-01-24T00:00:00.000Z"],["Date","2023-01-24T20:54:11.330Z"],[9],null,"\"no such file or directory\" after enabling CGO in Docker",true,"Quick fix for the \"no such file or directory\" error after enabling CGO, when running in a scratch docker image.","",[15,16,17],"go","cgo","docker","\u003Cp>Today I ran into the an error trying to deploy my go app in docker, where the container refused to start with the extremely helpful message \u003Ccode>exec /app/indiego: no such file or directory\u003C/code>. I had removed the \u003Ccode>CGO_ENABLE=0\u003C/code> variable from the Dockerfile, because I needed to enable cgo for a library. What I found out was that when enabling cgo, the resulting binary is not statically linked anymore and now depends on libc or musl. Since the \u003Ccode>scratch\u003C/code> image does not contain literally anything, the binary can't find the libraries and crashes with the aforementioned error.\u003C/p>",[16,17,15],"article","blog",[],"2023-09-02T19:26:59Z",{"html":25,"slug":26,"uuid":27,"date":28,"aliases":29,"title":30,"published":11,"modified":31,"description":32,"cover_image":33,"content_tags":34,"syndication":40,"abstract":42,"tags":43,"links":-1,"type":20,"folder":21,"comments":46,"latestComment":23},"\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\" rel=\"nofollow noopener noreferrer\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\" rel=\"nofollow noopener noreferrer\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>\n\u003Ch2>The IndieWeb\u003C/h2>\n\u003Cp>I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.\u003C/p>\n\u003Cp>The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.\u003C/p>\n\u003Cp>The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.\u003C/p>\n\u003Cp>The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.\u003C/p>\n\u003Cp>Some of the standards in the IndieWeb include:\u003C/p>\n\u003Cul>\n\u003Cli>Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.\u003C/li>\n\u003Cli>Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.\u003C/li>\n\u003Cli>IndieAuth, an OAuth2-based way to log in using only your domain name.\u003C/li>\n\u003C/ul>\n\u003Ch2>The implementation on my website\u003C/h2>\n\u003Cp>As explained in my earlier post \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">First Go Project: A Jam-stack Commenting API\u003C/a>, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.\u003C/p>\n\u003Ch3>Making the website machine-readable with Microformats\u003C/h3>\n\u003Cp>As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called \u003Ca href=\"http://microformats.org/wiki/h-entry\" rel=\"nofollow noopener noreferrer\">h-entry\u003C/a>. By adding the \u003Ccode>h-entry\u003C/code> class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as \u003Ccode>p-name\u003C/code>, \u003Ccode>p-author\u003C/code> or \u003Ccode>dt-published\u003C/code>.\u003C/p>\n\u003Cp>While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.\u003C/p>\n\u003Cp>Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.\u003C/p>\n\u003Ch3>Accepting comments and other interactions via Webmentions\u003C/h3>\n\u003Cp>The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.\u003C/p>\n\u003Cp>In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.\u003C/p>\n\u003Cp>Since I already have a \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">small custom service\u003C/a> running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions\u003C/p>\n\u003Cp>It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database.\nI found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.\u003C/p>\n\u003Cp>I have tested my webmentions implementation using \u003Ca href=\"https://webmention.rocks\" rel=\"nofollow noopener noreferrer\">webmention.rocks\u003C/a>, but I would appreciate it if you left me a mention as well 😃\u003C/p>\n\u003Ch3>Publishing short-form content such as replies, likes and bookmarks: A notes post type\u003C/h3>\n\u003Cp>The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">notes\u003C/a>. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.\u003C/p>\n\u003Cp>I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: \u003Ca href=\"https://tiim.ch/full-rss.xml\" rel=\"nofollow noopener noreferrer\">full-rss.xml\u003C/a>. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.\u003C/p>\n\u003Ch3>Notifying referenced websites: Sending Webmentions\u003C/h3>\n\u003Cp>Sending webmentions was easy compared to receiving webmentions:\u003C/p>\n\u003Cp>On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.\u003C/p>\n\u003Cp>Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library \u003Ca href=\"https://github.com/go-co-op/gocron\" rel=\"nofollow noopener noreferrer\">gocron\u003C/a> for scheduling the regular intervals, \u003Ca href=\"https://github.com/mmcdole/gofeed\" rel=\"nofollow noopener noreferrer\">gofeed\u003C/a> for parsing the RSS feed and \u003Ca href=\"https://willnorris.com/go/webmention\" rel=\"nofollow noopener noreferrer\">webmention\u003C/a> for extracting links and sending webmentions.\u003C/p>\n\u003Ch3>In the future: IndieAuth\u003C/h3>\n\u003Cp>The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.\u003C/p>\n\u003Cp>Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.\u003C/p>\u003Cdiv class=\"mf2\">\u003Cblockquote class=\"syndication\">This post is also on \u003Cul>\u003Cli>\u003Ca class=\"u-syndication\" href=\"https://news.indieweb.org/en\">news.indieweb.org\u003C/a>\u003C/li>\u003C/ul>\u003C/blockquote>\u003C/div>\n","blog/2022-12-indiewebifying-my-website-part-1","3b342241-c414-4670-bd22-03e13d6531b7",["Date","2022-11-12T10:55:14.000Z"],[9],"IndieWebifying my Website Part 1 - Microformats and Webmentions",["Date","2022-12-03T20:56:54.000Z"],"This site now supports sending and receiving webmentions and surfacing structured data using microformats2.","https://i.imgur.com/FpgIBxI.jpg",[35,36,37,38,15,39],"IndieWeb","Webmentions","mf2","tiim.ch","indiego",[41],"https://news.indieweb.org/en","\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>",[15,39,44,37,38,45],"indieweb","webmentions",[47,52,58,63,69,74,80,86],{"id":48,"type":49,"replyTo":13,"timestamp":23,"page":26,"url":50,"content":13,"name":51},"41435fdd-5fd2-4175-b9ea-ef9ce0dec154","webmention","https://evgenykuznetsov.org/en/reactions/2022/like-337215655/","Evgeny Kuznetsov",{"id":53,"type":49,"replyTo":13,"timestamp":54,"page":26,"url":55,"content":56,"name":57},"68bd3601-4f00-42c1-b28c-1f2ef75ac851","2023-08-02T09:10:03Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...","Tim Bachmann",{"id":59,"type":49,"replyTo":13,"timestamp":60,"page":26,"url":61,"content":62,"name":57},"5f508a42-8b83-4c10-9f7e-9c1b80e23ab1","2022-12-09T10:49:06Z","https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting","Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.",{"id":64,"type":49,"replyTo":13,"timestamp":65,"page":26,"url":66,"content":67,"name":68},"dc8dbf30-ff1f-4e13-ba36-37f12666005c","2022-12-05T08:41:07Z","https://martymcgui.re/2022/12/05/033926/","★ Liked https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","https://martymcgui.re/",{"id":70,"type":49,"replyTo":13,"timestamp":71,"page":26,"url":72,"content":13,"name":73},"6f6c1f11-2ae0-41a4-b7d0-e343ef63aa52","2022-11-27T22:32:27Z","https://brid.gy/like/twitter/tiimb/1591417020557525003/48372745","Jimmy Lipham",{"id":75,"type":49,"replyTo":13,"timestamp":76,"page":26,"url":77,"content":78,"name":79},"5e1c4149-8fb9-48fb-b285-5efbd626b259","2022-11-27T22:31:57Z","https://brid.gy/repost/twitter/tiimb/1591417020557525003/1591418692008558592","I published a new blog post:\nIndieWebifying my Website Part 1 - Microformats and Webmentions\ntiim.ch/blog/2022-12-i…\n#indieweb #microformats #webmentions #golang","Golang Smart Bot",{"id":81,"type":49,"replyTo":13,"timestamp":82,"page":26,"url":83,"content":84,"name":85},"6e0bf830-1735-42a2-9aa2-ea4c40ab7a45","2022-11-15T12:47:35Z","https://webmention.rocks/receive/1/f0fa5421056e068fe902932ef98f6d71","This test verifies that you accept a Webmention request that contains a valid source and target URL. To pass this test, your Webmention endpoint must return either HTTP 200, 201 or 202 along with the appropriate headers.\nIf your endpoint returns HTTP 201, then it MUST also return a Location header. If it returns HTTP 200 or 202, then it MUST NOT include a Location header.","Webmention Rocks!",{"id":87,"type":49,"replyTo":13,"timestamp":88,"page":26,"url":89,"content":90,"name":91},"4e237d08-9f22-480e-a46e-8f40adf06c5e","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/rm8as/","Liked\nIndieWebifying my Website Part 1 - Microformats and Webmentions\nPost detailsThis site now supports sending and receiving webmentions and surfacing structured data using microformats2. https://i.imgur.com/FpgIBxI.jpg","Jamie Tanna",{"html":93,"slug":94,"uuid":95,"date":96,"created":97,"aliases":98,"title":99,"published":11,"modified":100,"description":101,"cover_image":102,"content_tags":103,"abstract":106,"tags":107,"links":-1,"type":20,"folder":21,"comments":108,"latestComment":23},"\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\" rel=\"nofollow noopener noreferrer\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>\n\u003Cp>I kept my eyes open for a service that I liked, but I did not want to just integrate any old service into my website, I did have some requirements:\u003C/p>\n\u003Cul>\n\u003Cli>The service should not cost anything. I would rather host something myself than sign up for another subscription (because I'm already paying for a VPS anyway).\u003C/li>\n\u003Cli>I want to control how the comments on my website are displayed. I quite like my website design and I don't want a generic comment box below my posts.\u003C/li>\n\u003Cli>The service should respect the privacy of the people using my website.\u003C/li>\n\u003Cli>There should be an option to comment without setting up an account with the service.\u003C/li>\n\u003C/ul>\n\u003Cp>While looking around for how other people integrated comments into their static websites, I found a nice \u003Ca href=\"https://averagelinuxuser.com/static-website-commenting/\" rel=\"nofollow noopener noreferrer\">blog post from Average Linux User\u003C/a> which compares a few popular commenting systems.\nUnfortunately, most systems either are not very privacy-friendly, cost money or store the comments as comments on Github issues..?\nAfter looking through the options I decided to use this opportunity to write my own commenting system and dabble with the Go programming language.\u003C/p>\n\u003Ch2>Writing a commenting API in Go\u003C/h2>\n\u003Cp>First thing first, if you want to take a look at the code, check out the \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">Github repo\u003C/a>.\u003C/p>\n\u003Cp>I decided to write the commenting system in Go because I have been looking for an excuse to practice Go for a while, and this seemed like the perfect fit. It is a small CRUD app, consisting of a storage component, an API component and a small event component in the middle to easily compose the functionality I want.\u003C/p>\n\u003Cp>Currently, it supports the following functionality:\u003C/p>\n\u003Cul>\n\u003Cli>Listing all comments (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Listing all comments for a specified page (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Posting comments through the API\u003C/li>\n\u003Cli>A simple admin dashboard that lists all comments and allows the admin to delete them\u003C/li>\n\u003Cli>Email notifications when someone comments\u003C/li>\n\u003Cli>Email notifications when someone replies to your comment\u003C/li>\n\u003Cli>SQLite storage for comments\u003C/li>\n\u003C/ul>\n\u003Cp>The code is built in a way to make it easy to customise the features.\nFor example to disable features like the email reply notifications you can just \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/main.go#L52\" rel=\"nofollow noopener noreferrer\">comment out the line in the main.go\u003C/a> file that registers that hook.\u003C/p>\n\u003Cp>To write custom hooks that get executed when a new comment gets submitted or one gets deleted, just implement the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/event/handler.go\" rel=\"nofollow noopener noreferrer\">Handler\u003C/a> interface and register it in the main method.\u003C/p>\n\u003Cp>You can also easily add other storage options like databases or file storage by implementing the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/model/store.go\" rel=\"nofollow noopener noreferrer\">Store and SubscribtionStore\u003C/a> interfaces.\u003C/p>\n\u003Ch2>Can it be used in production? 🚗💨\u003C/h2>\n\u003Cp>I currently use it on this website! Go test it out (I might delete the comments if they are rude though 🤔).\u003C/p>\n\u003Cp>In all seriousness, I would not use it for a website where the comments are critical. But for a personal blog or similar, I don't see why not.\u003C/p>\n\u003Cp>If you want to host your own version, there is a Dockerfile available. If you decide to integrate this into your website, please comment below, ping me \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> or shoot me an email \u003Ca href=\"mailto:hey@tiim.ch\">hey@tiim.ch\u003C/a>, I would love to check it out.\u003C/p>","blog/2022-07-12-first-go-project-commenting-api","bff14052-4f3f-4dcb-bcee-155ae1c6b09e",["Date","2022-07-12T00:00:00.000Z"],["Date","2022-07-08T16:24:37.766Z"],[9],"First Go Project: A Jam-stack Commenting API",["Date","2022-11-23T21:42:29.000Z"],"I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!","/assets/2022-07-first-go-project-commenting-api.png",[15,104,105,38,39],"web-api","project","\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>",[15,39,105,38,104],[109,112,119,126,131,137,141,146,153,159,164],{"id":110,"type":49,"replyTo":13,"timestamp":111,"page":94,"url":55,"content":56,"name":57},"31ec5f44-15b2-498a-890d-350e38b9a83e","2023-08-02T09:10:04Z",{"id":113,"type":114,"replyTo":13,"timestamp":115,"page":94,"url":116,"content":117,"name":118},"6d792d24-ba58-4408-83a4-3583667ff4ad","comment","2023-07-09T19:25:02Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#6d792d24-ba58-4408-83a4-3583667ff4ad","Heya just saw your post on Reddit about this comment feature, didn't want to leave without using it ^^. Nicely done!","Anonymous",{"id":120,"type":114,"replyTo":121,"timestamp":122,"page":94,"url":123,"content":124,"name":125},"99dd9ccf-5349-4f41-9553-67986e1a1074","1c8ba0da-10df-4a7a-b067-55875441de2d","2022-12-07T23:01:37Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#99dd9ccf-5349-4f41-9553-67986e1a1074","You are right, there is not any documentation in the readme yet. Although hopefully, I will work on that soon. I'm in the middle of refactoring the project.\n\nTo query the comments there are two rest endpoints [\"/comment\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L41-L71) and [\"/comment/:page\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L73-L107) which return all comments or the comments for a specific page. The comments are loaded from this API endpoint when the site is generated.\n\nTo display the comments without rebuilding the site, new comments are fetched in the browser with the \"?since=\u003Ctime-of-last-build>\" query parameter.","Tiim",{"id":121,"type":114,"replyTo":13,"timestamp":127,"page":94,"url":128,"content":129,"name":130},"2022-12-07T22:33:44Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#1c8ba0da-10df-4a7a-b067-55875441de2d","Good stuff. Always nice to see a site supporting comments and/or Webmentions. Maybe I missed it in the readme but I am curious as to how one queries the API for comments. Do you pull in the comments from the database when you generate the site?","Poorchop",{"id":132,"type":49,"replyTo":13,"timestamp":133,"page":94,"url":134,"content":135,"name":136},"e42756e4-d5a5-4727-8c58-434d285b7ab3","2022-11-27T22:31:58Z","https://brid.gy/repost/twitter/tiimb/1546801590593638400/1546801615264415745","I published a new blog post:\nFirst Go Project: A jam-stack Commenting API\ntiim.ch/blog/2022-07-1…\n#golang #jamstack #API","Golang Bot",{"id":138,"type":49,"replyTo":13,"timestamp":139,"page":94,"url":140,"content":32,"name":57},"b8d3ae8b-0059-4379-8c35-30c97269908f","2022-11-21T22:19:23Z","https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1",{"id":142,"type":114,"replyTo":13,"timestamp":143,"page":94,"url":144,"content":145,"name":145},"171e3444-f1d0-492d-8bc7-c0a133a41783","2022-07-18T08:44:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#171e3444-f1d0-492d-8bc7-c0a133a41783","hola",{"id":147,"type":114,"replyTo":148,"timestamp":149,"page":94,"url":150,"content":151,"name":152},"5875bba1-e69b-467a-bab5-23ef4160d257","621574fd-eea2-48d6-87c8-aebd0f05f1aa","2022-07-13T21:06:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#5875bba1-e69b-467a-bab5-23ef4160d257","And a polite reply","polite",{"id":154,"type":114,"replyTo":13,"timestamp":155,"page":94,"url":156,"content":157,"name":158},"8494c653-ef37-47a2-ae1b-00d00e4815a9","2022-07-13T18:31:31Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#8494c653-ef37-47a2-ae1b-00d00e4815a9","Pretty cool dudez","somGuy",{"id":148,"type":114,"replyTo":13,"timestamp":160,"page":94,"url":161,"content":162,"name":163},"2022-07-12T21:40:39Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#621574fd-eea2-48d6-87c8-aebd0f05f1aa","This is a rude comment ;)","rude",{"id":165,"type":114,"replyTo":13,"timestamp":166,"page":94,"url":167,"content":168,"name":169},"fb6278ae-e48c-4397-ba29-bec4e5cb3a57","2022-07-12T13:30:14Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#fb6278ae-e48c-4397-ba29-bec4e5cb3a57","Good job dude!","wdup",{"html":171,"slug":172,"uuid":173,"title":174,"date":175,"modified":9,"section":176,"published":11,"cover_image":102,"content_tags":177,"links":181,"abstract":184,"tags":185,"type":20,"description":13,"folder":186,"comments":187,"latestComment":23},"\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>\n\u003Cul>\n\u003Cli>The basic commenting system\u003C/li>\n\u003Cli>Sending and receiving webmentions\u003C/li>\n\u003Cli>Micropub server implementation\u003C/li>\n\u003Cli>IndieAuth (decentralized authentication standard based on OAuth)\u003C/li>\n\u003Cli>Admin dashboard\u003C/li>\n\u003Cli>Admin backup endpoint\u003C/li>\n\u003C/ul>\n\u003Cp>Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and\ncomments through the fediverse show up back on my website.\u003C/p>\n\u003Cp>The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core\nof the application is only concerned with initializing those plugins.\u003C/p>\n\u003Cp>If you have any questions, or want to run IndieGo yourself, don't hesitate to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>.\u003C/p>","projects/indiego","0cf125b3-a99a-4996-8f84-ec5105d64c57","IndieGo",["Date","2023-08-02T08:39:00.000Z"],"Projects",[15,178,44,17,179,180],"golang","sqlite","dev",[182,183],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">IndieGo Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://comments.tiim.ch\" rel=\"nofollow noopener noreferrer\">Admin Interface\u003C/a> - authentication required\u003C/p>","\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>",[180,17,15,178,44,179],"projects",[],{"tag":15}],"uses":{"params":["slug"]}}]} diff --git a/tags/golang.html b/tags/golang.html new file mode 100644 index 00000000..0010a930 --- /dev/null +++ b/tags/golang.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️golang - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: golang

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/golang/__data.json b/tags/golang/__data.json new file mode 100644 index 00000000..1860c88f --- /dev/null +++ b/tags/golang/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":60},[2,29],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"cover_image":11,"content_tags":12,"links":19,"abstract":22,"tags":23,"type":24,"description":25,"folder":26,"comments":27,"latestComment":28},"\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>\n\u003Cul>\n\u003Cli>The basic commenting system\u003C/li>\n\u003Cli>Sending and receiving webmentions\u003C/li>\n\u003Cli>Micropub server implementation\u003C/li>\n\u003Cli>IndieAuth (decentralized authentication standard based on OAuth)\u003C/li>\n\u003Cli>Admin dashboard\u003C/li>\n\u003Cli>Admin backup endpoint\u003C/li>\n\u003C/ul>\n\u003Cp>Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and\ncomments through the fediverse show up back on my website.\u003C/p>\n\u003Cp>The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core\nof the application is only concerned with initializing those plugins.\u003C/p>\n\u003Cp>If you have any questions, or want to run IndieGo yourself, don't hesitate to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>.\u003C/p>","projects/indiego","0cf125b3-a99a-4996-8f84-ec5105d64c57","IndieGo",["Date","2023-08-02T08:39:00.000Z"],null,"Projects",true,"/assets/2022-07-first-go-project-commenting-api.png",[13,14,15,16,17,18],"go","golang","indieweb","docker","sqlite","dev",[20,21],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">IndieGo Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://comments.tiim.ch\" rel=\"nofollow noopener noreferrer\">Admin Interface\u003C/a> - authentication required\u003C/p>","\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>",[18,16,13,14,15,17],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":30,"slug":31,"name":32,"date":33,"content_tags":34,"like_of":36,"raw_data":38,"tags":55,"links":-1,"published":10,"type":56,"cover_image":-1,"description":57,"folder":58,"comments":59,"latestComment":28},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==\">https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/12/mte4nd","How Caddy 2 works, a deep dive into the source",["Date","2022-12-04T13:08:00.000Z"],[35,14,18],"caddy",{"url":37},"https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==",{"items":39,"rels":53,"relurls":54},[40],{"id":25,"value":25,"html":25,"type":41,"properties":43,"shape":25,"coords":25,"children":52},[42],"h-entry",{"category":44,"like-of":45,"name":46,"post-status":48,"published":50},[35,14,18],[37],[47]," How Caddy 2 works, a deep dive into the source",[49],"published",[51],"2022-12-04T14:08:00+0100",[],{},{},[35,18,14],"like","👍 Liked: https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTM2Nw==","mf2",[],{"tag":14}],"uses":{"params":["slug"]}}]} diff --git a/tags/graphql.html b/tags/graphql.html new file mode 100644 index 00000000..2c2ca65f --- /dev/null +++ b/tags/graphql.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️graphql - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: graphql

+ +

SvelteKit Server-Side Rendering (SSR) with @urql/svelte

+ 9/26/2022 +
SvelteKit Server-Side Rendering (SSR) with @urql/svelte +

Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/graphql/__data.json b/tags/graphql/__data.json new file mode 100644 index 00000000..9ed8f34f --- /dev/null +++ b/tags/graphql/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":57},[2,34],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":33},"\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\" rel=\"nofollow noopener noreferrer\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>\n\u003Cp>Server-side rendering (SSR) is one of the great features of SvelteKit. I will try to keep this blog post short and will therefore not explain what server-side rendering is and why you should take advantage of it \u003Cem>(you really should!)\u003C/em>. If you want to know more about SSR you can take a look at this article: \u003Ca href=\"https://towardsdev.com/server-side-rendering-srr-in-javascript-a1b7298f0d04\" rel=\"nofollow noopener noreferrer\">A Deep Dive into Server-Side Rendering (SSR) in JavaScript\u003C/a>.\u003C/p>\n\u003Ch2>Background - SSR in SvelteKit\u003C/h2>\n\u003Cp>SvelteKit implements SSR by providing a \u003Ca href=\"https://kit.svelte.dev/docs/load\" rel=\"nofollow noopener noreferrer\">\u003Ccode>load\u003C/code> function\u003C/a> for every layout and page component. If a page or layout needs to perform some asynchronous operation, this should be done inside of this load function. SvelteKit executes this function asynchronously on the server side as well as on the client side and the return value of this function is assigned to the \u003Ccode>data\u003C/code> prop of the associated component. Usually, this asynchronous operation is loading data from an external service, like in the case of this blog post a GraphQL server.\nYou can of course load data directly in the component, but SvelteKit will not wait for this to complete when doing SSR, and the resulting HTML will not include the loaded data.\u003C/p>\n\u003Ch2>Background - @urql/svelte\u003C/h2>\n\u003Cp>The urql library allows us to easily issue GraphQL queries and mutations. Some of the functionality it has to make our lives easier include:\u003C/p>\n\u003Cul>\n\u003Cli>Reloading a query when a query variable changes\u003C/li>\n\u003Cli>Reloading a query after a mutation that touches the same data as the query\u003C/li>\n\u003C/ul>\n\u003Cp>We want to keep these features, even when using urql when doing SSR.\u003C/p>\n\u003Ch2>The Problem\u003C/h2>\n\u003Cp>When implementing SSR in my project, I ran into two problems. I couldn't find any documentation or any articles solving them, so I decided to write down my solutions to those problems in this blog post.\u003C/p>\n\u003Ch3>Problem 1 - Svelte and urql Reactivity\u003C/h3>\n\u003Cp>Let's say we have the following load function, which executes a GraphQL query to load a list of red cars:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\n\n/** @type {import('./$types').PageLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const carColor = \"red\";\n\n const cars = client\n .query(carsQuery, {\n color: carColor,\n })\n .toPromise()\n .then((c) => c.data?.car);\n\n return {\n cars,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>This example uses the urql method \u003Ccode>client.query\u003C/code> to start a query to get us a list of cars with a red colour (The GraphQL query is not shown but the exact query is not important for this example).\nThe client gets a \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">special fetch function\u003C/a> from the event which has a few nice properties, like preventing a second network request on the client side if that same request was just issued on the server-side.\u003C/p>\n\u003Cp>Since the query code is now located in the load function and not in a svelte component, there is no way to easily change the \u003Ccode>carColor\u003C/code> and have urql automatically reload the query. The only way to change the variable is to set the value as a query parameter and read that from the \u003Ccode>event\u003C/code> argument. This however means that we have to refresh the whole page just to reload this query.\u003C/p>\n\u003Cp>The other thing urql does for us, reloading the query when we do a mutation on the same data, will not work with the above code either.\u003C/p>\n\u003Ch3>The solution: A query in the load function and a query in the component\u003C/h3>\n\u003Cp>To fix those two drawbacks we have to add the same query as in the load function to our component code as well. Unfortunately, this means when a user loads the page, it sends a request from the client side, even though the same request got sent from the server side already.\u003C/p>\n\u003Cp>I created a small wrapper function \u003Ccode>queryStoreInitialData\u003C/code> that creates the query inside of the component and intelligently switches from the (possibly stale) data from the load function to the new data. Using this wrapper, the page or layout might look as follows:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-svelte\"><script>\n import { queryStoreInitialData } from \"@/lib/gql-client\"; // The helper function mentioned above\n import { getContextClient } from \"@urql/svelte\";\n import { carsQuery } from \"./query\"; // The query\n\n export let data;\n\n $: gqlStore = queryStoreInitialData(\n {\n client: getContextClient(),\n query: carsQuery,\n },\n data.cars\n );\n $: cars = $gqlStore?.data?.car;\n</script>\n\n<div>\n <pre>\n {JSON.stringify(cars, null, 2)}\n </pre>\n</div>\n\u003C/code>\u003C/pre>\n\u003Col>\n\u003Cli>The native \u003Ccode>queryStore\u003C/code> function gets replaced with the wrapper function.\u003C/li>\n\u003Cli>The initial value of the query is supplied to the wrapper\u003C/li>\n\u003C/ol>\n\u003Cp>Unfortunately, we can not return the query result from the load function directly like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const result = await client.query(cars, {}).toPromise();\n\nreturn {\n cars: toInitialValue(result),\n};\n\u003C/code>\u003C/pre>\n\u003Cp>This results in the following error:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-stacktrace\">Cannot stringify a function (data.events.operation.context.fetch)\nError: Cannot stringify a function (data.events.operation.context.fetch)\n at render_response (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/render.js:181:20)\n at runMicrotasks (<anonymous>)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async render_page (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/index.js:276:10)\n at async resolve (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:232:17)\n at async respond (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:284:20)\n at async file:///app/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:406:22\n\u003C/code>\u003C/pre>\n\u003Cp>This is because the query result contains data that is not serializable.\nTo fix this I created the \u003Ccode>toInitialValue\u003C/code> function, which deletes all non-serializable elements from the result. The load function now looks like follows;\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\nimport { createServerClient, toInitialValue } from \"@/lib/gql-client\";\nimport { parse } from \"cookie\";\nimport { carsQuery } from \"./query\";\n\n/** @type {import('./$types').PageServerLoad} */\nexport const load = async (event) => {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const result = await client.query(cars, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n};\n\u003C/code>\u003C/pre>\n\u003Ch3>Problem 2 - Authentication\u003C/h3>\n\u003Cp>We will look at the same \u003Ccode>load\u003C/code> function as #Problem 1 - Svelte and urql Reactivity: the function creates a urql client with the fetch function from the event object and uses this client to send a query.\u003C/p>\n\u003Cp>Sometimes however the GraphQL API requires authentication in the form of a cookie to allow access.\u003C/p>\n\u003Cp>Unfortunately, the \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">fetch function that we get from the load event\u003C/a> will only pass the cookies on if the requested domain is the same as the base domain or a more specific subdomain of it. This means if your SvelteKit site runs on \u003Ccode>example.com\u003C/code> and your GraphQL server runs on \u003Ccode>gql.example.com\u003C/code> then the cookies will get forwarded and everything is fine. This however is, in my experience, often not the case. Either you might use an external service for your GraphQL API or you host it yourself and want to use its internal domain.\u003C/p>\n\u003Cp>The only way to pass the cookies on to the GraphQL server, in this case, is by manually setting the cookie header when creating the urql client. This however forces us to use the server-only load function, as we do not have access to the cookie header in the normal load function.\u003C/p>\n\u003Cp>The new code now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch,\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // inject the cookie header\n // FIXME: change the cookie name\n Cookie: `gql-session=${event.cookies.get(\"gql-session\")}`,\n },\n },\n });\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>To keep the size of the load functions across my codebase smaller I created a small wrapper function \u003Ccode>createServerClient\u003C/code>:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createServerClient(event.cookies);\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Ch2>The Code\u003C/h2>\n\u003Cp>Below you can find the three functions \u003Ccode>createServerClient\u003C/code>, \u003Ccode>queryStoreInitialData\u003C/code> and \u003Ccode>toInitialValue\u003C/code> that we used above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/lib/gql-client.js\n\nimport { browser } from \"$app/environment\";\nimport { urls } from \"@/config\";\nimport { createClient, queryStore } from \"@urql/svelte\";\nimport { derived, readable } from \"svelte/store\";\n\n/**\n * Helper function to create an urql client for a server-side-only load function\n *\n *\n * @param {import('@sveltejs/kit').Cookies} cookies\n * @returns\n */\nexport function createServerClient(cookies) {\n return createClient({\n // FIXME: adjust your graphql url\n url: urls.gql,\n fetch,\n // FIXME: if you don't need to authenticate, delete the following object:\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // FIXME: if you want to set a cookie adjust the cookie name\n Cookie: `gql-session=${cookies.get(\"gql-session\")}`,\n },\n },\n });\n}\n\n/**\n * Helper method to send a GraphQL query but use the data from the SvelteKit load function initially.\n *\n *\n * @param {any} queryArgs\n * @param {any} initialValue\n * @returns\n */\nexport function queryStoreInitialData(queryArgs, initialValue) {\n if (!initialValue || (!initialValue.error && !initialValue.data)) {\n throw new Error(\"No initial value from server\");\n }\n\n let query = readable({ fetching: true });\n if (browser) {\n query = queryStore(queryArgs);\n }\n\n return derived(query, (value, set) => {\n if (value.fetching) {\n set({ ...initialValue, source: \"server\", fetching: true });\n } else {\n set({ ...value, source: \"client\" });\n }\n });\n}\n\n/**\n * Make the result object of a urql query serialisable.\n *\n *\n * @template T\n * @param {Promise<import('@urql/svelte').OperationResult<T, any >>|import('@urql/svelte').OperationResult<T, any >} result\n * @returns {Promise<{fetching:false, error: undefined | {name?: string, message?: string; graphQLErrors?: any[]; networkError?: Error; response?: any;}, data: T|undefined}>}\n */\nexport async function toInitialValue(result) {\n const { error, data } = await result;\n\n // required to turn class array into array of javascript objects\n const errorObject = error ? {} : undefined;\n if (errorObject) {\n console.warn(error);\n errorObject.graphQLErrors = error?.graphQLErrors?.map((e) => ({ ...e }));\n errorObject.networkError = { ...error?.networkError };\n errorObject.response = { value: \"response omitted\" };\n }\n\n return {\n fetching: false,\n error: { ...error, ...errorObject },\n data,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Ca href=\"https://gist.github.com/Tiim/1adeb4d74ce7ae09d0d0aa4176a6195d\" rel=\"nofollow noopener noreferrer\">Link to the Gist\u003C/a>\u003C/p>\n\u003Ch2>End remarks\u003C/h2>\n\u003Cp>Even though I think this solution is not too bad, I wish @urql/svelte would implement a better way to handle SSR with sveltekit. I posted a \u003Ca href=\"https://github.com/FormidableLabs/urql/discussions/2703\" rel=\"nofollow noopener noreferrer\">question on the urql GitHub discussions board\u003C/a>, but I have not gotten any response yet.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>This article was written with \u003Ccode>@svelte/kit\u003C/code> version \u003Ccode>1.0.0-next.499\u003C/code> and \u003Ccode>@urql/svelte\u003C/code> version \u003Ccode>3.0.1\u003C/code>.\nI will try to update this article as I update my codebase to newer versions.\u003C/p>\n\u003C/blockquote>\n\u003Cp>If this post helped you, or you found a better or different way to solve SSR with urql, please let me know in the comments, write me an email or tag me on twitter \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a>.\u003C/p>","blog/2022-09-27-sveltekit-ssr-with-urql","1e223cab-bca2-4b3b-a75a-71f158c90cba",["Date","2022-09-26T00:00:00.000Z"],["Date","2022-09-26T08:55:23.886Z"],[9],null,"SvelteKit Server-Side Rendering (SSR) with @urql/svelte",true,"Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.","https://i.imgur.com/5DBIbbT.png",[15,16,17,18],"urql","sveltekit","SSR","graphql","\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>",[18,21,16,15],"ssr","article","blog",[25],{"id":26,"type":27,"replyTo":28,"timestamp":29,"page":4,"url":30,"content":31,"name":32},"a38babef-2c4c-41e6-a748-8723b6cc34ef","comment","","2023-02-05T00:02:51Z","https://tiim.ch/blog/2022-09-27-sveltekit-ssr-with-urql#a38babef-2c4c-41e6-a748-8723b6cc34ef","Hi\n\ninspiring article. Anyway, inspired with it I tried to find more seamless integration - get SSR rendered queries but keep original interface. \n\nYou may be interested in my approach, it's dropped in discussion you open \nhttps://github.com/urql-graphql/urql/discussions/2703","Farin","2023-09-02T19:26:59Z",{"html":35,"slug":36,"uuid":37,"title":38,"date":39,"modified":40,"section":41,"published":11,"content_tags":42,"links":49,"abstract":53,"tags":54,"type":22,"cover_image":-1,"description":28,"folder":55,"comments":56,"latestComment":33},"\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\" rel=\"nofollow noopener noreferrer\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>\n\u003Cp>The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents.\nThe admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/aqualetics-coach-screenshot.png\" alt=\"Screenshot of the coaches view\">\u003C/p>\n\u003Cp>The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.\u003C/p>","projects/aqualetics-coach","3210d289-1f9e-41b5-b1f9-d20f00f6a0c5","Aqualetics Coach",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",[43,44,18,45,46,47,48],"node","vue","hasura","postgresql","docker","dev",[50,51,52],"\u003Cp>\u003Ca href=\"https://sundrbi.ch/coach-application/\" rel=\"nofollow noopener noreferrer\">Overview\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch/2019/09/15/schwimmcoach-applikation-innovation/\" rel=\"nofollow noopener noreferrer\">Blog Post 🇩🇪\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch\" rel=\"nofollow noopener noreferrer\">Aqualetics Swim School\u003C/a>\u003C/p>","\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>",[48,47,18,45,43,46,44],"projects",[],{"tag":18}],"uses":{"params":["slug"]}}]} diff --git a/tags/hasura.html b/tags/hasura.html new file mode 100644 index 00000000..01ee3097 --- /dev/null +++ b/tags/hasura.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️hasura - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: hasura

+ +

TeamKit

+ 11/27/2022 +
+

TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/hasura/__data.json b/tags/hasura/__data.json new file mode 100644 index 00000000..13b88080 --- /dev/null +++ b/tags/hasura/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":45},[2,30],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":19,"abstract":23,"tags":24,"type":25,"cover_image":-1,"description":26,"folder":27,"comments":28,"latestComment":29},"\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\" rel=\"nofollow noopener noreferrer\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>\n\u003Cp>The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents.\nThe admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/aqualetics-coach-screenshot.png\" alt=\"Screenshot of the coaches view\">\u003C/p>\n\u003Cp>The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.\u003C/p>","projects/aqualetics-coach","3210d289-1f9e-41b5-b1f9-d20f00f6a0c5","Aqualetics Coach",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14,15,16,17,18],"node","vue","graphql","hasura","postgresql","docker","dev",[20,21,22],"\u003Cp>\u003Ca href=\"https://sundrbi.ch/coach-application/\" rel=\"nofollow noopener noreferrer\">Overview\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch/2019/09/15/schwimmcoach-applikation-innovation/\" rel=\"nofollow noopener noreferrer\">Blog Post 🇩🇪\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch\" rel=\"nofollow noopener noreferrer\">Aqualetics Swim School\u003C/a>\u003C/p>","\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>",[18,17,14,15,12,16,13],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":31,"slug":32,"uuid":33,"title":34,"date":35,"modified":36,"section":9,"published":10,"content_tags":37,"links":40,"abstract":42,"tags":43,"type":25,"cover_image":-1,"description":26,"folder":27,"comments":44,"latestComment":29},"\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>\n\u003Cp>I built TeamKit because the swim club I am a part of needed an easy way for coaches to have an overview of their teams, handle attendance and track their own hours (not implemented yet). I tried a bunch of existing apps and services, but all of them were either too clunky for us or required the team members to sign up as well. This was a dealbreaker for us because we have a bunch of kids teams which are too young to sign up to websites, and because many of the parents are not very tech literate.\u003C/p>\n\u003Cp>With TeamKit a coach can quickly add new team members to a team, create new members directly in the event view (useful for example a person that just wants to try out) without having to add more details than a name.\u003C/p>\n\u003Cp>In the latest update, TeamKit now allows users to create notes on events. This is useful for planning, writing quick notes for an event or a practice session and for sharing information with other coaches.\u003C/p>\n\u003Cp>If you are interested, TeamKit is currenlty free while it is still in beta.\u003C/p>","projects/teamkit","a3040709-cd2d-43cb-ab33-2061ba1ae061","TeamKit",["Date","2022-11-27T09:19:08.000Z"],null,[38,15,39,18],"sveltekit","postgres",[41],"\u003Cp>\u003Ca href=\"https://teamkit.cc\" rel=\"nofollow noopener noreferrer\">TeamKit\u003C/a>\u003C/p>","\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>",[18,15,39,38],[],{"tag":15}],"uses":{"params":["slug"]}}]} diff --git a/tags/heuristic.html b/tags/heuristic.html new file mode 100644 index 00000000..a67040d3 --- /dev/null +++ b/tags/heuristic.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️heuristic - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: heuristic

+ +

Automated Planning using Property-Directed Reachability with Seed Heuristics

+ 5/6/2023 +
Automated Planning using Property-Directed Reachability with Seed Heuristics +

Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/heuristic/__data.json b/tags/heuristic/__data.json new file mode 100644 index 00000000..a5fa5848 --- /dev/null +++ b/tags/heuristic/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":9,"published":10,"modified":11,"description":12,"cover_image":13,"cover_image_txt":8,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>\n\u003Cp>\u003Ca href=\"https://www.researchgate.net/publication/373994137_Automated_Planning_using_Property-Directed_Reachability_with_Seed_Heuristics\" rel=\"nofollow noopener noreferrer\">Download PDF\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-bibtex\">@phdthesis{bachmann2023,\n author = {Bachmann, Tim},\n year = {2023},\n month = {05},\n title = {Automated Planning using Property-Directed Reachability with Seed Heuristics},\n doi = {10.13140/RG.2.2.11456.30727},\n type = {Master's Thesis},\n school = {University of Basel}\n}\n\u003C/code>\u003C/pre>","blog/2023-05-06-pdr-with-seed-heuristics","111e68c4-0285-4f21-ab36-4c1ce1989da1",["Date","2023-05-06T11:15:53.000Z"],["Date","2023-05-06T11:15:53.000Z"],null,"Automated Planning using Property-Directed Reachability with Seed Heuristics",true,["Date","2023-09-18T13:32:00.000Z"],"Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.","https://media.tiim.ch/023c1722-ac3d-45fd-b66c-9ff319dfc180.webp",[15,16,17,18],"dev","planning-system","pdr","heuristic","\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>",[15,18,17,16],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":18}],"uses":{"params":["slug"]}}]} diff --git a/tags/how-to.html b/tags/how-to.html new file mode 100644 index 00000000..487f105f --- /dev/null +++ b/tags/how-to.html @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️how-to - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: how-to

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/how-to/__data.json b/tags/how-to/__data.json new file mode 100644 index 00000000..9ffc41a1 --- /dev/null +++ b/tags/how-to/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":23},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"date":8,"description":9,"cover_image":10,"content_tags":11,"abstract":17,"tags":18,"links":-1,"type":19,"folder":20,"comments":21,"latestComment":22},"\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>\n\u003Cp>\u003Cem>TLDR\u003C/em>:\u003C/p>\n\u003Cul>\n\u003Cli>Either use Bluetooth Audio Receiver from the Microsoft Store to connect you phone via Bluetooth,\u003C/li>\n\u003Cli>Or use an audio cable to connect the phone to the \"line-in\" on your PC.\u003C/li>\n\u003C/ul>\n\u003Ch2>Bluetooth (recommended)\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>: A PC with integrated Bluetooth or a Bluetooth dongle.\u003C/p>\n\u003Cp>I recommend this approach more than the wired one because it is way less effort, you don't have to deal with a USB or lightning to audio dongle and in my opinion it is more reliable.\u003C/p>\n\u003Cp>Pair your phone with your PC as normal, by opening the Bluetooth settings on your phone and on the PC and wait for the devices to show up. When you successfully paired the phone once you will not have to do this again. Now you need an app that will tell the phone that it can use the PC as a wireless speaker. The only app I found that will do this is the \u003Ca href=\"https://www.microsoft.com/de-de/p/bluetooth-audio-receiver/9n9wclwdqs5j\" rel=\"nofollow noopener noreferrer\">Bluetooth Audio Receiver\u003C/a> app from the Windows Store. Install and and open it. You should see your phone on the list of Bluetooth devices on the app. Select it and click on the \u003Ccode>Open Connection\u003C/code> button. It might take a moment but after it connected, you should hear all sounds from your phone on your PC.\u003C/p>\n\u003Ch2>Wired\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>:\u003C/p>\n\u003Cul>\n\u003Cli>Male-to-Male audio cable (3.5mm audio jack).\u003C/li>\n\u003Cli>A line-in port on your PC (usually blue audio jack on the back)\u003C/li>\n\u003Cli>USB-C to audio jack adapter (Optional)\u003C/li>\n\u003Cli>Lighting to audio jack adapter (Optional)\u003C/li>\n\u003C/ul>\n\u003Cp>This approach works if your PC doesn't support Bluetooth, or if the Bluetooth connection drops for some reason. Connect the audio cable to the blue line-in jack on the back of the computer. Then, connect the phone to the other end of the audio cable. If your phone does not have an audio jack, use the adapter on the USB-C or Lightning port. If your PC detects that you connected a new line-in device, it might open the audio settings automatically. If not, right-click on the volume icon on the taskbar next to the clock and select \u003Ccode>Sounds\u003C/code>. Navigate to the \u003Ccode>Input\u003C/code> tab and double click on the Line-In entry (the one with a cable icon). Navigate to the Monitor tab and select the check box for \"Use this device as a playback source\". This will tell windows it should play all sounds received through this input directly to the speakers. Usually this is used to monitor microphones but it works for this use case too. You should now hear any sound from your phone through your PC headphones or speakers. Make sure you turn this checkbox off when you disconnect your phone. Otherwise you might hear a crackle or other sounds when the loose cable gets touched.\u003C/p>\n\u003Cp>\u003Cem>Photo by Lisa Fotios from Pexels\u003C/em>\u003C/p>","blog/2022-02-phone-audio-to-pc","be57f2df-d58f-4b79-8a51-e20d482f46cf","How to Listen to Phone Audio on PC",true,["Date","2022-02-12T00:00:00.000Z"],"Learn how to connect your phone audio to your PC over wire or Bluetooth.","/assets/2022-02-phone-audio-to-pc.jpg",[12,13,14,15,16],"how-to","audio","windows","bluetooth","software","\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>",[13,15,12,16,14],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":12}],"uses":{"params":["slug"]}}]} diff --git a/tags/indiego.html b/tags/indiego.html new file mode 100644 index 00000000..c211f52d --- /dev/null +++ b/tags/indiego.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️indiego - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: indiego

+ +

First Go Project: A Jam-stack Commenting API

+ 7/12/2022 +
First Go Project: A Jam-stack Commenting API +

I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/indiego/__data.json b/tags/indiego/__data.json new file mode 100644 index 00000000..0ca4c6dd --- /dev/null +++ b/tags/indiego/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":155},[2,77],{"html":3,"slug":4,"uuid":5,"date":6,"aliases":7,"title":9,"published":10,"modified":11,"description":12,"cover_image":13,"content_tags":14,"syndication":21,"abstract":23,"tags":24,"links":-1,"type":27,"folder":28,"comments":29,"latestComment":34},"\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\" rel=\"nofollow noopener noreferrer\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\" rel=\"nofollow noopener noreferrer\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>\n\u003Ch2>The IndieWeb\u003C/h2>\n\u003Cp>I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.\u003C/p>\n\u003Cp>The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.\u003C/p>\n\u003Cp>The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.\u003C/p>\n\u003Cp>The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.\u003C/p>\n\u003Cp>Some of the standards in the IndieWeb include:\u003C/p>\n\u003Cul>\n\u003Cli>Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.\u003C/li>\n\u003Cli>Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.\u003C/li>\n\u003Cli>IndieAuth, an OAuth2-based way to log in using only your domain name.\u003C/li>\n\u003C/ul>\n\u003Ch2>The implementation on my website\u003C/h2>\n\u003Cp>As explained in my earlier post \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">First Go Project: A Jam-stack Commenting API\u003C/a>, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.\u003C/p>\n\u003Ch3>Making the website machine-readable with Microformats\u003C/h3>\n\u003Cp>As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called \u003Ca href=\"http://microformats.org/wiki/h-entry\" rel=\"nofollow noopener noreferrer\">h-entry\u003C/a>. By adding the \u003Ccode>h-entry\u003C/code> class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as \u003Ccode>p-name\u003C/code>, \u003Ccode>p-author\u003C/code> or \u003Ccode>dt-published\u003C/code>.\u003C/p>\n\u003Cp>While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.\u003C/p>\n\u003Cp>Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.\u003C/p>\n\u003Ch3>Accepting comments and other interactions via Webmentions\u003C/h3>\n\u003Cp>The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.\u003C/p>\n\u003Cp>In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.\u003C/p>\n\u003Cp>Since I already have a \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">small custom service\u003C/a> running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions\u003C/p>\n\u003Cp>It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database.\nI found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.\u003C/p>\n\u003Cp>I have tested my webmentions implementation using \u003Ca href=\"https://webmention.rocks\" rel=\"nofollow noopener noreferrer\">webmention.rocks\u003C/a>, but I would appreciate it if you left me a mention as well 😃\u003C/p>\n\u003Ch3>Publishing short-form content such as replies, likes and bookmarks: A notes post type\u003C/h3>\n\u003Cp>The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">notes\u003C/a>. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.\u003C/p>\n\u003Cp>I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: \u003Ca href=\"https://tiim.ch/full-rss.xml\" rel=\"nofollow noopener noreferrer\">full-rss.xml\u003C/a>. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.\u003C/p>\n\u003Ch3>Notifying referenced websites: Sending Webmentions\u003C/h3>\n\u003Cp>Sending webmentions was easy compared to receiving webmentions:\u003C/p>\n\u003Cp>On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.\u003C/p>\n\u003Cp>Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library \u003Ca href=\"https://github.com/go-co-op/gocron\" rel=\"nofollow noopener noreferrer\">gocron\u003C/a> for scheduling the regular intervals, \u003Ca href=\"https://github.com/mmcdole/gofeed\" rel=\"nofollow noopener noreferrer\">gofeed\u003C/a> for parsing the RSS feed and \u003Ca href=\"https://willnorris.com/go/webmention\" rel=\"nofollow noopener noreferrer\">webmention\u003C/a> for extracting links and sending webmentions.\u003C/p>\n\u003Ch3>In the future: IndieAuth\u003C/h3>\n\u003Cp>The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.\u003C/p>\n\u003Cp>Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.\u003C/p>\u003Cdiv class=\"mf2\">\u003Cblockquote class=\"syndication\">This post is also on \u003Cul>\u003Cli>\u003Ca class=\"u-syndication\" href=\"https://news.indieweb.org/en\">news.indieweb.org\u003C/a>\u003C/li>\u003C/ul>\u003C/blockquote>\u003C/div>\n","blog/2022-12-indiewebifying-my-website-part-1","3b342241-c414-4670-bd22-03e13d6531b7",["Date","2022-11-12T10:55:14.000Z"],[8],null,"IndieWebifying my Website Part 1 - Microformats and Webmentions",true,["Date","2022-12-03T20:56:54.000Z"],"This site now supports sending and receiving webmentions and surfacing structured data using microformats2.","https://i.imgur.com/FpgIBxI.jpg",[15,16,17,18,19,20],"IndieWeb","Webmentions","mf2","tiim.ch","go","indiego",[22],"https://news.indieweb.org/en","\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>",[19,20,25,17,18,26],"indieweb","webmentions","article","blog",[30,37,43,48,54,59,65,71],{"id":31,"type":32,"replyTo":33,"timestamp":34,"page":4,"url":35,"content":33,"name":36},"41435fdd-5fd2-4175-b9ea-ef9ce0dec154","webmention","","2023-09-02T19:26:59Z","https://evgenykuznetsov.org/en/reactions/2022/like-337215655/","Evgeny Kuznetsov",{"id":38,"type":32,"replyTo":33,"timestamp":39,"page":4,"url":40,"content":41,"name":42},"68bd3601-4f00-42c1-b28c-1f2ef75ac851","2023-08-02T09:10:03Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...","Tim Bachmann",{"id":44,"type":32,"replyTo":33,"timestamp":45,"page":4,"url":46,"content":47,"name":42},"5f508a42-8b83-4c10-9f7e-9c1b80e23ab1","2022-12-09T10:49:06Z","https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting","Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.",{"id":49,"type":32,"replyTo":33,"timestamp":50,"page":4,"url":51,"content":52,"name":53},"dc8dbf30-ff1f-4e13-ba36-37f12666005c","2022-12-05T08:41:07Z","https://martymcgui.re/2022/12/05/033926/","★ Liked https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","https://martymcgui.re/",{"id":55,"type":32,"replyTo":33,"timestamp":56,"page":4,"url":57,"content":33,"name":58},"6f6c1f11-2ae0-41a4-b7d0-e343ef63aa52","2022-11-27T22:32:27Z","https://brid.gy/like/twitter/tiimb/1591417020557525003/48372745","Jimmy Lipham",{"id":60,"type":32,"replyTo":33,"timestamp":61,"page":4,"url":62,"content":63,"name":64},"5e1c4149-8fb9-48fb-b285-5efbd626b259","2022-11-27T22:31:57Z","https://brid.gy/repost/twitter/tiimb/1591417020557525003/1591418692008558592","I published a new blog post:\nIndieWebifying my Website Part 1 - Microformats and Webmentions\ntiim.ch/blog/2022-12-i…\n#indieweb #microformats #webmentions #golang","Golang Smart Bot",{"id":66,"type":32,"replyTo":33,"timestamp":67,"page":4,"url":68,"content":69,"name":70},"6e0bf830-1735-42a2-9aa2-ea4c40ab7a45","2022-11-15T12:47:35Z","https://webmention.rocks/receive/1/f0fa5421056e068fe902932ef98f6d71","This test verifies that you accept a Webmention request that contains a valid source and target URL. To pass this test, your Webmention endpoint must return either HTTP 200, 201 or 202 along with the appropriate headers.\nIf your endpoint returns HTTP 201, then it MUST also return a Location header. If it returns HTTP 200 or 202, then it MUST NOT include a Location header.","Webmention Rocks!",{"id":72,"type":32,"replyTo":33,"timestamp":73,"page":4,"url":74,"content":75,"name":76},"4e237d08-9f22-480e-a46e-8f40adf06c5e","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/rm8as/","Liked\nIndieWebifying my Website Part 1 - Microformats and Webmentions\nPost detailsThis site now supports sending and receiving webmentions and surfacing structured data using microformats2. https://i.imgur.com/FpgIBxI.jpg","Jamie Tanna",{"html":78,"slug":79,"uuid":80,"date":81,"created":82,"aliases":83,"title":84,"published":10,"modified":85,"description":86,"cover_image":87,"content_tags":88,"abstract":91,"tags":92,"links":-1,"type":27,"folder":28,"comments":93,"latestComment":34},"\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\" rel=\"nofollow noopener noreferrer\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>\n\u003Cp>I kept my eyes open for a service that I liked, but I did not want to just integrate any old service into my website, I did have some requirements:\u003C/p>\n\u003Cul>\n\u003Cli>The service should not cost anything. I would rather host something myself than sign up for another subscription (because I'm already paying for a VPS anyway).\u003C/li>\n\u003Cli>I want to control how the comments on my website are displayed. I quite like my website design and I don't want a generic comment box below my posts.\u003C/li>\n\u003Cli>The service should respect the privacy of the people using my website.\u003C/li>\n\u003Cli>There should be an option to comment without setting up an account with the service.\u003C/li>\n\u003C/ul>\n\u003Cp>While looking around for how other people integrated comments into their static websites, I found a nice \u003Ca href=\"https://averagelinuxuser.com/static-website-commenting/\" rel=\"nofollow noopener noreferrer\">blog post from Average Linux User\u003C/a> which compares a few popular commenting systems.\nUnfortunately, most systems either are not very privacy-friendly, cost money or store the comments as comments on Github issues..?\nAfter looking through the options I decided to use this opportunity to write my own commenting system and dabble with the Go programming language.\u003C/p>\n\u003Ch2>Writing a commenting API in Go\u003C/h2>\n\u003Cp>First thing first, if you want to take a look at the code, check out the \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">Github repo\u003C/a>.\u003C/p>\n\u003Cp>I decided to write the commenting system in Go because I have been looking for an excuse to practice Go for a while, and this seemed like the perfect fit. It is a small CRUD app, consisting of a storage component, an API component and a small event component in the middle to easily compose the functionality I want.\u003C/p>\n\u003Cp>Currently, it supports the following functionality:\u003C/p>\n\u003Cul>\n\u003Cli>Listing all comments (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Listing all comments for a specified page (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Posting comments through the API\u003C/li>\n\u003Cli>A simple admin dashboard that lists all comments and allows the admin to delete them\u003C/li>\n\u003Cli>Email notifications when someone comments\u003C/li>\n\u003Cli>Email notifications when someone replies to your comment\u003C/li>\n\u003Cli>SQLite storage for comments\u003C/li>\n\u003C/ul>\n\u003Cp>The code is built in a way to make it easy to customise the features.\nFor example to disable features like the email reply notifications you can just \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/main.go#L52\" rel=\"nofollow noopener noreferrer\">comment out the line in the main.go\u003C/a> file that registers that hook.\u003C/p>\n\u003Cp>To write custom hooks that get executed when a new comment gets submitted or one gets deleted, just implement the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/event/handler.go\" rel=\"nofollow noopener noreferrer\">Handler\u003C/a> interface and register it in the main method.\u003C/p>\n\u003Cp>You can also easily add other storage options like databases or file storage by implementing the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/model/store.go\" rel=\"nofollow noopener noreferrer\">Store and SubscribtionStore\u003C/a> interfaces.\u003C/p>\n\u003Ch2>Can it be used in production? 🚗💨\u003C/h2>\n\u003Cp>I currently use it on this website! Go test it out (I might delete the comments if they are rude though 🤔).\u003C/p>\n\u003Cp>In all seriousness, I would not use it for a website where the comments are critical. But for a personal blog or similar, I don't see why not.\u003C/p>\n\u003Cp>If you want to host your own version, there is a Dockerfile available. If you decide to integrate this into your website, please comment below, ping me \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> or shoot me an email \u003Ca href=\"mailto:hey@tiim.ch\">hey@tiim.ch\u003C/a>, I would love to check it out.\u003C/p>","blog/2022-07-12-first-go-project-commenting-api","bff14052-4f3f-4dcb-bcee-155ae1c6b09e",["Date","2022-07-12T00:00:00.000Z"],["Date","2022-07-08T16:24:37.766Z"],[8],"First Go Project: A Jam-stack Commenting API",["Date","2022-11-23T21:42:29.000Z"],"I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!","/assets/2022-07-first-go-project-commenting-api.png",[19,89,90,18,20],"web-api","project","\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>",[19,20,90,18,89],[94,97,104,111,116,122,126,131,138,144,149],{"id":95,"type":32,"replyTo":33,"timestamp":96,"page":79,"url":40,"content":41,"name":42},"31ec5f44-15b2-498a-890d-350e38b9a83e","2023-08-02T09:10:04Z",{"id":98,"type":99,"replyTo":33,"timestamp":100,"page":79,"url":101,"content":102,"name":103},"6d792d24-ba58-4408-83a4-3583667ff4ad","comment","2023-07-09T19:25:02Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#6d792d24-ba58-4408-83a4-3583667ff4ad","Heya just saw your post on Reddit about this comment feature, didn't want to leave without using it ^^. Nicely done!","Anonymous",{"id":105,"type":99,"replyTo":106,"timestamp":107,"page":79,"url":108,"content":109,"name":110},"99dd9ccf-5349-4f41-9553-67986e1a1074","1c8ba0da-10df-4a7a-b067-55875441de2d","2022-12-07T23:01:37Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#99dd9ccf-5349-4f41-9553-67986e1a1074","You are right, there is not any documentation in the readme yet. Although hopefully, I will work on that soon. I'm in the middle of refactoring the project.\n\nTo query the comments there are two rest endpoints [\"/comment\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L41-L71) and [\"/comment/:page\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L73-L107) which return all comments or the comments for a specific page. The comments are loaded from this API endpoint when the site is generated.\n\nTo display the comments without rebuilding the site, new comments are fetched in the browser with the \"?since=\u003Ctime-of-last-build>\" query parameter.","Tiim",{"id":106,"type":99,"replyTo":33,"timestamp":112,"page":79,"url":113,"content":114,"name":115},"2022-12-07T22:33:44Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#1c8ba0da-10df-4a7a-b067-55875441de2d","Good stuff. Always nice to see a site supporting comments and/or Webmentions. Maybe I missed it in the readme but I am curious as to how one queries the API for comments. Do you pull in the comments from the database when you generate the site?","Poorchop",{"id":117,"type":32,"replyTo":33,"timestamp":118,"page":79,"url":119,"content":120,"name":121},"e42756e4-d5a5-4727-8c58-434d285b7ab3","2022-11-27T22:31:58Z","https://brid.gy/repost/twitter/tiimb/1546801590593638400/1546801615264415745","I published a new blog post:\nFirst Go Project: A jam-stack Commenting API\ntiim.ch/blog/2022-07-1…\n#golang #jamstack #API","Golang Bot",{"id":123,"type":32,"replyTo":33,"timestamp":124,"page":79,"url":125,"content":12,"name":42},"b8d3ae8b-0059-4379-8c35-30c97269908f","2022-11-21T22:19:23Z","https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1",{"id":127,"type":99,"replyTo":33,"timestamp":128,"page":79,"url":129,"content":130,"name":130},"171e3444-f1d0-492d-8bc7-c0a133a41783","2022-07-18T08:44:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#171e3444-f1d0-492d-8bc7-c0a133a41783","hola",{"id":132,"type":99,"replyTo":133,"timestamp":134,"page":79,"url":135,"content":136,"name":137},"5875bba1-e69b-467a-bab5-23ef4160d257","621574fd-eea2-48d6-87c8-aebd0f05f1aa","2022-07-13T21:06:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#5875bba1-e69b-467a-bab5-23ef4160d257","And a polite reply","polite",{"id":139,"type":99,"replyTo":33,"timestamp":140,"page":79,"url":141,"content":142,"name":143},"8494c653-ef37-47a2-ae1b-00d00e4815a9","2022-07-13T18:31:31Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#8494c653-ef37-47a2-ae1b-00d00e4815a9","Pretty cool dudez","somGuy",{"id":133,"type":99,"replyTo":33,"timestamp":145,"page":79,"url":146,"content":147,"name":148},"2022-07-12T21:40:39Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#621574fd-eea2-48d6-87c8-aebd0f05f1aa","This is a rude comment ;)","rude",{"id":150,"type":99,"replyTo":33,"timestamp":151,"page":79,"url":152,"content":153,"name":154},"fb6278ae-e48c-4397-ba29-bec4e5cb3a57","2022-07-12T13:30:14Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#fb6278ae-e48c-4397-ba29-bec4e5cb3a57","Good job dude!","wdup",{"tag":20}],"uses":{"params":["slug"]}}]} diff --git a/tags/indieweb.html b/tags/indieweb.html new file mode 100644 index 00000000..444040e7 --- /dev/null +++ b/tags/indieweb.html @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️indieweb - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: indieweb

+ +

adding Micropub support • AndreGarzia.com

+ 11/21/2022 +
+

👍 Liked: https://andregarzia.com/2022/03/adding-micropub-support.html

+
+
+ +

Hello Micropub

+ 11/21/2022 +
+

If you can read this entry, my micropub implementation works! You can expect a blog post about it shortly :)

+
+
+ +

Test note

+ 11/11/2022 +
+

This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/indieweb/__data.json b/tags/indieweb/__data.json new file mode 100644 index 00000000..07d70832 --- /dev/null +++ b/tags/indieweb/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":191},[2,41,106,126,157,169,178],{"html":3,"slug":4,"uuid":5,"date":6,"aliases":7,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_caption":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":25,"folder":26,"comments":27,"latestComment":40},"\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\" rel=\"nofollow noopener noreferrer\">TOS\u003C/a>:\u003C/p>\n\u003Cblockquote>\n\u003Cp>[...] Also, don't use Imgur to host image libraries you link to from elsewhere, content for your website, advertising, avatars, or anything else that turns us into your content delivery network.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Finally when I started \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">indie-webifying my website\u003C/a>, and was implementing the micropub protocol (which I will blog about at a later time), I decided that it was at the time to host the images on a platform that was meant to do that. I looked at a few storage providers such as cloudinary and S3 based object storage and landed on \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">Storj.io\u003C/a>, mostly because of the generous free tier, which should suffice for this little blog for quite a while.\u003C/p>\n\u003Cp>One thing that bothered me slightly was that all storage providers I looked at charge for traffic. It's not the fact that it's an additional expense (if your not in the free tier anymore) that bothers me, but the fact that I don't have any control over how much this will cost me. In all likelihood this will never cost me anything since this blog has not much traffic, but if a post were to go viral (one can dream...), this could result in a surprise bill at the end of the month.\u003C/p>\n\u003Cp>To help with the traffic costs I decided to try to use the free CDN functionality of Cloudflare to reduce the traffic to Storj. In this blog post I will describe how I did that.\u003C/p>\n\u003Ch2>Is this the right solution for you?\u003C/h2>\n\u003Cp>If you are in a similar situation as me, and just want to have somewhere to host your images for a personal website or to share images or screenshots as links while still having control over all your data, this could be a good solution.\u003C/p>\n\u003Cp>If you want to build a robust image pipeline with resizing and image optimization, or you are building an enterprise website this is probably not the right way. You should take a look at cloudinary or one of the big cloud providers.\u003C/p>\n\u003Ch2>Prerequisites\u003C/h2>\n\u003Cp>To use Cloudflare as a CDN, you need to have Cloudflare setup as your DNS host for the domain you want to serve the images from. Even if you just want to use a subdomain like \u003Ccode>media.example.com\u003C/code>, the whole \u003Ccode>example.com\u003C/code> domain needs to be on cloudflare. For me this was not much of an issue, I followed the instructions from cloudflare and pointed the nameserver of my domain to cloudflare. Although I did have an issue during the migration, which resulted in my website being down for two hours. But I'm pretty sure this was caused by my previous nameserver provider.\u003C/p>\n\u003Ch2>Setting up Storj & Cloudflare\u003C/h2>\n\u003Cp>I assume you already have an account at \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">storj.io\u003C/a>. The next step is creating a bucket for your images. A bucket is just a place for your files and folders to live in storj, just like in any other S3 compatible storage provider. (Actually there are no folders in storj and other S3 services, the folders are just prefixes of the filenames). When creating a bucket, make sure you save the passphrase securely, such as in your password manager. Whenever storj asks you for the passphrase, make sure you don't let storj generate a new one! Every new passphrase will create access to a new bucket.\u003C/p>\n\u003Cp>The next step is \u003Ca href=\"https://docs.storj.io/dcs/downloads/download-uplink-cli\" rel=\"nofollow noopener noreferrer\">installing the uplink cli\u003C/a>. Follow the quick start tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object\" rel=\"nofollow noopener noreferrer\">get an access grant\u003C/a>. Remember to use the same passphrase from above. Now follow the next quickstart tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object/set-up-uplink-cli\" rel=\"nofollow noopener noreferrer\">add the bucket to the uplink cli\u003C/a>. The file \u003Ccode>accessgrant.txt\u003C/code> in the tutorial only contains the access-grant string that you got from the last step.\u003C/p>\n\u003Cp>Finally we want to share the bucket so the images can be accessed from the web. For this you can run the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">uplink share --dns <domain> sj://<bucket>/<prefix> --not-after=none\n\u003C/code>\u003C/pre>\n\u003Cp>Replace \u003Ccode><domain>\u003C/code> with the domain you want to serve the images from. In my case I use \u003Ccode>media.tiim.ch\u003C/code>. Then replace \u003Ccode><bucket>\u003C/code> with the name of your bucket and \u003Ccode><prefix>\u003C/code> with the prefix.\u003C/p>\n\u003Cp>As mentioned above, you can think of a prefix as a folder. If you use for example \u003Ccode>media-site1\u003C/code> as a prefix, then every file in the \"folder\" \u003Ccode>media-site1\u003C/code> will be shared. This means you can use multiple prefixes to serve files for multiple websites in the same bucket.\u003C/p>\n\u003Cp>You will get the following output:\u003C/p>\n\u003Cpre>\u003Ccode>[...]\n=========== DNS INFO =====================================================================\nRemember to update the $ORIGIN with your domain name. You may also change the $TTL.\n$ORIGIN example.com.\n$TTL 3600\nmedia.example.com IN CNAME link.storjshare.io.\ntxt-media.example.com IN TXT storj-root:mybucket/myprefix\ntxt-media.example.com IN TXT storj-access:totallyrandomstringofnonsens\n\u003C/code>\u003C/pre>\n\u003Cp>Create the DNS entries in Cloudflare with the values printed in the last three lines. Make sure you enable the proxy setting when entering the CNAME entry to enable Cloudflares CDN service.\u003C/p>\n\u003Cp>And that's it. All files you put in the bucket with the correct prefix are now available under your domain! :)\u003C/p>\n\u003Cp>If this blog post helped you, or you have some issues or thoughts on this, leave a comment via the comment box below or via webmention.\u003C/p>","blog/2022-12-storj-cloudflare-image-hosting","6d5a964d-328e-43d7-9189-40280b012074",["Date","2022-12-03T13:37:33.000Z"],[8],null,"Hosting Images with Storj and Cloudflare",true,"Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.","https://media.tiim.ch/d280fad4-632a-4b5a-b6b2-6a5c0026b61c.jpg","Image generated by Dall-E: travel postcards scattered on grass, top down view, photoreal",[15,16,17,18],"CDN","IndieWeb","Cloudflare","Storj","\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\">TOS\u003C/a>:\u003C/p>",[21,22,23,24],"cdn","cloudflare","indieweb","storj","article","blog",[28,35],{"id":29,"type":30,"replyTo":31,"timestamp":32,"page":4,"url":33,"content":31,"name":34},"edeb5ddb-357a-41cf-b2b2-704df571d70c","webmention","","2023-01-11T06:03:38Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/1570290779419017216","Laura Forster",{"id":36,"type":30,"replyTo":31,"timestamp":37,"page":4,"url":38,"content":31,"name":39},"2331980c-acee-4549-a260-61b4119c63a9","2022-12-05T06:12:20Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/275384865","kevin","2023-09-02T19:26:59Z",{"html":42,"slug":43,"uuid":44,"date":45,"aliases":46,"title":47,"published":10,"modified":48,"description":49,"cover_image":50,"content_tags":51,"syndication":57,"abstract":59,"tags":60,"links":-1,"type":25,"folder":26,"comments":62,"latestComment":40},"\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\" rel=\"nofollow noopener noreferrer\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\" rel=\"nofollow noopener noreferrer\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>\n\u003Ch2>The IndieWeb\u003C/h2>\n\u003Cp>I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.\u003C/p>\n\u003Cp>The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.\u003C/p>\n\u003Cp>The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.\u003C/p>\n\u003Cp>The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.\u003C/p>\n\u003Cp>Some of the standards in the IndieWeb include:\u003C/p>\n\u003Cul>\n\u003Cli>Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.\u003C/li>\n\u003Cli>Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.\u003C/li>\n\u003Cli>IndieAuth, an OAuth2-based way to log in using only your domain name.\u003C/li>\n\u003C/ul>\n\u003Ch2>The implementation on my website\u003C/h2>\n\u003Cp>As explained in my earlier post \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">First Go Project: A Jam-stack Commenting API\u003C/a>, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.\u003C/p>\n\u003Ch3>Making the website machine-readable with Microformats\u003C/h3>\n\u003Cp>As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called \u003Ca href=\"http://microformats.org/wiki/h-entry\" rel=\"nofollow noopener noreferrer\">h-entry\u003C/a>. By adding the \u003Ccode>h-entry\u003C/code> class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as \u003Ccode>p-name\u003C/code>, \u003Ccode>p-author\u003C/code> or \u003Ccode>dt-published\u003C/code>.\u003C/p>\n\u003Cp>While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.\u003C/p>\n\u003Cp>Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.\u003C/p>\n\u003Ch3>Accepting comments and other interactions via Webmentions\u003C/h3>\n\u003Cp>The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.\u003C/p>\n\u003Cp>In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.\u003C/p>\n\u003Cp>Since I already have a \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">small custom service\u003C/a> running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions\u003C/p>\n\u003Cp>It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database.\nI found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.\u003C/p>\n\u003Cp>I have tested my webmentions implementation using \u003Ca href=\"https://webmention.rocks\" rel=\"nofollow noopener noreferrer\">webmention.rocks\u003C/a>, but I would appreciate it if you left me a mention as well 😃\u003C/p>\n\u003Ch3>Publishing short-form content such as replies, likes and bookmarks: A notes post type\u003C/h3>\n\u003Cp>The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">notes\u003C/a>. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.\u003C/p>\n\u003Cp>I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: \u003Ca href=\"https://tiim.ch/full-rss.xml\" rel=\"nofollow noopener noreferrer\">full-rss.xml\u003C/a>. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.\u003C/p>\n\u003Ch3>Notifying referenced websites: Sending Webmentions\u003C/h3>\n\u003Cp>Sending webmentions was easy compared to receiving webmentions:\u003C/p>\n\u003Cp>On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.\u003C/p>\n\u003Cp>Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library \u003Ca href=\"https://github.com/go-co-op/gocron\" rel=\"nofollow noopener noreferrer\">gocron\u003C/a> for scheduling the regular intervals, \u003Ca href=\"https://github.com/mmcdole/gofeed\" rel=\"nofollow noopener noreferrer\">gofeed\u003C/a> for parsing the RSS feed and \u003Ca href=\"https://willnorris.com/go/webmention\" rel=\"nofollow noopener noreferrer\">webmention\u003C/a> for extracting links and sending webmentions.\u003C/p>\n\u003Ch3>In the future: IndieAuth\u003C/h3>\n\u003Cp>The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.\u003C/p>\n\u003Cp>Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.\u003C/p>\u003Cdiv class=\"mf2\">\u003Cblockquote class=\"syndication\">This post is also on \u003Cul>\u003Cli>\u003Ca class=\"u-syndication\" href=\"https://news.indieweb.org/en\">news.indieweb.org\u003C/a>\u003C/li>\u003C/ul>\u003C/blockquote>\u003C/div>\n","blog/2022-12-indiewebifying-my-website-part-1","3b342241-c414-4670-bd22-03e13d6531b7",["Date","2022-11-12T10:55:14.000Z"],[8],"IndieWebifying my Website Part 1 - Microformats and Webmentions",["Date","2022-12-03T20:56:54.000Z"],"This site now supports sending and receiving webmentions and surfacing structured data using microformats2.","https://i.imgur.com/FpgIBxI.jpg",[16,52,53,54,55,56],"Webmentions","mf2","tiim.ch","go","indiego",[58],"https://news.indieweb.org/en","\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>",[55,56,23,53,54,61],"webmentions",[63,67,73,77,83,88,94,100],{"id":64,"type":30,"replyTo":31,"timestamp":40,"page":43,"url":65,"content":31,"name":66},"41435fdd-5fd2-4175-b9ea-ef9ce0dec154","https://evgenykuznetsov.org/en/reactions/2022/like-337215655/","Evgeny Kuznetsov",{"id":68,"type":30,"replyTo":31,"timestamp":69,"page":43,"url":70,"content":71,"name":72},"68bd3601-4f00-42c1-b28c-1f2ef75ac851","2023-08-02T09:10:03Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...","Tim Bachmann",{"id":74,"type":30,"replyTo":31,"timestamp":75,"page":43,"url":76,"content":11,"name":72},"5f508a42-8b83-4c10-9f7e-9c1b80e23ab1","2022-12-09T10:49:06Z","https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting",{"id":78,"type":30,"replyTo":31,"timestamp":79,"page":43,"url":80,"content":81,"name":82},"dc8dbf30-ff1f-4e13-ba36-37f12666005c","2022-12-05T08:41:07Z","https://martymcgui.re/2022/12/05/033926/","★ Liked https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","https://martymcgui.re/",{"id":84,"type":30,"replyTo":31,"timestamp":85,"page":43,"url":86,"content":31,"name":87},"6f6c1f11-2ae0-41a4-b7d0-e343ef63aa52","2022-11-27T22:32:27Z","https://brid.gy/like/twitter/tiimb/1591417020557525003/48372745","Jimmy Lipham",{"id":89,"type":30,"replyTo":31,"timestamp":90,"page":43,"url":91,"content":92,"name":93},"5e1c4149-8fb9-48fb-b285-5efbd626b259","2022-11-27T22:31:57Z","https://brid.gy/repost/twitter/tiimb/1591417020557525003/1591418692008558592","I published a new blog post:\nIndieWebifying my Website Part 1 - Microformats and Webmentions\ntiim.ch/blog/2022-12-i…\n#indieweb #microformats #webmentions #golang","Golang Smart Bot",{"id":95,"type":30,"replyTo":31,"timestamp":96,"page":43,"url":97,"content":98,"name":99},"6e0bf830-1735-42a2-9aa2-ea4c40ab7a45","2022-11-15T12:47:35Z","https://webmention.rocks/receive/1/f0fa5421056e068fe902932ef98f6d71","This test verifies that you accept a Webmention request that contains a valid source and target URL. To pass this test, your Webmention endpoint must return either HTTP 200, 201 or 202 along with the appropriate headers.\nIf your endpoint returns HTTP 201, then it MUST also return a Location header. If it returns HTTP 200 or 202, then it MUST NOT include a Location header.","Webmention Rocks!",{"id":101,"type":30,"replyTo":31,"timestamp":102,"page":43,"url":103,"content":104,"name":105},"4e237d08-9f22-480e-a46e-8f40adf06c5e","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/rm8as/","Liked\nIndieWebifying my Website Part 1 - Microformats and Webmentions\nPost detailsThis site now supports sending and receiving webmentions and surfacing structured data using microformats2. https://i.imgur.com/FpgIBxI.jpg","Jamie Tanna",{"html":107,"slug":108,"uuid":109,"title":110,"date":111,"modified":8,"section":112,"published":10,"cover_image":113,"content_tags":114,"links":119,"abstract":122,"tags":123,"type":25,"description":31,"folder":124,"comments":125,"latestComment":40},"\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>\n\u003Cul>\n\u003Cli>The basic commenting system\u003C/li>\n\u003Cli>Sending and receiving webmentions\u003C/li>\n\u003Cli>Micropub server implementation\u003C/li>\n\u003Cli>IndieAuth (decentralized authentication standard based on OAuth)\u003C/li>\n\u003Cli>Admin dashboard\u003C/li>\n\u003Cli>Admin backup endpoint\u003C/li>\n\u003C/ul>\n\u003Cp>Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and\ncomments through the fediverse show up back on my website.\u003C/p>\n\u003Cp>The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core\nof the application is only concerned with initializing those plugins.\u003C/p>\n\u003Cp>If you have any questions, or want to run IndieGo yourself, don't hesitate to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>.\u003C/p>","projects/indiego","0cf125b3-a99a-4996-8f84-ec5105d64c57","IndieGo",["Date","2023-08-02T08:39:00.000Z"],"Projects","/assets/2022-07-first-go-project-commenting-api.png",[55,115,23,116,117,118],"golang","docker","sqlite","dev",[120,121],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">IndieGo Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://comments.tiim.ch\" rel=\"nofollow noopener noreferrer\">Admin Interface\u003C/a> - authentication required\u003C/p>","\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>",[118,116,55,115,23,117],"projects",[],{"html":127,"slug":128,"name":129,"date":130,"content_tags":131,"like_of":135,"raw_data":137,"tags":153,"links":-1,"published":10,"type":154,"cover_image":-1,"description":155,"folder":53,"comments":156,"latestComment":40},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://werd.io/2022/the-fediverse-and-the-indieweb\">https://werd.io/2022/the-fediverse-and-the-indieweb\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/ntc1nd","The fediverse and the indieweb",["Date","2022-11-30T06:35:00.000Z"],[132,133,134],"Indieweb","fediverse","mastodon",{"url":136},"https://werd.io/2022/the-fediverse-and-the-indieweb",{"items":138,"rels":151,"relurls":152},[139],{"id":31,"value":31,"html":31,"type":140,"properties":142,"shape":31,"coords":31,"children":150},[141],"h-entry",{"category":143,"like-of":144,"name":145,"post-status":146,"published":148},[132,133,134],[136],[129],[147],"published",[149],"2022-11-30T07:35:00+0100",[],{},{},[133,23,134],"like","👍 Liked: https://werd.io/2022/the-fediverse-and-the-indieweb",[],{"html":158,"slug":159,"title":160,"date":161,"content_tags":162,"like_of":164,"tags":166,"links":-1,"published":10,"type":154,"cover_image":-1,"description":167,"folder":53,"comments":168,"latestComment":40},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://andregarzia.com/2022/03/adding-micropub-support.html\">https://andregarzia.com/2022/03/adding-micropub-support.html\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/mjqymd","adding Micropub support • AndreGarzia.com",["Date","2022-11-21T14:25:00.000Z"],[163,23],"micropub",{"url":165},"https://andregarzia.com/2022/03/adding-micropub-support.html",[23,163],"👍 Liked: https://andregarzia.com/2022/03/adding-micropub-support.html",[],{"html":170,"slug":171,"title":172,"date":173,"content_tags":174,"abstract":170,"tags":175,"links":-1,"published":10,"type":176,"cover_image":-1,"description":31,"folder":53,"comments":177,"latestComment":40},"\u003Cp>If you can read this entry, my micropub implementation works! You can expect a blog post about it shortly :)\u003C/p>","mf2/2022/11/ntuwnd","Hello Micropub",["Date","2022-11-21T13:55:00.000Z"],[23,163],[23,163],"note",[],{"html":179,"slug":180,"uuid":181,"date":182,"created":183,"title":184,"published":10,"modified":185,"content_tags":186,"reply_to":187,"abstract":188,"tags":189,"links":-1,"type":176,"cover_image":-1,"description":31,"folder":53,"comments":190,"latestComment":40},"\u003Ch2>This is a test note\u003C/h2>\n\u003Cp>This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)\u003C/p>\n\u003Cp>You can find a list of all my notes \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">here\u003C/a>\u003C/p>","mf2/2022/hkhmlv","874d0a10-6de8-4816-9b69-5c2fbe782774",["Date","2022-11-11T14:43:58.000Z"],["Date","2022-11-11T14:43:58.000Z"],"Test note",["Date","2022-11-11T14:43:58.000Z"],[53,23],"https://webmention.rocks/test/4","\u003Cp>This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)\u003C/p>",[23,53],[],{"tag":23}],"uses":{"params":["slug"]}}]} diff --git a/tags/irc.html b/tags/irc.html new file mode 100644 index 00000000..d6683611 --- /dev/null +++ b/tags/irc.html @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️irc - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: irc

+ +

Weechat Notifications with ntfy.sh

+ 3/28/2023 +
Weechat Notifications with ntfy.sh +

Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/irc/__data.json b/tags/irc/__data.json new file mode 100644 index 00000000..b1c390fb --- /dev/null +++ b/tags/irc/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":66},[2,26],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\" rel=\"nofollow noopener noreferrer\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\" rel=\"nofollow noopener noreferrer\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>\n\u003Cp>Also, I recently found the web service \u003Ca href=\"https://ntfy.sh\" rel=\"nofollow noopener noreferrer\">ntfy.sh\u003C/a>. It sends push notifications whenever you send an HTTP post request to a certain URL. I already have ntfy.sh installed on my android phone, and I also found a minimal and lightweight \u003Ca href=\"https://github.com/lucas-bortoli/ntfysh-windows\" rel=\"nofollow noopener noreferrer\">desktop client\u003C/a>.\u003C/p>\n\u003Cp>I managed to set a WeeChat trigger up that fires every time I get mentioned (highlighted in WeeChat terminology), and a trigger that fires every time I get a private message. Both of those triggers execute the \u003Ccode>/exec\u003C/code> command which runs an arbitrary shell command. The exec command runs the \u003Ccode>wget\u003C/code> program to send a post request to the ntfy.sh server, which in turn sends a notification to all apps that subscribe to the same URL as the post request was sent. I would usually use the curl program for this instead of wget, but the docker default docker image doesn't contain a curl install.\u003C/p>\n\u003Cp>Here you can see the two \u003Ccode>/trigger\u003C/code> commands:\u003C/p>\n\u003Cp>\u003Cem>trigger on mention\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode>/trigger addreplace notify_highlight print '' '${tg_highlight}' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"- -header=Title: New highlight: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cem>trigger on private message\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-weechat\">/trigger addreplace notify_privmsg print '' '${tg_tag_notify} == private && ${buffer.notify} > 0' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"--header=Title: New private message: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Ch2>The trigger commands in detail\u003C/h2>\n\u003Cp>In case you don't just want to copy and paste some random command from the internet into your WeeChat (which you shouldn't do anyway), I will try to explain the trigger command that fires when you get mentioned in a message:\u003C/p>\n\u003Cp>Let's first look at the trigger command itself:\n\u003Ccode>/trigger addreplace <name> <hook> <argument> <condition> <variable-replace> <command>\u003C/code>\nWe call the \u003Ccode>/trigger\u003C/code> command with the \u003Ccode>addreplace\u003C/code> subcommand. This subcommand will either register a new trigger or replace it if one with the same name already exists.\u003C/p>\n\u003Cul>\n\u003Cli>\u003Ccode>name\u003C/code> - This argument is self-explanatory, the name of the trigger. In our case I called it \u003Ccode>notify_highlight\u003C/code>, but you could call it whatever you want.\u003C/li>\n\u003Cli>\u003Ccode>hook\u003C/code> - This argument specifies which hook or event the trigger should listen for. WeeChat is built as an event-driven platform, so pretty much anything from mouse movements to IRC messages are handled via events. In this case, we want to trigger on the \u003Ccode>print\u003C/code> event, which is fired every time a new message gets received from IRC.\u003C/li>\n\u003Cli>\u003Ccode>argument\u003C/code> - The argument is needed for some hooks, but not for the \u003Ccode>print\u003C/code> hook, so we are going to ignore that one for now and just set it to an empty string \u003Ccode>''\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>condition\u003C/code> - The condition must evaluate to \u003Ccode>true\u003C/code> for the trigger to fire. This is helpful because the \u003Ccode>print\u003C/code> trigger fires for every new message, but we only want to be notified when the new message mentions our nick. The condition for this is \u003Ccode>${tg_highlight}\u003C/code>. You can find the list of variables that you can access with the command \u003Ccode>/trigger monitor\u003C/code>, which prints all variables for every trigger that gets executed.\u003C/li>\n\u003Cli>\u003Ccode>variable-replace\u003C/code> - This took me a while to understand. This command is used to manipulate data and save it to a variable. The syntax is inspired by the sed command. Explaining it fully is out of the scope of this blog post, but you can take a look at the \u003Ca href=\"https://weechat.org/files/doc/devel/weechat_user.en.html#trigger_regex\" rel=\"nofollow noopener noreferrer\">docs\u003C/a>. In our example, we replace the whole content of the variable \u003Ccode>tg_message\u003C/code> with the format string \u003Ccode>${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}\u003C/code> which results in a sting like \u003Ccode><tiim> Hello world!\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>command\u003C/code> - The last argument is the command that gets executed whenever this trigger fires. In our case, we use the \u003Ccode>/execute\u003C/code> command, which starts the wget command which in turn sends a post request to ntfy.sh. Make sure you set the ntfy topic (the part after \u003Ccode>https://ntfy.sh/\u003C/code>) to something private and long enough so that nobody else is going to guess it by accident.\u003C/li>\n\u003C/ul>\n\u003Cp>Don't forget to subscribe to the ntfy topic on your phone or whatever device you want to receive the notification on.\u003C/p>\n\u003Cp>The possibilities with the trigger plugin are endless, I hope this inspires you to build your own customizations using weechat.\u003C/p>","blog/2023-03-28-weechat-notification-ntfy","ef51e944-86fa-44b0-ab9d-be7f8d8e569a",["Date","2023-03-28T10:05:19.000Z"],["Date","2023-01-15T20:35:40.643Z"],[9],null,"Weechat Notifications with ntfy.sh",true,"Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.","https://media.tiim.ch/97833b1d-d602-4d9a-9689-3077e96e45ba.webp","stable diffusion - Anything V3.0 - boy using an old DOS computer, 90s vibes, muted pastel colors, stylized, thick lines, IRC, console",[16,17,18,19],"weechat","ntfy.sh","wget","irc","\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>",[19,17,16,18],"article","blog",[],"2023-09-02T19:26:59Z",{"html":27,"slug":28,"uuid":29,"date":30,"created":31,"aliases":32,"title":33,"published":11,"modified":34,"description":35,"cover_image":36,"cover_image_txt":37,"content_tags":38,"abstract":40,"tags":41,"links":-1,"type":22,"folder":23,"comments":42,"latestComment":25},"\u003Cp>I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used \u003Ca href=\"https://hexchat.github.io/\" rel=\"nofollow noopener noreferrer\">HexChat\u003C/a> in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.\u003C/p>\n\u003Cp>The one client I have seen mentioned a lot is \u003Ca href=\"https://weechat.org/\" rel=\"nofollow noopener noreferrer\">WeeChat\u003C/a> (not to be confused with WeChat, the Chinese instant messenger). WeeChat runs in the terminal as a \u003Ca href=\"https://en.wikipedia.org/wiki/Text-based_user_interface\" rel=\"nofollow noopener noreferrer\">TUI\u003C/a> and after a while of getting used to (and after enabling 'mouse mode') it seems intuitive enough.\u003C/p>\n\u003Cp>The nice thing about WeeChat running not as a graphical application, is that it makes it possible to run on a server and access it from anywhere over ssh.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>INFO\u003C/span>\u003Cp>Except on mobile devices, but weechat has mobile apps that can connect to it directly.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Since I pretty much host all my selfhosted software in docker on a VPS, I was looking if someone already published a docker image for WeeChat. There is a bunch of them, but only \u003Ca href=\"https://hub.docker.com/r/weechat/weechat\" rel=\"nofollow noopener noreferrer\">weechat/weechat\u003C/a> (the official image) is still updated regularly. The docker hub page does not have any documentation, but I managed to find it in the \u003Ca href=\"https://github.com/weechat/weechat-container\" rel=\"nofollow noopener noreferrer\">weechat/weechat-container\u003C/a> github repo.\u003C/p>\n\u003Cp>As it says in the readme on github, you can start the container with\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">docker run -it weechat/weechat\n\u003C/code>\u003C/pre>\n\u003Cp>which will run weechat directly in the foreground.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>Don't skip the \u003Ccode>-it\u003C/code> command line flags. The \u003Ccode>-i\u003C/code> or \u003Ccode>--interactive\u003C/code> keeps stdin open, which is required to send input to weechat. Weechat also closes immediately if the stdin gets closed, which took me a while to figure out.\nThe \u003Ccode>-t\u003C/code> or \u003Ccode>--tty\u003C/code> flag is required to provide a fake tty to the container. I don't really understand what that means but without this you won't see the user interface of weechat.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Running in the foreground is not really that helpful if we want to run weechat on a server, so we need to detach (let it run in the background) from the container with the \u003Ccode>-d\u003C/code> or \u003Ccode>--detach\u003C/code> flag. It also helps to specify a name for the container with the \u003Ccode>--name <name>\u003C/code> argument, so we can quickly find the container again later. The docker command now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">docker run -it -d --name weechat weechat/weechat\n\u003C/code>\u003C/pre>\n\u003Cp>When we run this command, we will notice that weechat is running in the background. To access it we can run \u003Ccode>docker attach weechat\u003C/code>. To detach from weechat without exiting the container, we can press \u003Ccode>CTRL-p CTRL-q\u003C/code> as described in the \u003Ca href=\"https://docs.docker.com/engine/reference/commandline/attach/#description\" rel=\"nofollow noopener noreferrer\">docker attach reference\u003C/a>\u003C/p>\n\u003Cp>I noticed that there are two versions of the weechat image: a debian version and an alpine linux version. Generally the Alpine Linux versions of containers are smaller than the Debian versions, so I decided to use the alpine version: \u003Ccode>weechat/weechat:latest-alpine\u003C/code>.\u003C/p>\n\u003Cp>With this we are practically done, but if we ever remove and restart the container, all of the chat logs and customisations to weechat will be gone. To prevent this we need to add the config and log files to a volume.\u003C/p>\n\u003Cp>I generally use the folder \u003Ccode>~/docker/(service)\u003C/code> to point my docker volumes to, so I have a convenient place to inspect, modify and back up the data.\u003C/p>\n\u003Cp>Let's create the folder and add the volume to the docker container. I also added the \u003Ccode>--restart unless-stopped\u003C/code> flag to make sure the container gets restarted if it either exits for some reason of if docker restarts.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">mkdir -p ~/docker/weechat/data\nmkdir -p ~/docker/weechat/config\n\ndocker run -it -d --restart unless-stopped \\\n -v \"~/docker/weechat/data:/home/user/.weechat\" \\\n -v \"~/docker/weechat/config:/home/user/.config/weechat\" \\\n --name weechat weechat/weechat:latest-alpine`\n\u003C/code>\u003C/pre>\n\u003Cp>Running this command on the server is all we need to have weechat running in docker.\u003C/p>\n\u003Cblockquote>\n\u003Cp>But how do I quickly connect to weechat? Do I always have to first ssh into the server and then run docker attach?\u003C/p>\n\u003C/blockquote>\n\u003Cp>Yes but, as almost always, we can simplify this with a bash script:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-bash\">#!/usr/bin/env bash\n\nHOST=<ssh host>\nssh -t \"${HOST}\" docker attach weechat\n\u003C/code>\u003C/pre>\n\u003Cp>This bash script starts ssh with the \u003Ccode>-t\u003C/code> flag which tells ssh that the command is interactive.\nCopy this script into your \u003Ccode>~/.local/bin\u003C/code> folder and make it executable.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">nano ~/.local/bin/weechat.sh\nchmod +x weechat.sh\n\u003C/code>\u003C/pre>\n\u003Cp>And that's it! Running \u003Ccode>weechat.sh\u003C/code> will open an ssh session to your server and attach to the weechat container. Happy Chatting!\u003C/p>\n\u003Cp>If you liked this post, consider subscribing to my blog via \u003Ca href=\"https://tiim.ch/blog/rss.xml\" rel=\"nofollow noopener noreferrer\">RSS\u003C/a>, or on \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">social media\u003C/a>. If you have any questions, feel free to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>. I also usually hang out in \u003Ca href=\"irc://irc.libera.chat/##tiim\">\u003Ccode>##tiim\u003C/code> on irc.libera.chat\u003C/a>. My name on IRC is \u003Ccode>tiim\u003C/code>.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Update 2022-01-18\u003C/span>\u003Cp>I have found that at the beginning of a session, the input to weechat doesn't seem to work. Sometimes weechat refuses to let me type anything and/or doesn't recognize mouse events.\nAfter a while of spamming keys and \u003Ccode>Alt-m\u003C/code> (toggle mouse mode), it seems to fix itself most of the time.\nI have no idea if thats a problem with weechat, with docker or with ssh, and so far have not found a solution for this. If you have the same problem or even know how to fix it, feel free to reach out.\u003C/p>\n\u003C/blockquote>","blog/2023-01-15-weechat-docker","889ff4db-3ccb-4ab1-9676-a2b0ea8f19eb",["Date","2023-01-15T00:00:00.000Z"],["Date","2023-01-15T00:17:07.000Z"],[9],"Running the WeeChat IRC Client on a VPS in Docker",["Date","2023-01-18T11:34:27.000Z"],"Walkthrough on how to setup the WeeChat IRC client in docker.","https://media.tiim.ch/a28c65a1-ed95-43d3-af87-a2ad222bee7f.jpg","Stable Diffusion - anime landscape, pastel colors, thick outlines, forest, mountains, golden light",[19,16,39],"docker","\u003Cp>I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used \u003Ca href=\"https://hexchat.github.io/\">HexChat\u003C/a> in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.\u003C/p>",[39,19,16],[43,50,55,60],{"id":44,"type":45,"replyTo":46,"timestamp":47,"page":28,"url":48,"content":12,"name":49},"52b7b3e6-e233-4379-8f0c-3332aed562a6","webmention","","2023-03-28T10:10:56Z","https://tiim.ch/blog/2023-03-28-weechat-notification-ntfy","Tim Bachmann",{"id":51,"type":45,"replyTo":46,"timestamp":52,"page":28,"url":53,"content":46,"name":54},"f6f58ebf-a68f-415e-baed-cb8bf38189fd","2023-03-02T00:05:49Z","https://brid.gy/like/twitter/tiimb/1614403118258601987/1557313575072546816","DM Cyber Security",{"id":56,"type":45,"replyTo":46,"timestamp":57,"page":28,"url":58,"content":46,"name":59},"45d40e9f-6498-4432-bdb0-01210e55d092","2023-01-25T18:20:43Z","https://brid.gy/like/twitter/tiimb/1614403118258601987/8717982","Christopher Scott",{"id":61,"type":45,"replyTo":46,"timestamp":62,"page":28,"url":63,"content":64,"name":65},"979c42d0-a8fc-4a52-a85d-bedb672fb144","2023-01-25T09:22:51Z","https://brid.gy/repost/twitter/tiimb/1614403118258601987/1618133427139792898","New blog post: A walkthrough on how to set up the WeeChat IRC client in docker. #irc #docker @WeeChatClient\ntiim.ch/blog/2023-01-1…","WeeChat",{"tag":19}],"uses":{"params":["slug"]}}]} diff --git a/tags/java.html b/tags/java.html new file mode 100644 index 00000000..a9f7d4af --- /dev/null +++ b/tags/java.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️java - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: java

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/java/__data.json b/tags/java/__data.json new file mode 100644 index 00000000..02a55994 --- /dev/null +++ b/tags/java/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":28},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":18,"abstract":21,"tags":22,"type":23,"cover_image":-1,"description":24,"folder":25,"comments":26,"latestComment":27},"\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>\n\u003Cp>Android widgets are handled by the operating system and only support a limited set of features for rendering.\nTo display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.\u003C/p>","projects/markdown-widget","c6a11779-cf98-4983-8744-9b1effae8d7a","Android Markdown Widget",["Date","2023-08-02T08:59:00.000Z"],null,"Projects",true,[12,13,14,15,16,17],"android","java","kotlin","markdown","widget","dev",[19,20],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Android-Markdown-Widget\" rel=\"nofollow noopener noreferrer\">Android Markdown Widget Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://f-droid.org/packages/ch.tiim.markdown_widget/\" rel=\"nofollow noopener noreferrer\">Download on F-Droid\u003C/a>\u003C/p>","\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>",[12,17,13,14,15,16],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":13}],"uses":{"params":["slug"]}}]} diff --git a/tags/javascript.html b/tags/javascript.html new file mode 100644 index 00000000..5228e57c --- /dev/null +++ b/tags/javascript.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️javascript - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: javascript

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/javascript/__data.json b/tags/javascript/__data.json new file mode 100644 index 00000000..a090aa3f --- /dev/null +++ b/tags/javascript/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"description":8,"content_tags":9,"date":14,"cover_image":15,"abstract":16,"tags":17,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\" rel=\"nofollow noopener noreferrer\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>\n\u003Cp>If you want to see an example of this method in action, go check out my \u003Ca href=\"https://tiimb.work\" rel=\"nofollow noopener noreferrer\">personal website\u003C/a> on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>\u003C/p>\n\u003Cp>I won't be explaining how to setup a Vue project. If you're looking for a Tutorial on that go check out the awesome \u003Ca href=\"https://vuejs.org/v2/guide/\" rel=\"nofollow noopener noreferrer\">Vue.js Guide\u003C/a>.\u003C/p>\n\u003Cp>So you have setup your awesome Vue project and want to host it on GitHub Pages. The way Muhammad explained it you would build the project using \u003Ccode>npm run build\u003C/code>, commit the \u003Ccode>dist/\u003C/code> folder along with your source files and point GitHub to the dist folder. This might get quite messy because you either have commit messages with the sole purpose of uploading the dist folder or you commit the code changes at the same time which makes it hard to find the relevant changes if you ever want to look at your commits again.\u003C/p>\n\u003Cp>So what can you do about this?\u003C/p>\n\u003Cp>Git to the rescue, let's use a branch that contains all the build files.\u003C/p>\n\u003Ch2>Step 1 - keeping our working branch clean 🛀\u003C/h2>\n\u003Cp>To make sure that the branch we are working from stays clean of any build files we are gonna add a \u003Ccode>.gitignore\u003C/code> file to the root.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># .gitignore\ndist/\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 2 - adding a second branch 🌳\u003C/h2>\n\u003Cp>We are not goint to branch off master like how we would do it if we were to modify our code with the intention to merge it back to the main branch. Instead we are gonna create a squeaky clean new branch that will only ever hold the dist files. After all we will not ever need to merge these two branches together.\u003C/p>\n\u003Cp>We do this by creating a new git repository inside the dist folder:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">cd dist/\ngit init\ngit add .\ngit commit -m 'Deploying my awesome vue app'\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 3 - deploying 🚚\u003C/h2>\n\u003Cp>We are gonna force push our new git repository to a branch on GitHub. This might go against git best practices but since we won't ever checkout this branch we don't have to worry about that.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">git push -f git@github.com:<username>/<repo>.git <branch>\n\u003C/code>\u003C/pre>\n\u003Cp>⚠️ Make sure you double or tripple check your destination branch! You don't want to accidentally overwrite your working branch. Using the branch \u003Ccode>gh-pages\u003C/code> will most likely be a good idea.\u003C/p>\n\u003Ch2>Step 4 - pointing GitHub to the right place 👈\u003C/h2>\n\u003Cp>Now we are almost done. The only thing left is telling GitHub where our assets live.\u003C/p>\n\u003Cp>Go to your repo, on the top right navigate to \u003Ccode>Settings\u003C/code> and scroll down to GitHub pages. Enable it and set your source branch to the branch you force pushed to, for example \u003Ccode>gh-pages\u003C/code>.\u003C/p>\n\u003Ch2>Step 5 - automating everything 😴\u003C/h2>\n\u003Cp>If you don't mind doing this whole process (Step 2 and 3) every time you want to deploy you can stop now. If you're as lazy as me, here is the script I use to deploy with one command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># deploy.sh\n\n#!/usr/bin/env sh\n\n# abort on errors\nset -e\n\n# build\necho Linting..\nnpm run lint\necho Building. this may take a minute...\nnpm run build\n\n# navigate into the build output directory\ncd dist\n\n# if you are deploying to a custom domain\n# echo 'example.com' > CNAME\n\necho Deploying..\ngit init\ngit add -A\ngit commit -m 'deploy'\n\n# deploy\ngit push -f git@github.com:<username>/<repo>.git <branch>\n\ncd -\n\n\u003C/code>\u003C/pre>\n\u003Cp>If your on windows look into the Windows Subsystem for Linus (WSL) it will be worth it.\u003C/p>\n\u003Cp>If you are still reading, thank you very much. This is actually my first article and I'm really happy to hear about any opinions and criticisms.\nHappy Coding ♥\u003C/p>","blog/2019-05-vue-on-github-pages","96054292-eb45-4d8a-9aca-bb050175ff2a","How I use Vue.js on GitHub Pages",true,"How to properly deploy a Vue.js app on GitHub Pages",[10,11,12,13],"GitHub Pages","Vue.js","Javascript","dev",["Date","2019-05-04T00:00:00.000Z"],"/assets/2019-05-vue-on-github-pages.png","\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>",[13,18,19,20],"github-pages","javascript","vue.js","article","blog",[],"2023-09-02T19:26:59Z",{"tag":19}],"uses":{"params":["slug"]}}]} diff --git a/tags/kotlin.html b/tags/kotlin.html new file mode 100644 index 00000000..2ca42a17 --- /dev/null +++ b/tags/kotlin.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️kotlin - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: kotlin

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/kotlin/__data.json b/tags/kotlin/__data.json new file mode 100644 index 00000000..3de21ad9 --- /dev/null +++ b/tags/kotlin/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":28},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":18,"abstract":21,"tags":22,"type":23,"cover_image":-1,"description":24,"folder":25,"comments":26,"latestComment":27},"\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>\n\u003Cp>Android widgets are handled by the operating system and only support a limited set of features for rendering.\nTo display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.\u003C/p>","projects/markdown-widget","c6a11779-cf98-4983-8744-9b1effae8d7a","Android Markdown Widget",["Date","2023-08-02T08:59:00.000Z"],null,"Projects",true,[12,13,14,15,16,17],"android","java","kotlin","markdown","widget","dev",[19,20],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Android-Markdown-Widget\" rel=\"nofollow noopener noreferrer\">Android Markdown Widget Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://f-droid.org/packages/ch.tiim.markdown_widget/\" rel=\"nofollow noopener noreferrer\">Download on F-Droid\u003C/a>\u003C/p>","\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>",[12,17,13,14,15,16],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":14}],"uses":{"params":["slug"]}}]} diff --git a/tags/lenex.html b/tags/lenex.html new file mode 100644 index 00000000..370fd863 --- /dev/null +++ b/tags/lenex.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️lenex - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: lenex

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/lenex/__data.json b/tags/lenex/__data.json new file mode 100644 index 00000000..cdc7de2d --- /dev/null +++ b/tags/lenex/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":16,"abstract":18,"tags":19,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\" rel=\"nofollow noopener noreferrer\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/lenex-splits-sheet-creator.png\" alt=\"Screenshot of a split sheet\">\u003C/p>\n\u003Cp>The split sheet creator is a quick and easy way to create a split sheet from your Lenex sign-up file.\nThe split sheet creator does not send your Lenex file to any servers. The file is opened directly in your browser and never leaves your computer!\u003C/p>\n\u003Cp>I built this web app using my \u003Ca href=\"https://www.npmjs.com/package/js-lenex\" rel=\"nofollow noopener noreferrer\">lenex javascript library\u003C/a>.\u003C/p>\n\u003Ch2>What is a split sheet?\u003C/h2>\n\u003Cp>Swim coaches often write down the times of athletes after some fractions of a race (splits). Usually those splits are recorded every lap or every second lap.\u003C/p>","projects/lenex-split-sheet","a03b6eef-ffbe-428d-a559-42bd361ab88c","Lenex Splits Sheet Creator",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14,15],"swim","lenex","svelte","dev",[17],"\u003Cp>\u003Ca href=\"https://tiim.ch/lenex-splits-sheet-creator/\" rel=\"nofollow noopener noreferrer\">Open Splits Sheet Creator\u003C/a>\u003C/p>","\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>",[15,13,14,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":13}],"uses":{"params":["slug"]}}]} diff --git a/tags/linux.html b/tags/linux.html new file mode 100644 index 00000000..4905e738 --- /dev/null +++ b/tags/linux.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️linux - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: linux

+ +

Getting the Absolute Path of a Remote Directory in Ansible

+ 9/20/2023 +
Getting the Absolute Path of a Remote Directory in Ansible +

There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/linux/__data.json b/tags/linux/__data.json new file mode 100644 index 00000000..a7b71f68 --- /dev/null +++ b/tags/linux/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_image_txt":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>\n\u003Cp>Deleting with elevated permission on the command line is easy: The command \u003Ccode>sudo rm -rf ~/docker/myservice\u003C/code> performs the \u003Ccode>rm\u003C/code> operation as the root user. In bash, this will delete the \u003Ccode>docker/myservice\u003C/code> folder in the user's home directory, but when doing the equivalent in Ansible, this won't work!\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\"># This does not work!\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"~/docker/myservice\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>This code will try to delete the file \u003Ccode>/user/root/docker/myservice\u003C/code>, which is not what we wanted.\u003C/p>\n\u003Cp>The bash version works because the shell first resolves the tilde in the argument to the current users' directory before calling the sudo command. In Ansible, we first switch to the root user and only then the tilde is resolved: this time to the home directory of the root user.\u003C/p>\n\u003Cp>To circumvent this, we can manually resolve the path to an absolute path. Unfortunately, I have not found a straightforward way to do this in Ansible, however the bash command \u003Ccode>readlink -f <path>\u003C/code> does exactly this. To use it in Ansible, we can use the following configuration:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yaml\">- name: Get absolute folder path\n ansible.builtin.command:\n cmd: \"readlink -f ~/docker/myservice\"\n register: folder_abs\n changed_when: False\n\n- name: Debug\n debug:\n msg: \"{{folder_abs.stdout}}\" # prints /user/tim/docker/myservice\n\n- name: Delete the folder using root permissions\n become: true\n ansible.builtin.file:\n path: \"{{folder_abs.stdout}}\"\n state: \"absent\"\n\u003C/code>\u003C/pre>\n\u003Cp>With this Ansible script, we manually resolve the absolute path and use it to delete the folder using root permissions. If you know of an easier way to resolve to an absolute path, please let me know!\u003C/p>","blog/2023-09-20-ansible-absolute-path","ad58acaf-56b0-4bcf-9b72-d6c054fc48d4",["Date","2023-09-20T21:39:13.000Z"],["Date","2023-09-20T20:22:35.634Z"],null,"Getting the Absolute Path of a Remote Directory in Ansible",true,"There is no builtin way to convert a relative path to an absolute path in ansible. However we can use the readlink command for this.","https://media.tiim.ch/3c1246e4-3201-4df6-af87-6aa4ab98800e.webp","(stable doodle) server room, neon, cables",[15,16,17,18],"dev","ansible","linux","bash","\u003Cp>I recently had to find a way to delete a folder using Ansible that was being created by Docker. The folder had a path like \u003Ccode>~/docker/myservice\u003C/code>. Since docker had created it as part of a volume, the folder did not belong to the current user. So deleting the folder using normal permissions failed.\u003C/p>",[16,18,15,17],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/markdown.html b/tags/markdown.html new file mode 100644 index 00000000..7ab54e53 --- /dev/null +++ b/tags/markdown.html @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️markdown - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: markdown

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/markdown/__data.json b/tags/markdown/__data.json new file mode 100644 index 00000000..cb781c68 --- /dev/null +++ b/tags/markdown/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":57},[2,25,43],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":15,"abstract":18,"tags":19,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>A github hosed wiki for all things 3D scanning: \u003Ca href=\"https://3dscanning.wiki/Photogrammetry\" rel=\"nofollow noopener noreferrer\">photogrammetry\u003C/a>, \u003Ca href=\"https://3dscanning.wiki/Lidar\" rel=\"nofollow noopener noreferrer\">lidar\u003C/a>, laser scanning and more. The page is a static website built from a github repository with markdown files.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Deprecated\u003C/span>\u003Cp>The 3D scanning wiki is now offline. But all pages are still available on github.\u003C/p>\n\u003C/blockquote>","projects/3d-scanning-wiki","fd95701b-6d38-4ff0-85a1-d1f919bf9251","The 3D Scanning Wiki",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-11-21T12:45:23.000Z"],"Projects",true,[12,13,14],"sveltekit","markdown","dev",[16,17],"\u003Cp>\u003Ca href=\"https://3dscanning.wiki/\" rel=\"nofollow noopener noreferrer\">3D Scanning Wiki\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://github.com/3dscanningwiki\" rel=\"nofollow noopener noreferrer\">Github Organisation\u003C/a>\u003C/p>","\u003Cp>A github hosed wiki for all things 3D scanning: \u003Ca href=\"https://3dscanning.wiki/Photogrammetry\">photogrammetry\u003C/a>, \u003Ca href=\"https://3dscanning.wiki/Lidar\">lidar\u003C/a>, laser scanning and more. The page is a static website built from a github repository with markdown files.\u003C/p>",[14,13,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"title":29,"date":30,"modified":31,"section":9,"published":10,"content_tags":32,"links":37,"abstract":40,"tags":41,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":42,"latestComment":24},"\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>\n\u003Cp>Android widgets are handled by the operating system and only support a limited set of features for rendering.\nTo display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.\u003C/p>","projects/markdown-widget","c6a11779-cf98-4983-8744-9b1effae8d7a","Android Markdown Widget",["Date","2023-08-02T08:59:00.000Z"],null,[33,34,35,13,36,14],"android","java","kotlin","widget",[38,39],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Android-Markdown-Widget\" rel=\"nofollow noopener noreferrer\">Android Markdown Widget Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://f-droid.org/packages/ch.tiim.markdown_widget/\" rel=\"nofollow noopener noreferrer\">Download on F-Droid\u003C/a>\u003C/p>","\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>",[33,14,34,35,13,36],[],{"html":44,"slug":45,"uuid":46,"title":47,"date":48,"modified":49,"section":9,"published":10,"content_tags":50,"links":52,"abstract":54,"tags":55,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":56,"latestComment":24},"\u003Cp>My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.\u003C/p>\n\u003Cp>I currently do not update this repo anymore because i moved all cheat sheets into my \u003Ca href=\"https://tiim.ch/tags/obsidian\" rel=\"nofollow noopener noreferrer\">Obsidian Vault\u003C/a>. I am looking into a way to keep those two repositories in sync in the future.\u003C/p>","projects/my-cheatsheets","c6d2511a-5d5c-4957-b730-5536f949a1b4","My Cheat Sheets",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[13,14,51],"cheatsheet",[53],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/my-cheatsheets\" rel=\"nofollow noopener noreferrer\">Cheat Sheets on Github\u003C/a>\u003C/p>","\u003Cp>My assorted collection of cheat sheets that I use almost daily. From useful LaTeX snippets to Linux commands to PostgreSQL and Python. Check it out and don't hesitate to contribute to it.\u003C/p>",[51,14,13],[],{"tag":13}],"uses":{"params":["slug"]}}]} diff --git a/tags/mastodon.html b/tags/mastodon.html new file mode 100644 index 00000000..408cba92 --- /dev/null +++ b/tags/mastodon.html @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️mastodon - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: mastodon

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/mastodon/__data.json b/tags/mastodon/__data.json new file mode 100644 index 00000000..917c0bd1 --- /dev/null +++ b/tags/mastodon/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":76},[2,38,66],{"html":3,"slug":4,"name":5,"date":6,"content_tags":7,"like_of":11,"raw_data":13,"tags":30,"links":-1,"published":32,"type":33,"cover_image":-1,"description":34,"folder":35,"comments":36,"latestComment":37},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html\">https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/12/otewmj","I Was Wrong About Mastodon",["Date","2022-12-01T08:24:00.000Z"],[8,9,10],"Mastodon","twitter","moderation",{"url":12},"https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html",{"items":14,"rels":28,"relurls":29},[15],{"id":16,"value":16,"html":16,"type":17,"properties":19,"shape":16,"coords":16,"children":27},"",[18],"h-entry",{"category":20,"like-of":21,"name":22,"post-status":23,"published":25},[8,9,10],[12],[5],[24],"published",[26],"2022-12-01T09:24:00+0100",[],{},{},[31,10,9],"mastodon",true,"like","👍 Liked: https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html","mf2",[],"2023-09-02T19:26:59Z",{"html":39,"slug":40,"name":41,"date":42,"content_tags":43,"like_of":46,"raw_data":48,"tags":62,"links":-1,"published":32,"type":33,"cover_image":-1,"description":64,"folder":35,"comments":65,"latestComment":37},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://werd.io/2022/the-fediverse-and-the-indieweb\">https://werd.io/2022/the-fediverse-and-the-indieweb\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/ntc1nd","The fediverse and the indieweb",["Date","2022-11-30T06:35:00.000Z"],[44,45,31],"Indieweb","fediverse",{"url":47},"https://werd.io/2022/the-fediverse-and-the-indieweb",{"items":49,"rels":60,"relurls":61},[50],{"id":16,"value":16,"html":16,"type":51,"properties":52,"shape":16,"coords":16,"children":59},[18],{"category":53,"like-of":54,"name":55,"post-status":56,"published":57},[44,45,31],[47],[41],[24],[58],"2022-11-30T07:35:00+0100",[],{},{},[45,63,31],"indieweb","👍 Liked: https://werd.io/2022/the-fediverse-and-the-indieweb",[],{"html":67,"slug":68,"date":69,"content_tags":70,"like_of":71,"tags":73,"links":-1,"published":32,"type":33,"cover_image":-1,"description":74,"folder":35,"comments":75,"latestComment":37},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/\">https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/mji1md",["Date","2022-11-22T10:28:00.000Z"],[9,45,31],{"url":72},"https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/",[45,31,9],"👍 Liked: https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/",[],{"tag":31}],"uses":{"params":["slug"]}}]} diff --git a/tags/mf2.html b/tags/mf2.html new file mode 100644 index 00000000..8507a9d2 --- /dev/null +++ b/tags/mf2.html @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️mf2 - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: mf2

+ +

Test note

+ 11/11/2022 +
+

This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/mf2/__data.json b/tags/mf2/__data.json new file mode 100644 index 00000000..3951f1f9 --- /dev/null +++ b/tags/mf2/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":91},[2,77],{"html":3,"slug":4,"uuid":5,"date":6,"aliases":7,"title":9,"published":10,"modified":11,"description":12,"cover_image":13,"content_tags":14,"syndication":21,"abstract":23,"tags":24,"links":-1,"type":27,"folder":28,"comments":29,"latestComment":34},"\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\" rel=\"nofollow noopener noreferrer\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\" rel=\"nofollow noopener noreferrer\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>\n\u003Ch2>The IndieWeb\u003C/h2>\n\u003Cp>I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.\u003C/p>\n\u003Cp>The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.\u003C/p>\n\u003Cp>The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.\u003C/p>\n\u003Cp>The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.\u003C/p>\n\u003Cp>Some of the standards in the IndieWeb include:\u003C/p>\n\u003Cul>\n\u003Cli>Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.\u003C/li>\n\u003Cli>Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.\u003C/li>\n\u003Cli>IndieAuth, an OAuth2-based way to log in using only your domain name.\u003C/li>\n\u003C/ul>\n\u003Ch2>The implementation on my website\u003C/h2>\n\u003Cp>As explained in my earlier post \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">First Go Project: A Jam-stack Commenting API\u003C/a>, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.\u003C/p>\n\u003Ch3>Making the website machine-readable with Microformats\u003C/h3>\n\u003Cp>As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called \u003Ca href=\"http://microformats.org/wiki/h-entry\" rel=\"nofollow noopener noreferrer\">h-entry\u003C/a>. By adding the \u003Ccode>h-entry\u003C/code> class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as \u003Ccode>p-name\u003C/code>, \u003Ccode>p-author\u003C/code> or \u003Ccode>dt-published\u003C/code>.\u003C/p>\n\u003Cp>While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.\u003C/p>\n\u003Cp>Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.\u003C/p>\n\u003Ch3>Accepting comments and other interactions via Webmentions\u003C/h3>\n\u003Cp>The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.\u003C/p>\n\u003Cp>In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.\u003C/p>\n\u003Cp>Since I already have a \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">small custom service\u003C/a> running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions\u003C/p>\n\u003Cp>It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database.\nI found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.\u003C/p>\n\u003Cp>I have tested my webmentions implementation using \u003Ca href=\"https://webmention.rocks\" rel=\"nofollow noopener noreferrer\">webmention.rocks\u003C/a>, but I would appreciate it if you left me a mention as well 😃\u003C/p>\n\u003Ch3>Publishing short-form content such as replies, likes and bookmarks: A notes post type\u003C/h3>\n\u003Cp>The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">notes\u003C/a>. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.\u003C/p>\n\u003Cp>I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: \u003Ca href=\"https://tiim.ch/full-rss.xml\" rel=\"nofollow noopener noreferrer\">full-rss.xml\u003C/a>. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.\u003C/p>\n\u003Ch3>Notifying referenced websites: Sending Webmentions\u003C/h3>\n\u003Cp>Sending webmentions was easy compared to receiving webmentions:\u003C/p>\n\u003Cp>On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.\u003C/p>\n\u003Cp>Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library \u003Ca href=\"https://github.com/go-co-op/gocron\" rel=\"nofollow noopener noreferrer\">gocron\u003C/a> for scheduling the regular intervals, \u003Ca href=\"https://github.com/mmcdole/gofeed\" rel=\"nofollow noopener noreferrer\">gofeed\u003C/a> for parsing the RSS feed and \u003Ca href=\"https://willnorris.com/go/webmention\" rel=\"nofollow noopener noreferrer\">webmention\u003C/a> for extracting links and sending webmentions.\u003C/p>\n\u003Ch3>In the future: IndieAuth\u003C/h3>\n\u003Cp>The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.\u003C/p>\n\u003Cp>Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.\u003C/p>\u003Cdiv class=\"mf2\">\u003Cblockquote class=\"syndication\">This post is also on \u003Cul>\u003Cli>\u003Ca class=\"u-syndication\" href=\"https://news.indieweb.org/en\">news.indieweb.org\u003C/a>\u003C/li>\u003C/ul>\u003C/blockquote>\u003C/div>\n","blog/2022-12-indiewebifying-my-website-part-1","3b342241-c414-4670-bd22-03e13d6531b7",["Date","2022-11-12T10:55:14.000Z"],[8],null,"IndieWebifying my Website Part 1 - Microformats and Webmentions",true,["Date","2022-12-03T20:56:54.000Z"],"This site now supports sending and receiving webmentions and surfacing structured data using microformats2.","https://i.imgur.com/FpgIBxI.jpg",[15,16,17,18,19,20],"IndieWeb","Webmentions","mf2","tiim.ch","go","indiego",[22],"https://news.indieweb.org/en","\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>",[19,20,25,17,18,26],"indieweb","webmentions","article","blog",[30,37,43,48,54,59,65,71],{"id":31,"type":32,"replyTo":33,"timestamp":34,"page":4,"url":35,"content":33,"name":36},"41435fdd-5fd2-4175-b9ea-ef9ce0dec154","webmention","","2023-09-02T19:26:59Z","https://evgenykuznetsov.org/en/reactions/2022/like-337215655/","Evgeny Kuznetsov",{"id":38,"type":32,"replyTo":33,"timestamp":39,"page":4,"url":40,"content":41,"name":42},"68bd3601-4f00-42c1-b28c-1f2ef75ac851","2023-08-02T09:10:03Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...","Tim Bachmann",{"id":44,"type":32,"replyTo":33,"timestamp":45,"page":4,"url":46,"content":47,"name":42},"5f508a42-8b83-4c10-9f7e-9c1b80e23ab1","2022-12-09T10:49:06Z","https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting","Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.",{"id":49,"type":32,"replyTo":33,"timestamp":50,"page":4,"url":51,"content":52,"name":53},"dc8dbf30-ff1f-4e13-ba36-37f12666005c","2022-12-05T08:41:07Z","https://martymcgui.re/2022/12/05/033926/","★ Liked https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","https://martymcgui.re/",{"id":55,"type":32,"replyTo":33,"timestamp":56,"page":4,"url":57,"content":33,"name":58},"6f6c1f11-2ae0-41a4-b7d0-e343ef63aa52","2022-11-27T22:32:27Z","https://brid.gy/like/twitter/tiimb/1591417020557525003/48372745","Jimmy Lipham",{"id":60,"type":32,"replyTo":33,"timestamp":61,"page":4,"url":62,"content":63,"name":64},"5e1c4149-8fb9-48fb-b285-5efbd626b259","2022-11-27T22:31:57Z","https://brid.gy/repost/twitter/tiimb/1591417020557525003/1591418692008558592","I published a new blog post:\nIndieWebifying my Website Part 1 - Microformats and Webmentions\ntiim.ch/blog/2022-12-i…\n#indieweb #microformats #webmentions #golang","Golang Smart Bot",{"id":66,"type":32,"replyTo":33,"timestamp":67,"page":4,"url":68,"content":69,"name":70},"6e0bf830-1735-42a2-9aa2-ea4c40ab7a45","2022-11-15T12:47:35Z","https://webmention.rocks/receive/1/f0fa5421056e068fe902932ef98f6d71","This test verifies that you accept a Webmention request that contains a valid source and target URL. To pass this test, your Webmention endpoint must return either HTTP 200, 201 or 202 along with the appropriate headers.\nIf your endpoint returns HTTP 201, then it MUST also return a Location header. If it returns HTTP 200 or 202, then it MUST NOT include a Location header.","Webmention Rocks!",{"id":72,"type":32,"replyTo":33,"timestamp":73,"page":4,"url":74,"content":75,"name":76},"4e237d08-9f22-480e-a46e-8f40adf06c5e","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/rm8as/","Liked\nIndieWebifying my Website Part 1 - Microformats and Webmentions\nPost detailsThis site now supports sending and receiving webmentions and surfacing structured data using microformats2. https://i.imgur.com/FpgIBxI.jpg","Jamie Tanna",{"html":78,"slug":79,"uuid":80,"date":81,"created":82,"title":83,"published":10,"modified":84,"content_tags":85,"reply_to":86,"abstract":87,"tags":88,"links":-1,"type":89,"cover_image":-1,"description":33,"folder":17,"comments":90,"latestComment":34},"\u003Ch2>This is a test note\u003C/h2>\n\u003Cp>This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)\u003C/p>\n\u003Cp>You can find a list of all my notes \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">here\u003C/a>\u003C/p>","mf2/2022/hkhmlv","874d0a10-6de8-4816-9b69-5c2fbe782774",["Date","2022-11-11T14:43:58.000Z"],["Date","2022-11-11T14:43:58.000Z"],"Test note",["Date","2022-11-11T14:43:58.000Z"],[17,25],"https://webmention.rocks/test/4","\u003Cp>This is a new content type on my website. Notes are not advertised but if you stumble upon them good for you ;)\u003C/p>",[25,17],"note",[],{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/micropub.html b/tags/micropub.html new file mode 100644 index 00000000..197d549f --- /dev/null +++ b/tags/micropub.html @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️micropub - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: micropub

+ +

adding Micropub support • AndreGarzia.com

+ 11/21/2022 +
+

👍 Liked: https://andregarzia.com/2022/03/adding-micropub-support.html

+
+
+ +

Hello Micropub

+ 11/21/2022 +
+

If you can read this entry, my micropub implementation works! You can expect a blog post about it shortly :)

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/micropub/__data.json b/tags/micropub/__data.json new file mode 100644 index 00000000..2a0e526e --- /dev/null +++ b/tags/micropub/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":29},[2,19],{"html":3,"slug":4,"title":5,"date":6,"content_tags":7,"like_of":10,"tags":12,"links":-1,"published":13,"type":14,"cover_image":-1,"description":15,"folder":16,"comments":17,"latestComment":18},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://andregarzia.com/2022/03/adding-micropub-support.html\">https://andregarzia.com/2022/03/adding-micropub-support.html\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/mjqymd","adding Micropub support • AndreGarzia.com",["Date","2022-11-21T14:25:00.000Z"],[8,9],"micropub","indieweb",{"url":11},"https://andregarzia.com/2022/03/adding-micropub-support.html",[9,8],true,"like","👍 Liked: https://andregarzia.com/2022/03/adding-micropub-support.html","mf2",[],"2023-09-02T19:26:59Z",{"html":20,"slug":21,"title":22,"date":23,"content_tags":24,"abstract":20,"tags":25,"links":-1,"published":13,"type":26,"cover_image":-1,"description":27,"folder":16,"comments":28,"latestComment":18},"\u003Cp>If you can read this entry, my micropub implementation works! You can expect a blog post about it shortly :)\u003C/p>","mf2/2022/11/ntuwnd","Hello Micropub",["Date","2022-11-21T13:55:00.000Z"],[9,8],[9,8],"note","",[],{"tag":8}],"uses":{"params":["slug"]}}]} diff --git a/tags/moderation.html b/tags/moderation.html new file mode 100644 index 00000000..bb5ee346 --- /dev/null +++ b/tags/moderation.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️moderation - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: moderation

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/moderation/__data.json b/tags/moderation/__data.json new file mode 100644 index 00000000..083d3a3f --- /dev/null +++ b/tags/moderation/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":38},[2],{"html":3,"slug":4,"name":5,"date":6,"content_tags":7,"like_of":11,"raw_data":13,"tags":30,"links":-1,"published":32,"type":33,"cover_image":-1,"description":34,"folder":35,"comments":36,"latestComment":37},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html\">https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/12/otewmj","I Was Wrong About Mastodon",["Date","2022-12-01T08:24:00.000Z"],[8,9,10],"Mastodon","twitter","moderation",{"url":12},"https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html",{"items":14,"rels":28,"relurls":29},[15],{"id":16,"value":16,"html":16,"type":17,"properties":19,"shape":16,"coords":16,"children":27},"",[18],"h-entry",{"category":20,"like-of":21,"name":22,"post-status":23,"published":25},[8,9,10],[12],[5],[24],"published",[26],"2022-12-01T09:24:00+0100",[],{},{},[31,10,9],"mastodon",true,"like","👍 Liked: https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html","mf2",[],"2023-09-02T19:26:59Z",{"tag":10}],"uses":{"params":["slug"]}}]} diff --git a/tags/networking.html b/tags/networking.html new file mode 100644 index 00000000..1a610813 --- /dev/null +++ b/tags/networking.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️networking - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: networking

+ +

Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN

+ 3/15/2023 +
Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN +

I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/networking/__data.json b/tags/networking/__data.json new file mode 100644 index 00000000..709c961e --- /dev/null +++ b/tags/networking/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>\n\u003Cp>I will show you how I fixed this problem for me and explain what the commands I used do. This post is mostly for my reference, but I hope it helps anyone else as well.\u003C/p>\n\u003Ch2>Finding out what your problem is\u003C/h2>\n\u003Cp>Let's check first if we have internet access inside WSL2. For this run the ping command with an IP address as a destination:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping 8.8.8.8\n\u003C/code>\u003C/pre>\n\u003Cp>If you get something like this as the output, your internet connection is fine, and it's just the DNS nameserver addresses that are misconfigured, you can jump forward to Solution 2.\u003C/p>\n\u003Cpre>\u003Ccode>PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n64 bytes from 8.8.8.8: icmp_seq=1 ttl=108 time=4.53 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=108 time=3.94 ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=108 time=3.97 ms\n64 bytes from 8.8.8.8: icmp_seq=4 ttl=108 time=3.78 ms\n64 bytes from 8.8.8.8: icmp_seq=5 ttl=108 time=3.77 ms\n64 bytes from 8.8.8.8: icmp_seq=6 ttl=108 time=3.76 ms\n64 bytes from 8.8.8.8: icmp_seq=7 ttl=108 time=3.81 ms\n\u003C/code>\u003C/pre>\n\u003Cp>If you don't get any responses from the ping (i.e. no more output after the \u003Ccode>PING 8.8.8.8 (8.8.8.8) ...\u003C/code> line), you need to configure the WSL and the VPN network adapter metric. Go to Solution 1.\u003C/p>\n\u003Cp>To check if the DNS is working, we can again use the ping command, this time with a domain name:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping google.com\n\u003C/code>\u003C/pre>\n\u003Cp>If you get responses, the DNS and your internet connection are working! If not go to Section 2.\u003C/p>\n\u003Ch2>Solution 1: Fixing the Network Adapter\u003C/h2>\n\u003Cp>Run the following two commands in PowerShell as administrator:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match \"Cisco AnyConnect\"} | Set-NetIPInterface -InterfaceMetric 4000\n\nGet-NetIPInterface -InterfaceAlias \"vEthernet (WSL)\" | Set-NetIPInterface -InterfaceMetric 1\n\u003C/code>\u003C/pre>\n\u003Cp>Let me explain what those two commands do. Both follow the same pattern of listing all network adapters, selecting a specific adapter from the list and setting its \"metric\".\u003C/p>\n\u003Cp>You can imagine an adapter as a virtual network port on the back of your pc or laptop. But instead of sending packets through the wire, the driver for a specific port can do whatever it wants with those packets, in the case of a VPN, the packets get encrypted and forwarded to the internet via another adapter.\u003C/p>\n\u003Cp>The \u003Ca href=\"https://learn.microsoft.com/en-us/windows-server/networking/technologies/network-subsystem/net-sub-interface-metric\" rel=\"nofollow noopener noreferrer\">InterfaceMetric\u003C/a> is a value associated with each adapter that determines the order of those adapters. This allows windows to determine which adapter to prefer over another one.\u003C/p>\n\u003Cp>By setting the interface metric of the Cisco adapter to 4000 and the metric of the WSL adapter to one, we allow the traffic from WSL to flow through the Cisco adapter. To be honest I do not exactly understand why this works but it does.\u003C/p>\n\u003Ch2>Solution 2: Registering the VPN DNS inside of WSL\u003C/h2>\n\u003Cp>Setting the DNS servers is, unfortunately, a little bit more involved than just running two commands, we need to edit the files \u003Ccode>/etc/wsl.conf\u003C/code> and \u003Ccode>/etc/resolv.conf\u003C/code>, and restart wsl in between. Let's get to it:\u003C/p>\n\u003Cp>Edit the file \u003Ccode>/etc/wsl.conf\u003C/code> inside of WSL2 using a text editor. I suggest doing this through the terminal since you need root permissions to do that:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/wsl.conf\n# feel free to use another editor such as vim or emacs\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely this file does not exist yet, otherwise, I suggest you create a backup of the original file to preserve the settings.\u003C/p>\n\u003Cp>Add the following config settings into the file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-ini\">[network]\ngenerateResolvConf = false\n\u003C/code>\u003C/pre>\n\u003Cp>This will instruct WSL to not override the \u003Ccode>/etc/resolv.conf\u003C/code> file on every start-up. Save the file and restart WSL with the following command so that the changed config takes effect:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open a PowerShell terminal and list all network adapters with the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ipconfig /all\n\u003C/code>\u003C/pre>\n\u003Cp>Find the Cisco AnyConnect adapter and copy the IP addresses in the DNS-Server field. We will need those IPs in the next step.\u003C/p>\n\u003Cp>Start WSL again and edit the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely there is already something in this file, you can discard it. When undoing the changes, WSL will automatically regenerate this file anyway, so you don't need to back it up.\u003C/p>\n\u003Cp>Delete all the contents and enter the IP addresses you noted down in the last step in the following format:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-resolv\">nameserver xxx.xxx.xxx.xxx\n\u003C/code>\u003C/pre>\n\u003Cp>Put each address on a new line, preceded by the string \u003Ccode>nameserver\u003C/code>.\nSave the file and restart WSL with the same command as above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open up WSL for the last time and set the immutable flag for the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">chattr +i /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>And for the last time shut down WSL. Your DNS should now be working fine!\u003C/p>\n\u003Ch2>Undoing those changes\u003C/h2>\n\u003Cp>I did not have a need to undo the steps for \u003Ccode>Solution 1\u003C/code>, and I'm pretty sure the metric resets after each system reboot anyway so there is not much to do.\u003C/p>\n\u003Cp>To get DNS working again when not connected to the VPN run the following commands:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo chattr -i /etc/resolv.conf\nsudo rm /etc/resolv.conf\nsudo rm /etc/wsl.conf\nwsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>This will first clear the immutable flag off \u003Ccode>/etc/resolv.conf\u003C/code>, and delete it. Next, it will delete \u003Ccode>/etc/wsl.conf\u003C/code> if you have a backup of a previous \u003Ccode>wsl.conf\u003C/code> file, you can replace it with that. At last, we shutdown WSL again for the changes to take effect.\u003C/p>\n\u003Cp>Unfortunately, this is quite a procedure to get a VPN to work with WSL2, but I'm hopeful that this will soon not be necessairy anymore.\u003C/p>","blog/2023-03-21-anyconnect-wsl2","c67bc4dc-4c96-41b1-afb5-15a99457dedf",["Date","2023-03-15T15:22:04.511Z"],["Date","2023-03-15T15:22:04.511Z"],[9],null,"Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN",true,"I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.","https://media.tiim.ch/66ca4290-3fc0-450f-977b-f00f888e4af3.webp","Stable Diffusion - Anything V3.0 - 1boy, hacker, in front of computer, back of head visible, vintage neon color scheme, terminal, big monitor",[16,17,18,19],"wsl","vpn","networking","dns","\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>",[19,18,17,16],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":18}],"uses":{"params":["slug"]}}]} diff --git a/tags/newsletter.html b/tags/newsletter.html new file mode 100644 index 00000000..b8ea2ca1 --- /dev/null +++ b/tags/newsletter.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️newsletter - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: newsletter

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/newsletter/__data.json b/tags/newsletter/__data.json new file mode 100644 index 00000000..6902c99c --- /dev/null +++ b/tags/newsletter/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":45},[2],{"html":3,"slug":4,"date":5,"content_tags":6,"in_reply_to":10,"raw_data":12,"abstract":30,"tags":31,"links":-1,"published":32,"type":33,"cover_image":-1,"description":34,"folder":35,"comments":36,"latestComment":44},"\u003Cdiv class=\"mf2\">\u003Cp>This post is in reply to \"\u003Ca class=\"u-in-reply-to\" href=\"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\">https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\u003C/a>\"\u003C/p>\u003C/div>\n\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>","mf2/2023/07/mteymz",["Date","2023-07-02T17:58:00.000Z"],[7,8,9],"rss","email","newsletter",{"url":11},"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/",{"items":13,"rels":28,"relurls":29},[14],{"id":15,"value":15,"html":15,"type":16,"properties":18,"shape":15,"coords":15,"children":27},"",[17],"h-entry",{"category":19,"content":20,"in-reply-to":22,"post-status":23,"published":25},[7,8,9],[21],"Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\n\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile. ",[11],[24],"published",[26],"2023-07-02T19:58:00+0200",[],{},{},"\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>",[8,9,7],true,"reply","💬 In reply to: https://kevincox.ca/2023/06/27/decade-of-rss-via-email/","mf2",[37],{"id":38,"type":39,"replyTo":15,"timestamp":40,"page":4,"url":41,"content":42,"name":43},"447820cb-5432-4f54-bd83-94e5674366e6","comment","2023-07-03T11:01:20Z","https://tiim.ch/mf2/2023/07/mteymz#447820cb-5432-4f54-bd83-94e5674366e6","I definitely get that email-based feed reading is not for everyone, but I don't think we are that different here. My inbox is indeed reserved for \"for things that require my attention\". That is why only a very small number of feeds go to my inbox (like WebMention replies to my posts). The vast majority of feeds go into dedicated folders with no notifications. So in a way these folders are my feed reader. The only relation that they have to my \"normal email workflow\" is that the exist in the same app. When I say I get my feeds via email lots of people immediately picture having feeds show up in their regular email workflow and are (rightfully) mortified. I think this approach would work well for almost no one. I think it is very important to have separation by priority much like Thunderbird has a separate news feed rather than dumping the feeds into your inbox. The difference here is that the separation is different folders in the same IMAP account rather than a distinct news account.","Kevin","2023-09-02T19:26:59Z",{"tag":9}],"uses":{"params":["slug"]}}]} diff --git a/tags/node.html b/tags/node.html new file mode 100644 index 00000000..8977bf9f --- /dev/null +++ b/tags/node.html @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️node - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: node

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/node/__data.json b/tags/node/__data.json new file mode 100644 index 00000000..0c1ef518 --- /dev/null +++ b/tags/node/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":30},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":19,"abstract":23,"tags":24,"type":25,"cover_image":-1,"description":26,"folder":27,"comments":28,"latestComment":29},"\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\" rel=\"nofollow noopener noreferrer\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>\n\u003Cp>The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents.\nThe admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/aqualetics-coach-screenshot.png\" alt=\"Screenshot of the coaches view\">\u003C/p>\n\u003Cp>The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.\u003C/p>","projects/aqualetics-coach","3210d289-1f9e-41b5-b1f9-d20f00f6a0c5","Aqualetics Coach",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14,15,16,17,18],"node","vue","graphql","hasura","postgresql","docker","dev",[20,21,22],"\u003Cp>\u003Ca href=\"https://sundrbi.ch/coach-application/\" rel=\"nofollow noopener noreferrer\">Overview\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch/2019/09/15/schwimmcoach-applikation-innovation/\" rel=\"nofollow noopener noreferrer\">Blog Post 🇩🇪\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch\" rel=\"nofollow noopener noreferrer\">Aqualetics Swim School\u003C/a>\u003C/p>","\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>",[18,17,14,15,12,16,13],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":12}],"uses":{"params":["slug"]}}]} diff --git a/tags/ntfy.sh.html b/tags/ntfy.sh.html new file mode 100644 index 00000000..bf946416 --- /dev/null +++ b/tags/ntfy.sh.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️ntfy.sh - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: ntfy.sh

+ +

Weechat Notifications with ntfy.sh

+ 3/28/2023 +
Weechat Notifications with ntfy.sh +

Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/ntfy.sh/__data.json b/tags/ntfy.sh/__data.json new file mode 100644 index 00000000..fe45566a --- /dev/null +++ b/tags/ntfy.sh/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\" rel=\"nofollow noopener noreferrer\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\" rel=\"nofollow noopener noreferrer\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>\n\u003Cp>Also, I recently found the web service \u003Ca href=\"https://ntfy.sh\" rel=\"nofollow noopener noreferrer\">ntfy.sh\u003C/a>. It sends push notifications whenever you send an HTTP post request to a certain URL. I already have ntfy.sh installed on my android phone, and I also found a minimal and lightweight \u003Ca href=\"https://github.com/lucas-bortoli/ntfysh-windows\" rel=\"nofollow noopener noreferrer\">desktop client\u003C/a>.\u003C/p>\n\u003Cp>I managed to set a WeeChat trigger up that fires every time I get mentioned (highlighted in WeeChat terminology), and a trigger that fires every time I get a private message. Both of those triggers execute the \u003Ccode>/exec\u003C/code> command which runs an arbitrary shell command. The exec command runs the \u003Ccode>wget\u003C/code> program to send a post request to the ntfy.sh server, which in turn sends a notification to all apps that subscribe to the same URL as the post request was sent. I would usually use the curl program for this instead of wget, but the docker default docker image doesn't contain a curl install.\u003C/p>\n\u003Cp>Here you can see the two \u003Ccode>/trigger\u003C/code> commands:\u003C/p>\n\u003Cp>\u003Cem>trigger on mention\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode>/trigger addreplace notify_highlight print '' '${tg_highlight}' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"- -header=Title: New highlight: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cem>trigger on private message\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-weechat\">/trigger addreplace notify_privmsg print '' '${tg_tag_notify} == private && ${buffer.notify} > 0' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"--header=Title: New private message: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Ch2>The trigger commands in detail\u003C/h2>\n\u003Cp>In case you don't just want to copy and paste some random command from the internet into your WeeChat (which you shouldn't do anyway), I will try to explain the trigger command that fires when you get mentioned in a message:\u003C/p>\n\u003Cp>Let's first look at the trigger command itself:\n\u003Ccode>/trigger addreplace <name> <hook> <argument> <condition> <variable-replace> <command>\u003C/code>\nWe call the \u003Ccode>/trigger\u003C/code> command with the \u003Ccode>addreplace\u003C/code> subcommand. This subcommand will either register a new trigger or replace it if one with the same name already exists.\u003C/p>\n\u003Cul>\n\u003Cli>\u003Ccode>name\u003C/code> - This argument is self-explanatory, the name of the trigger. In our case I called it \u003Ccode>notify_highlight\u003C/code>, but you could call it whatever you want.\u003C/li>\n\u003Cli>\u003Ccode>hook\u003C/code> - This argument specifies which hook or event the trigger should listen for. WeeChat is built as an event-driven platform, so pretty much anything from mouse movements to IRC messages are handled via events. In this case, we want to trigger on the \u003Ccode>print\u003C/code> event, which is fired every time a new message gets received from IRC.\u003C/li>\n\u003Cli>\u003Ccode>argument\u003C/code> - The argument is needed for some hooks, but not for the \u003Ccode>print\u003C/code> hook, so we are going to ignore that one for now and just set it to an empty string \u003Ccode>''\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>condition\u003C/code> - The condition must evaluate to \u003Ccode>true\u003C/code> for the trigger to fire. This is helpful because the \u003Ccode>print\u003C/code> trigger fires for every new message, but we only want to be notified when the new message mentions our nick. The condition for this is \u003Ccode>${tg_highlight}\u003C/code>. You can find the list of variables that you can access with the command \u003Ccode>/trigger monitor\u003C/code>, which prints all variables for every trigger that gets executed.\u003C/li>\n\u003Cli>\u003Ccode>variable-replace\u003C/code> - This took me a while to understand. This command is used to manipulate data and save it to a variable. The syntax is inspired by the sed command. Explaining it fully is out of the scope of this blog post, but you can take a look at the \u003Ca href=\"https://weechat.org/files/doc/devel/weechat_user.en.html#trigger_regex\" rel=\"nofollow noopener noreferrer\">docs\u003C/a>. In our example, we replace the whole content of the variable \u003Ccode>tg_message\u003C/code> with the format string \u003Ccode>${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}\u003C/code> which results in a sting like \u003Ccode><tiim> Hello world!\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>command\u003C/code> - The last argument is the command that gets executed whenever this trigger fires. In our case, we use the \u003Ccode>/execute\u003C/code> command, which starts the wget command which in turn sends a post request to ntfy.sh. Make sure you set the ntfy topic (the part after \u003Ccode>https://ntfy.sh/\u003C/code>) to something private and long enough so that nobody else is going to guess it by accident.\u003C/li>\n\u003C/ul>\n\u003Cp>Don't forget to subscribe to the ntfy topic on your phone or whatever device you want to receive the notification on.\u003C/p>\n\u003Cp>The possibilities with the trigger plugin are endless, I hope this inspires you to build your own customizations using weechat.\u003C/p>","blog/2023-03-28-weechat-notification-ntfy","ef51e944-86fa-44b0-ab9d-be7f8d8e569a",["Date","2023-03-28T10:05:19.000Z"],["Date","2023-01-15T20:35:40.643Z"],[9],null,"Weechat Notifications with ntfy.sh",true,"Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.","https://media.tiim.ch/97833b1d-d602-4d9a-9689-3077e96e45ba.webp","stable diffusion - Anything V3.0 - boy using an old DOS computer, 90s vibes, muted pastel colors, stylized, thick lines, IRC, console",[16,17,18,19],"weechat","ntfy.sh","wget","irc","\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>",[19,17,16,18],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/obsidian.html b/tags/obsidian.html new file mode 100644 index 00000000..bfbd4448 --- /dev/null +++ b/tags/obsidian.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️obsidian - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: obsidian

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/obsidian/__data.json b/tags/obsidian/__data.json new file mode 100644 index 00000000..8deb2bba --- /dev/null +++ b/tags/obsidian/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":16,"abstract":18,"tags":19,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\" rel=\"nofollow noopener noreferrer\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\" rel=\"nofollow noopener noreferrer\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>","projects/obsidian-fitbit-script","f42e0cad-df0e-4862-8beb-352071f81890","Obsidian.md Fitbit Activity Script",["Date","2022-04-27T20:40:15.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14,15],"obsidian","fitbit","plugin","dev",[17],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Fitbit-Obsidian-Templater-Script\" rel=\"nofollow noopener noreferrer\">GitHub Repo\u003C/a>\u003C/p>","\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>",[15,13,12,14],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":12}],"uses":{"params":["slug"]}}]} diff --git a/tags/pddl.html b/tags/pddl.html new file mode 100644 index 00000000..5a5aaf33 --- /dev/null +++ b/tags/pddl.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️pddl - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: pddl

+ +

Modelling Git Operations as Planning Problems

+ 1/20/2021 +
Modelling Git Operations as Planning Problems +

Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/pddl/__data.json b/tags/pddl/__data.json new file mode 100644 index 00000000..2ec9268a --- /dev/null +++ b/tags/pddl/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"description":8,"content_tags":9,"date":14,"modified":15,"cover_image":16,"abstract":17,"tags":18,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>\n\u003Cp>\u003Ca href=\"https://tiim.ch/assets/2021-01-20-Thesis.pdf\" rel=\"nofollow noopener noreferrer\">Download Thesis\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode>@thesis{bachmann2021,\n\ttitle = {Modelling Git Operations as Planning Problems},\n\tauthor = {Tim Bachmann},\n\tyear = {2021},\n month = {01},\n\ttype = {Bachelor's Thesis},\n\tschool = {University of Basel},\n\tdoi = {10.13140/RG.2.2.24784.17922}\n}\n\u003C/code>\u003C/pre>","blog/2021-01-git-operations-as-planning-problems","dc6e6d10-c460-4d3c-8fe2-4ce7535b4af1","Modelling Git Operations as Planning Problems",true,"Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.",[10,11,12,13],"Git","PDDL","Planning-System","dev",["Date","2021-01-20T00:00:00.000Z"],["Date","2023-09-18T11:41:51.000Z"],"/assets/2021-01-git-operations-as-planning-problems.png","\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>",[13,19,20,21],"git","pddl","planning-system","article","blog",[],"2023-09-02T19:26:59Z",{"tag":20}],"uses":{"params":["slug"]}}]} diff --git a/tags/pdr.html b/tags/pdr.html new file mode 100644 index 00000000..1b2dade4 --- /dev/null +++ b/tags/pdr.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️pdr - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: pdr

+ +

Automated Planning using Property-Directed Reachability with Seed Heuristics

+ 5/6/2023 +
Automated Planning using Property-Directed Reachability with Seed Heuristics +

Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/pdr/__data.json b/tags/pdr/__data.json new file mode 100644 index 00000000..dd244f09 --- /dev/null +++ b/tags/pdr/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":9,"published":10,"modified":11,"description":12,"cover_image":13,"cover_image_txt":8,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>\n\u003Cp>\u003Ca href=\"https://www.researchgate.net/publication/373994137_Automated_Planning_using_Property-Directed_Reachability_with_Seed_Heuristics\" rel=\"nofollow noopener noreferrer\">Download PDF\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-bibtex\">@phdthesis{bachmann2023,\n author = {Bachmann, Tim},\n year = {2023},\n month = {05},\n title = {Automated Planning using Property-Directed Reachability with Seed Heuristics},\n doi = {10.13140/RG.2.2.11456.30727},\n type = {Master's Thesis},\n school = {University of Basel}\n}\n\u003C/code>\u003C/pre>","blog/2023-05-06-pdr-with-seed-heuristics","111e68c4-0285-4f21-ab36-4c1ce1989da1",["Date","2023-05-06T11:15:53.000Z"],["Date","2023-05-06T11:15:53.000Z"],null,"Automated Planning using Property-Directed Reachability with Seed Heuristics",true,["Date","2023-09-18T13:32:00.000Z"],"Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.","https://media.tiim.ch/023c1722-ac3d-45fd-b66c-9ff319dfc180.webp",[15,16,17,18],"dev","planning-system","pdr","heuristic","\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>",[15,18,17,16],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/photo.html b/tags/photo.html new file mode 100644 index 00000000..544a148a --- /dev/null +++ b/tags/photo.html @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️photo - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: photo

+ +

+ 12/9/2022 +
 title image +

The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/photo/__data.json b/tags/photo/__data.json new file mode 100644 index 00000000..d7dd0817 --- /dev/null +++ b/tags/photo/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":38},[2],{"html":3,"slug":4,"date":5,"content_tags":6,"photos":9,"raw_data":12,"abstract":3,"tags":31,"links":-1,"published":33,"type":34,"cover_image":11,"description":15,"folder":35,"comments":36,"latestComment":37},"\u003Cp>The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once.\u003C/p>","mf2/2022/12/ota4mt",["Date","2022-12-09T10:50:00.000Z"],[7,8],"Photo","winter",[10],{"url":11},"https://media.tiim.ch/47537749-f79a-4603-92a8-42c71d6b96ec.jpg",{"items":13,"rels":29,"relurls":30},[14],{"id":15,"value":15,"html":15,"type":16,"properties":18,"shape":15,"coords":15,"children":28},"",[17],"h-entry",{"category":19,"content":20,"mp-photo-alt":22,"post-status":24,"published":26},[7,8],[21],"The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once. ",[23],"A snowy field near Basel, Switzerland. ",[25],"published",[27],"2022-12-09T11:50:00+0100",[],{},{},[32,8],"photo",true,"note","mf2",[],"2023-09-02T19:26:59Z",{"tag":32}],"uses":{"params":["slug"]}}]} diff --git a/tags/php.html b/tags/php.html new file mode 100644 index 00000000..63eddb07 --- /dev/null +++ b/tags/php.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️php - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: php

+ +

Swim Club Birsfelden Website

+ 3/5/2022 +
+

The website of the "ScBirs" swim club. This website serves as the center of all information distribution for the swimclub.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/php/__data.json b/tags/php/__data.json new file mode 100644 index 00000000..0e1b229f --- /dev/null +++ b/tags/php/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":23},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":15,"abstract":3,"tags":17,"type":18,"cover_image":-1,"description":19,"folder":20,"comments":21,"latestComment":22},"\u003Cp>The website of the \"ScBirs\" swim club. This website serves as the center of all information distribution for the swimclub.\u003C/p>","projects/scbirs-website","32787e66-2678-435f-be39-c27265bc9a6f","Swim Club Birsfelden Website",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14],"wordpress","php","dev",[16],"\u003Cp>\u003Ca href=\"https://scbirs.ch\" rel=\"nofollow noopener noreferrer\">Website\u003C/a>\u003C/p>",[14,13,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":13}],"uses":{"params":["slug"]}}]} diff --git a/tags/planning-system.html b/tags/planning-system.html new file mode 100644 index 00000000..2a24fd0c --- /dev/null +++ b/tags/planning-system.html @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️planning-system - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: planning-system

+ +

Automated Planning using Property-Directed Reachability with Seed Heuristics

+ 5/6/2023 +
Automated Planning using Property-Directed Reachability with Seed Heuristics +

Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.

+
+
+ +

Modelling Git Operations as Planning Problems

+ 1/20/2021 +
Modelling Git Operations as Planning Problems +

Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/planning-system/__data.json b/tags/planning-system/__data.json new file mode 100644 index 00000000..d1b0e44f --- /dev/null +++ b/tags/planning-system/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":43},[2,25],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":9,"published":10,"modified":11,"description":12,"cover_image":13,"cover_image_txt":8,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>\n\u003Cp>\u003Ca href=\"https://www.researchgate.net/publication/373994137_Automated_Planning_using_Property-Directed_Reachability_with_Seed_Heuristics\" rel=\"nofollow noopener noreferrer\">Download PDF\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-bibtex\">@phdthesis{bachmann2023,\n author = {Bachmann, Tim},\n year = {2023},\n month = {05},\n title = {Automated Planning using Property-Directed Reachability with Seed Heuristics},\n doi = {10.13140/RG.2.2.11456.30727},\n type = {Master's Thesis},\n school = {University of Basel}\n}\n\u003C/code>\u003C/pre>","blog/2023-05-06-pdr-with-seed-heuristics","111e68c4-0285-4f21-ab36-4c1ce1989da1",["Date","2023-05-06T11:15:53.000Z"],["Date","2023-05-06T11:15:53.000Z"],null,"Automated Planning using Property-Directed Reachability with Seed Heuristics",true,["Date","2023-09-18T13:32:00.000Z"],"Masters Thesis. The goal of this thesis is to implement a pre-processing step to the Property Directed Reachability algorithm, to potentially improve the run-time performance. We use the pattern database heuristic to make use of the planning task structure for the seeding algorithm.","https://media.tiim.ch/023c1722-ac3d-45fd-b66c-9ff319dfc180.webp",[15,16,17,18],"dev","planning-system","pdr","heuristic","\u003Cp>Planning is the process of finding a path in a planning task from the initial state to a goal state. Multiple algorithms have been implemented to solve such planning tasks, one of them being the Property-Directed Reachability algorithm. Property-Directed Reachability utilizes a series of propositional formulas called layers to represent a super-set of states with a goal distance of at most the layer index. The algorithm iteratively improves the layers such that they represent a minimum number of states. This happens by strengthening the layer formulas and therefore excluding states with a goal distance higher than the layer index. The goal of this thesis is to implement a pre-processing step to seed the layers with a formula that already excludes as many states as possible, to potentially improve the run-time performance. We use the pattern database heuristic and its associated pattern generators to make use of the planning task structure for the seeding algorithm. We found that seeding does not consistently improve the performance of the Property-Directed Reachability algorithm. Although we observed a significant reduction in planning time for some tasks, it significantly increased for others.\u003C/p>",[15,18,17,16],"article","blog",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"title":29,"published":10,"description":30,"content_tags":31,"date":35,"modified":36,"cover_image":37,"abstract":38,"tags":39,"links":-1,"type":21,"folder":22,"comments":42,"latestComment":24},"\u003Ch2>Abstract\u003C/h2>\n\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>\n\u003Cp>\u003Ca href=\"https://tiim.ch/assets/2021-01-20-Thesis.pdf\" rel=\"nofollow noopener noreferrer\">Download Thesis\u003C/a>\u003C/p>\n\u003Ch2>Cite\u003C/h2>\n\u003Cpre>\u003Ccode>@thesis{bachmann2021,\n\ttitle = {Modelling Git Operations as Planning Problems},\n\tauthor = {Tim Bachmann},\n\tyear = {2021},\n month = {01},\n\ttype = {Bachelor's Thesis},\n\tschool = {University of Basel},\n\tdoi = {10.13140/RG.2.2.24784.17922}\n}\n\u003C/code>\u003C/pre>","blog/2021-01-git-operations-as-planning-problems","dc6e6d10-c460-4d3c-8fe2-4ce7535b4af1","Modelling Git Operations as Planning Problems","Bachelor Thesis. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.",[32,33,34,15],"Git","PDDL","Planning-System",["Date","2021-01-20T00:00:00.000Z"],["Date","2023-09-18T11:41:51.000Z"],"/assets/2021-01-git-operations-as-planning-problems.png","\u003Cp>Version control systems use a graph data structure to track revisions of files. Those graphs are mutated with various commands by the respective version control system. The goal of this thesis is to formally define a model of a subset of Git commands which mutate the revision graph, and to model those mutations as a planning task in the Planning Domain Definition Language. Multiple ways to model those graphs will be explored and those models will be compared by testing them using a set of planners.\u003C/p>",[15,40,41,16],"git","pddl",[],{"tag":16}],"uses":{"params":["slug"]}}]} diff --git a/tags/plugin.html b/tags/plugin.html new file mode 100644 index 00000000..a440a61a --- /dev/null +++ b/tags/plugin.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️plugin - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: plugin

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/plugin/__data.json b/tags/plugin/__data.json new file mode 100644 index 00000000..bd6178b1 --- /dev/null +++ b/tags/plugin/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":16,"abstract":18,"tags":19,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\" rel=\"nofollow noopener noreferrer\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\" rel=\"nofollow noopener noreferrer\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>","projects/obsidian-fitbit-script","f42e0cad-df0e-4862-8beb-352071f81890","Obsidian.md Fitbit Activity Script",["Date","2022-04-27T20:40:15.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14,15],"obsidian","fitbit","plugin","dev",[17],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Fitbit-Obsidian-Templater-Script\" rel=\"nofollow noopener noreferrer\">GitHub Repo\u003C/a>\u003C/p>","\u003Cp>If you use \u003Ca href=\"https://obsidian.md/\">Obsidian.md\u003C/a> to track your daily activity and wear a fitbit, this script is for you! This is a user script for the \u003Ca href=\"https://silentvoid13.github.io/Templater/\">Templater\u003C/a> Obsidian plugin. The script will connect to the fitbit API to fetch all your activity for a given day and format it as markdown.\u003C/p>",[15,13,12,14],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":14}],"uses":{"params":["slug"]}}]} diff --git a/tags/postgres.html b/tags/postgres.html new file mode 100644 index 00000000..7595c4e3 --- /dev/null +++ b/tags/postgres.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️postgres - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: postgres

+ +

TeamKit

+ 11/27/2022 +
+

TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/postgres/__data.json b/tags/postgres/__data.json new file mode 100644 index 00000000..2b336000 --- /dev/null +++ b/tags/postgres/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":16,"abstract":18,"tags":19,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>\n\u003Cp>I built TeamKit because the swim club I am a part of needed an easy way for coaches to have an overview of their teams, handle attendance and track their own hours (not implemented yet). I tried a bunch of existing apps and services, but all of them were either too clunky for us or required the team members to sign up as well. This was a dealbreaker for us because we have a bunch of kids teams which are too young to sign up to websites, and because many of the parents are not very tech literate.\u003C/p>\n\u003Cp>With TeamKit a coach can quickly add new team members to a team, create new members directly in the event view (useful for example a person that just wants to try out) without having to add more details than a name.\u003C/p>\n\u003Cp>In the latest update, TeamKit now allows users to create notes on events. This is useful for planning, writing quick notes for an event or a practice session and for sharing information with other coaches.\u003C/p>\n\u003Cp>If you are interested, TeamKit is currenlty free while it is still in beta.\u003C/p>","projects/teamkit","a3040709-cd2d-43cb-ab33-2061ba1ae061","TeamKit",["Date","2022-11-27T09:19:08.000Z"],null,"Projects",true,[12,13,14,15],"sveltekit","hasura","postgres","dev",[17],"\u003Cp>\u003Ca href=\"https://teamkit.cc\" rel=\"nofollow noopener noreferrer\">TeamKit\u003C/a>\u003C/p>","\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>",[15,13,14,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":14}],"uses":{"params":["slug"]}}]} diff --git a/tags/postgresql.html b/tags/postgresql.html new file mode 100644 index 00000000..1b54109e --- /dev/null +++ b/tags/postgresql.html @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️postgresql - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: postgresql

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/postgresql/__data.json b/tags/postgresql/__data.json new file mode 100644 index 00000000..bd17f98c --- /dev/null +++ b/tags/postgresql/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":30},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":19,"abstract":23,"tags":24,"type":25,"cover_image":-1,"description":26,"folder":27,"comments":28,"latestComment":29},"\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\" rel=\"nofollow noopener noreferrer\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>\n\u003Cp>The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents.\nThe admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/aqualetics-coach-screenshot.png\" alt=\"Screenshot of the coaches view\">\u003C/p>\n\u003Cp>The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.\u003C/p>","projects/aqualetics-coach","3210d289-1f9e-41b5-b1f9-d20f00f6a0c5","Aqualetics Coach",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14,15,16,17,18],"node","vue","graphql","hasura","postgresql","docker","dev",[20,21,22],"\u003Cp>\u003Ca href=\"https://sundrbi.ch/coach-application/\" rel=\"nofollow noopener noreferrer\">Overview\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch/2019/09/15/schwimmcoach-applikation-innovation/\" rel=\"nofollow noopener noreferrer\">Blog Post 🇩🇪\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch\" rel=\"nofollow noopener noreferrer\">Aqualetics Swim School\u003C/a>\u003C/p>","\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>",[18,17,14,15,12,16,13],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":16}],"uses":{"params":["slug"]}}]} diff --git a/tags/project.html b/tags/project.html new file mode 100644 index 00000000..8a5063d0 --- /dev/null +++ b/tags/project.html @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️project - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: project

+ +

First Go Project: A Jam-stack Commenting API

+ 7/12/2022 +
First Go Project: A Jam-stack Commenting API +

I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/project/__data.json b/tags/project/__data.json new file mode 100644 index 00000000..67a5fcee --- /dev/null +++ b/tags/project/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":94},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":12,"description":13,"cover_image":14,"content_tags":15,"abstract":21,"tags":22,"links":-1,"type":23,"folder":24,"comments":25,"latestComment":93},"\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\" rel=\"nofollow noopener noreferrer\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>\n\u003Cp>I kept my eyes open for a service that I liked, but I did not want to just integrate any old service into my website, I did have some requirements:\u003C/p>\n\u003Cul>\n\u003Cli>The service should not cost anything. I would rather host something myself than sign up for another subscription (because I'm already paying for a VPS anyway).\u003C/li>\n\u003Cli>I want to control how the comments on my website are displayed. I quite like my website design and I don't want a generic comment box below my posts.\u003C/li>\n\u003Cli>The service should respect the privacy of the people using my website.\u003C/li>\n\u003Cli>There should be an option to comment without setting up an account with the service.\u003C/li>\n\u003C/ul>\n\u003Cp>While looking around for how other people integrated comments into their static websites, I found a nice \u003Ca href=\"https://averagelinuxuser.com/static-website-commenting/\" rel=\"nofollow noopener noreferrer\">blog post from Average Linux User\u003C/a> which compares a few popular commenting systems.\nUnfortunately, most systems either are not very privacy-friendly, cost money or store the comments as comments on Github issues..?\nAfter looking through the options I decided to use this opportunity to write my own commenting system and dabble with the Go programming language.\u003C/p>\n\u003Ch2>Writing a commenting API in Go\u003C/h2>\n\u003Cp>First thing first, if you want to take a look at the code, check out the \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">Github repo\u003C/a>.\u003C/p>\n\u003Cp>I decided to write the commenting system in Go because I have been looking for an excuse to practice Go for a while, and this seemed like the perfect fit. It is a small CRUD app, consisting of a storage component, an API component and a small event component in the middle to easily compose the functionality I want.\u003C/p>\n\u003Cp>Currently, it supports the following functionality:\u003C/p>\n\u003Cul>\n\u003Cli>Listing all comments (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Listing all comments for a specified page (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Posting comments through the API\u003C/li>\n\u003Cli>A simple admin dashboard that lists all comments and allows the admin to delete them\u003C/li>\n\u003Cli>Email notifications when someone comments\u003C/li>\n\u003Cli>Email notifications when someone replies to your comment\u003C/li>\n\u003Cli>SQLite storage for comments\u003C/li>\n\u003C/ul>\n\u003Cp>The code is built in a way to make it easy to customise the features.\nFor example to disable features like the email reply notifications you can just \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/main.go#L52\" rel=\"nofollow noopener noreferrer\">comment out the line in the main.go\u003C/a> file that registers that hook.\u003C/p>\n\u003Cp>To write custom hooks that get executed when a new comment gets submitted or one gets deleted, just implement the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/event/handler.go\" rel=\"nofollow noopener noreferrer\">Handler\u003C/a> interface and register it in the main method.\u003C/p>\n\u003Cp>You can also easily add other storage options like databases or file storage by implementing the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/model/store.go\" rel=\"nofollow noopener noreferrer\">Store and SubscribtionStore\u003C/a> interfaces.\u003C/p>\n\u003Ch2>Can it be used in production? 🚗💨\u003C/h2>\n\u003Cp>I currently use it on this website! Go test it out (I might delete the comments if they are rude though 🤔).\u003C/p>\n\u003Cp>In all seriousness, I would not use it for a website where the comments are critical. But for a personal blog or similar, I don't see why not.\u003C/p>\n\u003Cp>If you want to host your own version, there is a Dockerfile available. If you decide to integrate this into your website, please comment below, ping me \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> or shoot me an email \u003Ca href=\"mailto:hey@tiim.ch\">hey@tiim.ch\u003C/a>, I would love to check it out.\u003C/p>","blog/2022-07-12-first-go-project-commenting-api","bff14052-4f3f-4dcb-bcee-155ae1c6b09e",["Date","2022-07-12T00:00:00.000Z"],["Date","2022-07-08T16:24:37.766Z"],[9],null,"First Go Project: A Jam-stack Commenting API",true,["Date","2022-11-23T21:42:29.000Z"],"I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!","/assets/2022-07-first-go-project-commenting-api.png",[16,17,18,19,20],"go","web-api","project","tiim.ch","indiego","\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>",[16,20,18,19,17],"article","blog",[26,34,41,48,53,59,64,69,76,82,87],{"id":27,"type":28,"replyTo":29,"timestamp":30,"page":4,"url":31,"content":32,"name":33},"31ec5f44-15b2-498a-890d-350e38b9a83e","webmention","","2023-08-02T09:10:04Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...","Tim Bachmann",{"id":35,"type":36,"replyTo":29,"timestamp":37,"page":4,"url":38,"content":39,"name":40},"6d792d24-ba58-4408-83a4-3583667ff4ad","comment","2023-07-09T19:25:02Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#6d792d24-ba58-4408-83a4-3583667ff4ad","Heya just saw your post on Reddit about this comment feature, didn't want to leave without using it ^^. Nicely done!","Anonymous",{"id":42,"type":36,"replyTo":43,"timestamp":44,"page":4,"url":45,"content":46,"name":47},"99dd9ccf-5349-4f41-9553-67986e1a1074","1c8ba0da-10df-4a7a-b067-55875441de2d","2022-12-07T23:01:37Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#99dd9ccf-5349-4f41-9553-67986e1a1074","You are right, there is not any documentation in the readme yet. Although hopefully, I will work on that soon. I'm in the middle of refactoring the project.\n\nTo query the comments there are two rest endpoints [\"/comment\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L41-L71) and [\"/comment/:page\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L73-L107) which return all comments or the comments for a specific page. The comments are loaded from this API endpoint when the site is generated.\n\nTo display the comments without rebuilding the site, new comments are fetched in the browser with the \"?since=\u003Ctime-of-last-build>\" query parameter.","Tiim",{"id":43,"type":36,"replyTo":29,"timestamp":49,"page":4,"url":50,"content":51,"name":52},"2022-12-07T22:33:44Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#1c8ba0da-10df-4a7a-b067-55875441de2d","Good stuff. Always nice to see a site supporting comments and/or Webmentions. Maybe I missed it in the readme but I am curious as to how one queries the API for comments. Do you pull in the comments from the database when you generate the site?","Poorchop",{"id":54,"type":28,"replyTo":29,"timestamp":55,"page":4,"url":56,"content":57,"name":58},"e42756e4-d5a5-4727-8c58-434d285b7ab3","2022-11-27T22:31:58Z","https://brid.gy/repost/twitter/tiimb/1546801590593638400/1546801615264415745","I published a new blog post:\nFirst Go Project: A jam-stack Commenting API\ntiim.ch/blog/2022-07-1…\n#golang #jamstack #API","Golang Bot",{"id":60,"type":28,"replyTo":29,"timestamp":61,"page":4,"url":62,"content":63,"name":33},"b8d3ae8b-0059-4379-8c35-30c97269908f","2022-11-21T22:19:23Z","https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","This site now supports sending and receiving webmentions and surfacing structured data using microformats2.",{"id":65,"type":36,"replyTo":29,"timestamp":66,"page":4,"url":67,"content":68,"name":68},"171e3444-f1d0-492d-8bc7-c0a133a41783","2022-07-18T08:44:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#171e3444-f1d0-492d-8bc7-c0a133a41783","hola",{"id":70,"type":36,"replyTo":71,"timestamp":72,"page":4,"url":73,"content":74,"name":75},"5875bba1-e69b-467a-bab5-23ef4160d257","621574fd-eea2-48d6-87c8-aebd0f05f1aa","2022-07-13T21:06:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#5875bba1-e69b-467a-bab5-23ef4160d257","And a polite reply","polite",{"id":77,"type":36,"replyTo":29,"timestamp":78,"page":4,"url":79,"content":80,"name":81},"8494c653-ef37-47a2-ae1b-00d00e4815a9","2022-07-13T18:31:31Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#8494c653-ef37-47a2-ae1b-00d00e4815a9","Pretty cool dudez","somGuy",{"id":71,"type":36,"replyTo":29,"timestamp":83,"page":4,"url":84,"content":85,"name":86},"2022-07-12T21:40:39Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#621574fd-eea2-48d6-87c8-aebd0f05f1aa","This is a rude comment ;)","rude",{"id":88,"type":36,"replyTo":29,"timestamp":89,"page":4,"url":90,"content":91,"name":92},"fb6278ae-e48c-4397-ba29-bec4e5cb3a57","2022-07-12T13:30:14Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#fb6278ae-e48c-4397-ba29-bec4e5cb3a57","Good job dude!","wdup","2023-09-02T19:26:59Z",{"tag":18}],"uses":{"params":["slug"]}}]} diff --git a/tags/quick-tip.html b/tags/quick-tip.html new file mode 100644 index 00000000..a3208333 --- /dev/null +++ b/tags/quick-tip.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️quick-tip - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: quick-tip

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/quick-tip/__data.json b/tags/quick-tip/__data.json new file mode 100644 index 00000000..98ed79b3 --- /dev/null +++ b/tags/quick-tip/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":22},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"description":8,"content_tags":9,"date":13,"cover_image":14,"abstract":15,"tags":16,"links":-1,"type":18,"folder":19,"comments":20,"latestComment":21},"\u003Ch2>The problem\u003C/h2>\n\u003Cp>Let's say you have a rest API with the following endpoint that returns all of the books in your database:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-rest\">GET /book/\n\u003C/code>\u003C/pre>\n\u003Cp>Your SQL query might look like something like this\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT *\nFROM books\n\u003C/code>\u003C/pre>\n\u003Cp>Sometimes you want to only list books, for example, from a specific author. How do we do this in SQL?\u003C/p>\n\u003Ch2>Naive solution: String concatenation ✂\u003C/h2>\n\u003Cp>One way would be to concatenate your sql query something like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const arguments = [];\nconst queryString = \"SELECT * FROM books WHERE true\";\nif (authorFilter != null) {\n queryString += \"AND author = ?\";\n arguments.push(authorFilter);\n}\ndb.query(queryString, arguments);\n\u003C/code>\u003C/pre>\n\u003Cp>I'm not much of a fan of manually concatenating strings.\u003C/p>\n\u003Ch2>The coalesce function 🌟\u003C/h2>\n\u003Cp>Most Databases have the function \u003Ccode>coalesce\u003C/code> which accepts a variable amount of arguments and returns the first argument that is not null.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- Examle\nSELECT coalesce(null, null, 'tiim.ch', null, '@TiimB') as example;\n\n-- Will return\n\nexample\n---------\ntiim.ch\n\u003C/code>\u003C/pre>\n\u003Cp>But how will this function help us?\u003C/p>\n\u003Ch2>Optional filters with the coalesce function\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT *\nFROM books\nWHERE\n author = coalesce(?, author);\n\u003C/code>\u003C/pre>\n\u003Cp>If the filter value is null the coalesce expression will resolve to \u003Ccode>author\u003C/code>\nand the comparison \u003Ccode>author = author\u003C/code> will be true.\u003C/p>\n\u003Cp>If on the other hand the value is set for example to Shakespeare then the author will be compared to Shakespeare.\u003C/p>\n\u003Cp>I came across this way to implement optional filters only recently. If you have a more idiomatic way to do this let me know please ✨\u003C/p>\n\u003Cp>If you liked this post please follow me on here or on Twitter under \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> 😎\u003C/p>","blog/2019-07-sql-optional-filters-coalesce","899fb73c-a78e-4cd9-b712-1886715b2d56","How to write optional filters in SQL",true,"A simple way to filter by optional values in SQL with the COALESCE function.",[10,11,12],"SQL","quick-tip","dev",["Date","2019-07-11T00:00:00.000Z"],"/assets/2019-07-sql-optional-filters-coalesce.png","\u003Cp>Let's say you have a rest API with the following endpoint that returns all of the books in your database:\u003C/p>",[12,11,17],"sql","article","blog",[],"2023-09-02T19:26:59Z",{"tag":11}],"uses":{"params":["slug"]}}]} diff --git a/tags/reddit.html b/tags/reddit.html new file mode 100644 index 00000000..8e61c4c8 --- /dev/null +++ b/tags/reddit.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️reddit - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: reddit

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/reddit/__data.json b/tags/reddit/__data.json new file mode 100644 index 00000000..551ad1a7 --- /dev/null +++ b/tags/reddit/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\" rel=\"nofollow noopener noreferrer\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\" rel=\"nofollow noopener noreferrer\">Way back machine\u003C/a>.\u003C/p>\n\u003Cp>Another community I used to be really active in, was for a small indie roleplaying game called \u003Ca href=\"\">Illarion\u003C/a>. Again, the community relied heavily on a forum for communications. This time it was used for players to engage in \"out of character\" communication, as well as a way to simulate a metaphorical bullet board in the game town square where characters could leave notes for each other.\nSince the game was closely inspired by TTRPGs like D&D, the role-playing part was more important than the in-game mechanics. The forum allowed characters to interact with each other that were not online at the same time. Again, I got really invested in this community, even going so far as joining other guild-specific forums.\u003C/p>\n\u003Cp>I eventually moved on from both of those amazing communities, because my interests changed. I left the AutoHotkey community because I started to get more involved with other programming languages, and I left the Illarion community because I (with the support of my parents) was looking for a less time-intensive game. Unfortunately, I never happened to find another online community like those two ever again...\u003C/p>\n\u003Cp>Sometime later I joined Reddit and was amazed. It felt like a place where all communities come together on a single site. No need to check on multiple websites for new posts, everything neatly together in a single website, accessible on a single (third party) app. I remember wondering why people were still using forums when Reddit was so much simpler.\u003C/p>\n\u003Cp>Jumping to the present and I realize that I was wrong. Even though I am subscribed to a bunch of communities on Reddit, I barely comment on any posts and posted even less. While I am a community member on record, I do not feel like one. The wealth of communities, as well as the incentive to go on the front page to see the most popular posts of the whole site, made me want to open Reddit, but it did not give me the feeling of belonging. I rather felt like a spectator that from time to time gathers the courage to shout his own ideas into the ether.\u003C/p>\n\u003Cblockquote>\n\u003Cp>Side note: Discord comes much closer to the feeling of community. However, the nature of chat makes the interactions fleeting, being in a chat room with a few hundred other people, where every message is just a few sentences at most does not lead to the same connections. No one expects their message to be read again after a few days.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Now the company behind Reddit started to lose the goodwill of the users. While I don't think Reddit will die anytime soon, I think there are a lot of people looking for alternatives. And the best alternative to the website that killed forums is... forums.\u003C/p>\n\u003Cp>While forums largely still work the same as they did 15 years ago, there have been developments that might make them more feasible for our desire to have everything accessible on a single site or on a single app. Last time a social media company, Twitter, annoyed its user base, the fediverse, and more specifically Mastodon, started to go more mainstream. This time I hope there will be other projects that profit. I have heard people mentioning the projects Kbin and Lemmy, both forum-like platforms that implement the ActivityPub specification. Same as Mastodon, this means users are able to interact with users on other instances. Even further, this should also allow users of any federated social network, such as Mastodon, to post and comment on any federated forum. Even established forum software such as \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">Flarum\u003C/a> and \u003Ca href=\"https://community.nodebb.org/topic/17117/what-s-next-after-v3/18\" rel=\"nofollow noopener noreferrer\">nodeBB\u003C/a> are considering adding federation support.\u003C/p>\n\u003Cp>I really hope that forums make a comeback, not only because of the nostalgia but also because to me it feels like a more sustainable way to build a community. And now with the possibility to federate via the fediverse, a forum doesn't have to be a walled garden of members any more. In the end, most importantly I hope people are still finding communities they can be as passionate about as I was, without any corporate overlords trying to keep their eyeballs on ads as long as possible.\u003C/p>","blog/2023-06-16-forums","624afba6-2962-4710-9bc7-686702cc9b55",["Date","2023-06-16T18:56:56.000Z"],["Date","2023-06-16T15:09:15.488Z"],[9],null,"Forums",true,"My experience of using forums in my teens, what changed after I started using reddit and my hopes for internet communities in the future.","https://media.tiim.ch/fe5de393-9773-4eaa-877a-decffbd706b4.webp","Stable Diffusion - bunch people talking to each other, social, speech bubbles, digital art, minimalistic",[16,17,18,19],"forum","fediverse","reddit","activitypub","\u003Cp>My first real programming experience was with a scripting language called \u003Ca href=\"https://www.autohotkey.com/\">AutoHotkey\u003C/a>. This was before I was fluent enough in English to join the English-speaking community around this language. But luckily, there was an official German forum. It was really active, not only consisting of newcomers to the language but also veterans. When I joined this forum in my teens I quickly went from just asking beginner questions, to enjoying helping other beginners, that asked the same questions as I did previously. I got better at the language, learned new programming concepts all through reading posts, helped others, and shared my projects on this forum. I got excited when I saw a post from other users that I recognized.\nWhen AutoHotkey got forked and the new interpreter introduced classes and object-oriented programming, I felt in way over my head. Since I was not alone in this, one person took the time to write an incredibly detailed guide as a forum post. I recently found this post printed on paper. I had printed it right before going on vacation since I desperately wanted to learn but knew I was not going to have access to the internet for a while.\nUnfortunately, the German forum has since been discontinued, but some of the pages are still up on the \u003Ca href=\"https://web.archive.org/web/20121005080807/http://de.autohotkey.com/forum/\">Way back machine\u003C/a>.\u003C/p>",[19,17,16,18],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":18}],"uses":{"params":["slug"]}}]} diff --git a/tags/rss.html b/tags/rss.html new file mode 100644 index 00000000..6a529c25 --- /dev/null +++ b/tags/rss.html @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️rss - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: rss

+ +

You should be using RSS

+ 6/5/2022 +
You should be using RSS +

Decide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/rss/__data.json b/tags/rss/__data.json new file mode 100644 index 00000000..5f80eb2c --- /dev/null +++ b/tags/rss/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":71},[2,32],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"content_tags":14,"abstract":18,"tags":19,"links":-1,"type":20,"folder":21,"comments":22,"latestComment":31},"\u003Cp>I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.\u003C/p>\n\u003Cp>But there is another way! By subscribing to RSS feeds you are in control of what you are shown. Most websites, blogs, news sites and even social media sites provide RSS feeds to subscribe to. You get only the articles, videos or audio content you are subscribed to, without any algorithm messing with your attention.\u003C/p>\n\u003Ch2>But what exactly is an RSS feed?\u003C/h2>\n\u003Cp>RSS stands for \"Really Simple Syndication\", and it is a protocol for a website to provide a list of content. It is an old protocol, the first version was introduced in 1999, but it might be more useful nowadays than ever.\nIf you listen to podcasts, you are already familiar with RSS feeds: a podcast is an RSS feed which links to audio files instead of online articles.\nAn RSS feed is just an XML document which contains information about the feed and a list of content.\nWhen you use an app to subscribe to an RSS feed, this app will just save the URL to the XML document and load it regularly to check if new content is available. You are completely in control of how often the feed is refreshed and what feeds you want to subscribe to. Some RSS reader apps also allow you to specify some rules for example about if you should be notified, based on the feed, the content or the tags.\u003C/p>\n\u003Ch2>How to subscribe to a feed?\u003C/h2>\n\u003Cp>Since an RSS feed is just an XML document, you don't \u003Cem>technically\u003C/em> have to subscribe to a feed to read it, you \u003Cem>could\u003C/em> just open the document and read the XML. But that would be painful. Luckily there are several plugins, apps and services that allow you to easily subscribe to and read RSS feeds.\u003C/p>\n\u003Cp>If you want to start using RSS and are not sure if you will take the time to open a dedicated app, I would recommend using an RSS plugin for another software that you are using regularly. For example, the \u003Ca href=\"https://thunderbird.net/\" rel=\"nofollow noopener noreferrer\">Thunderbird\u003C/a> email client already has built-in RSS support. If you want to read to the feeds directly inside of your browser, you can use the \u003Ca href=\"https://nodetics.com/feedbro/\" rel=\"nofollow noopener noreferrer\">feedbro\u003C/a> extension for Chrome, Firefox, and other Chromium-based browsers. I use the \u003Ca href=\"https://vivaldi.com\" rel=\"nofollow noopener noreferrer\">Vivaldi\u003C/a> browser which comes with an integrated RSS feed reader.\u003C/p>\n\u003Ch2>What if there is no RSS feed?\u003C/h2>\n\u003Cp>Unfortunately not every website offers an RSS feed. Although it might be worth it to hunt for them. Some websites offer an RSS feed but do not link to it anywhere.\nIf there is no feed, but a newsletter is offered, the service \"\u003Ca href=\"https://kill-the-newsletter.com\" rel=\"nofollow noopener noreferrer\">Kill The Newsletter\u003C/a>\" will provide you with email addresses and a corresponding RSS URL to convert any newsletter to a feed. Another service to consider is \u003Ca href=\"http://fetchrss.com\" rel=\"nofollow noopener noreferrer\">FetchRSS\u003C/a>. It turns any website into an RSS feed.\u003C/p>\n\u003Ch2>RSS Apps\u003C/h2>\n\u003Cp>If you want to have a dedicated app for your reading, you're in luck! There is a plethora of apps to choose from, all with different features and user interfaces.\nThere are three main types of apps: standalone apps, service-based apps, and self-hosted apps. Most apps are standalone, meaning they fetch the RSS feeds only when open, and don't sync to your other devices. The service-based apps rely on a cloud service which will fetch the feeds around the clock, even when all your devices are off. They can also send you a summary mail if you forget to check for some time and they can sync your subscriptions across all your devices. Unfortunately, most service-based apps only offer a limited experience for free. The last category is self-hosted apps. They are similar to the service based apps but instead of some company running the service, you have to provide a server for the service to run yourself.\u003C/p>\n\u003Cp>I use a standalone app, because I do not want to rely on a service, but I also don't want to go through the hassle of setting up a self-hosted solution.\u003C/p>\n\u003Cp>If you are still unsure what RSS app you could try out, I provided a list below. Make sure to add the \u003Ca href=\"https://tiim.ch/blog/rss.xml\" rel=\"nofollow noopener noreferrer\">RSS feed for my blog\u003C/a> (\u003Ccode>https://tiim.ch/blog/rss.xml\u003C/code>) to test it out 😉\u003C/p>\n\u003Ch3>Standalone Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://thunderbird.net\" rel=\"nofollow noopener noreferrer\">Thunderbird\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://ravenreader.app\" rel=\"nofollow noopener noreferrer\">RavenReader\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://netnewswire.com\" rel=\"nofollow noopener noreferrer\">NetNewsWire\u003C/a> (Free, Integration with Services possible)\u003C/li>\n\u003Cli>\u003Ca href=\"https://vivaldi.com\" rel=\"nofollow noopener noreferrer\">Vivaldi Browser\u003C/a> (Free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://nodetics.com/feedbro/\" rel=\"nofollow noopener noreferrer\">feedbro browser extension\u003C/a> (Free)\u003C/li>\n\u003C/ul>\n\u003Ch3>Service-Based Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://feedreader.com\" rel=\"nofollow noopener noreferrer\">FeedReader\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://feeder.co\" rel=\"nofollow noopener noreferrer\">Feeder\u003C/a> (Freemium, 10 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.inoreader.com/pricing\" rel=\"nofollow noopener noreferrer\">Inoreader\u003C/a> (Freemium, Ads and 150 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://newsblur.com\" rel=\"nofollow noopener noreferrer\">NewsBlur\u003C/a> (Freemium, 64 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.feedspot.com\" rel=\"nofollow noopener noreferrer\">Feedspot\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://feedly.com\" rel=\"nofollow noopener noreferrer\">Feedly\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://feedbin.com\" rel=\"nofollow noopener noreferrer\">Feedbin\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://theoldreader.com\" rel=\"nofollow noopener noreferrer\">TheOldReader\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://bazqux.com\" rel=\"nofollow noopener noreferrer\">BazQux\u003C/a>\u003C/li>\n\u003C/ul>\n\u003Ch3>Self-hosted Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://www.commafeed.com/\" rel=\"nofollow noopener noreferrer\">CommaFeed\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://freshrss.org\" rel=\"nofollow noopener noreferrer\">FreshRSS\u003C/a> (Free, OSS)\u003C/li>\n\u003C/ul>","blog/2022-06-use-rss","ee633c0d-d668-48e5-af57-aaae9d243099",["Date","2022-06-05T00:00:00.000Z"],["Date","2022-06-04T22:01:15.123Z"],[9],null,"You should be using RSS",true,"Decide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read.","https://i.imgur.com/t3mebu7.png",[15,16,17],"rss","dev","software","\u003Cp>I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.\u003C/p>",[16,15,17],"article","blog",[23],{"id":24,"type":25,"replyTo":26,"timestamp":27,"page":4,"url":28,"content":29,"name":30},"31ea6d40-e8c2-4ada-ac16-028a3036d2de","webmention","","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/oatm9/","Liked\nYou should be using RSS\nPost detailsDecide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read. https://i.imgur.com/t3mebu7.png","Jamie Tanna","2023-09-02T19:26:59Z",{"html":33,"slug":34,"date":35,"content_tags":36,"in_reply_to":39,"raw_data":41,"abstract":58,"tags":59,"links":-1,"published":11,"type":60,"cover_image":-1,"description":61,"folder":62,"comments":63,"latestComment":31},"\u003Cdiv class=\"mf2\">\u003Cp>This post is in reply to \"\u003Ca class=\"u-in-reply-to\" href=\"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\">https://kevincox.ca/2023/06/27/decade-of-rss-via-email/\u003C/a>\"\u003C/p>\u003C/div>\n\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>","mf2/2023/07/mteymz",["Date","2023-07-02T17:58:00.000Z"],[15,37,38],"email","newsletter",{"url":40},"https://kevincox.ca/2023/06/27/decade-of-rss-via-email/",{"items":42,"rels":56,"relurls":57},[43],{"id":26,"value":26,"html":26,"type":44,"properties":46,"shape":26,"coords":26,"children":55},[45],"h-entry",{"category":47,"content":48,"in-reply-to":50,"post-status":51,"published":53},[15,37,38],[49],"Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\n\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile. ",[40],[52],"published",[54],"2023-07-02T19:58:00+0200",[],{},{},"\u003Cp>Its funny how preferences vary. I reserve email for things that require my attention, and news/blog articles definitely don't fall unter that. I even use the service kill-the-newsletter.com to convert the newsletters I want to read into RSS feeds.\nThe syncing aspect is however a good point. I really wish I could sync the thunderbird newsfeed over multiple devices and on mobile.\u003C/p>",[37,38,15],"reply","💬 In reply to: https://kevincox.ca/2023/06/27/decade-of-rss-via-email/","mf2",[64],{"id":65,"type":66,"replyTo":26,"timestamp":67,"page":34,"url":68,"content":69,"name":70},"447820cb-5432-4f54-bd83-94e5674366e6","comment","2023-07-03T11:01:20Z","https://tiim.ch/mf2/2023/07/mteymz#447820cb-5432-4f54-bd83-94e5674366e6","I definitely get that email-based feed reading is not for everyone, but I don't think we are that different here. My inbox is indeed reserved for \"for things that require my attention\". That is why only a very small number of feeds go to my inbox (like WebMention replies to my posts). The vast majority of feeds go into dedicated folders with no notifications. So in a way these folders are my feed reader. The only relation that they have to my \"normal email workflow\" is that the exist in the same app. When I say I get my feeds via email lots of people immediately picture having feeds show up in their regular email workflow and are (rightfully) mortified. I think this approach would work well for almost no one. I think it is very important to have separation by priority much like Thunderbird has a separate news feed rather than dumping the feeds into your inbox. The difference here is that the separation is different folders in the same IMAP account rather than a distinct news account.","Kevin",{"tag":15}],"uses":{"params":["slug"]}}]} diff --git a/tags/rust.html b/tags/rust.html new file mode 100644 index 00000000..0c771b04 --- /dev/null +++ b/tags/rust.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️rust - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: rust

+ +

Pomo 🍅

+ 8/3/2023 +
+

I created pomo as a way to keep me focused for working on my masters thesis, and at the same time +allowed me to learn the rust programming language.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/rust/__data.json b/tags/rust/__data.json new file mode 100644 index 00000000..14a36be8 --- /dev/null +++ b/tags/rust/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":24},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":15,"abstract":17,"tags":18,"type":19,"cover_image":-1,"description":20,"folder":21,"comments":22,"latestComment":23},"\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>\n\u003Cp>Pomo is a simple pomodoro timer. It allows you to either specify the number of repetitions (pomodori), the duration of the pomodori and the duration of the breaks, or\nyou can stecify an end time, and let pomo calculate the durations and repetitions.\u003C/p>\n\u003Cp>Pomo runs as a cli tool and stores the current state in a json file. All pomo executions excep \u003Ccode>pomo watch\u003C/code> just\nmodify this json file and terminate. The watch command displays the current pomodoro timer, optionally writes the timer to a text file,\nand watches for changes of the json file.\u003C/p>","projects/pomo","bfa1a7fa-b8a3-469f-b8e8-727ab705cb93","Pomo 🍅",["Date","2023-08-03T11:03:00.000Z"],null,"Projects",true,[12,13,14],"rust","cli","dev",[16],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/pomo\" rel=\"nofollow noopener noreferrer\">pomo Github\u003C/a>\u003C/p>","\u003Cp>I created pomo as a way to keep me focused for working on my masters thesis, and at the same time\nallowed me to learn the rust programming language.\u003C/p>",[13,14,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":12}],"uses":{"params":["slug"]}}]} diff --git a/tags/software.html b/tags/software.html new file mode 100644 index 00000000..67c8c650 --- /dev/null +++ b/tags/software.html @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️software - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: software

+ +

You should be using RSS

+ 6/5/2022 +
You should be using RSS +

Decide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/software/__data.json b/tags/software/__data.json new file mode 100644 index 00000000..036f22e4 --- /dev/null +++ b/tags/software/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":48},[2,32],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"content_tags":14,"abstract":18,"tags":19,"links":-1,"type":20,"folder":21,"comments":22,"latestComment":31},"\u003Cp>I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.\u003C/p>\n\u003Cp>But there is another way! By subscribing to RSS feeds you are in control of what you are shown. Most websites, blogs, news sites and even social media sites provide RSS feeds to subscribe to. You get only the articles, videos or audio content you are subscribed to, without any algorithm messing with your attention.\u003C/p>\n\u003Ch2>But what exactly is an RSS feed?\u003C/h2>\n\u003Cp>RSS stands for \"Really Simple Syndication\", and it is a protocol for a website to provide a list of content. It is an old protocol, the first version was introduced in 1999, but it might be more useful nowadays than ever.\nIf you listen to podcasts, you are already familiar with RSS feeds: a podcast is an RSS feed which links to audio files instead of online articles.\nAn RSS feed is just an XML document which contains information about the feed and a list of content.\nWhen you use an app to subscribe to an RSS feed, this app will just save the URL to the XML document and load it regularly to check if new content is available. You are completely in control of how often the feed is refreshed and what feeds you want to subscribe to. Some RSS reader apps also allow you to specify some rules for example about if you should be notified, based on the feed, the content or the tags.\u003C/p>\n\u003Ch2>How to subscribe to a feed?\u003C/h2>\n\u003Cp>Since an RSS feed is just an XML document, you don't \u003Cem>technically\u003C/em> have to subscribe to a feed to read it, you \u003Cem>could\u003C/em> just open the document and read the XML. But that would be painful. Luckily there are several plugins, apps and services that allow you to easily subscribe to and read RSS feeds.\u003C/p>\n\u003Cp>If you want to start using RSS and are not sure if you will take the time to open a dedicated app, I would recommend using an RSS plugin for another software that you are using regularly. For example, the \u003Ca href=\"https://thunderbird.net/\" rel=\"nofollow noopener noreferrer\">Thunderbird\u003C/a> email client already has built-in RSS support. If you want to read to the feeds directly inside of your browser, you can use the \u003Ca href=\"https://nodetics.com/feedbro/\" rel=\"nofollow noopener noreferrer\">feedbro\u003C/a> extension for Chrome, Firefox, and other Chromium-based browsers. I use the \u003Ca href=\"https://vivaldi.com\" rel=\"nofollow noopener noreferrer\">Vivaldi\u003C/a> browser which comes with an integrated RSS feed reader.\u003C/p>\n\u003Ch2>What if there is no RSS feed?\u003C/h2>\n\u003Cp>Unfortunately not every website offers an RSS feed. Although it might be worth it to hunt for them. Some websites offer an RSS feed but do not link to it anywhere.\nIf there is no feed, but a newsletter is offered, the service \"\u003Ca href=\"https://kill-the-newsletter.com\" rel=\"nofollow noopener noreferrer\">Kill The Newsletter\u003C/a>\" will provide you with email addresses and a corresponding RSS URL to convert any newsletter to a feed. Another service to consider is \u003Ca href=\"http://fetchrss.com\" rel=\"nofollow noopener noreferrer\">FetchRSS\u003C/a>. It turns any website into an RSS feed.\u003C/p>\n\u003Ch2>RSS Apps\u003C/h2>\n\u003Cp>If you want to have a dedicated app for your reading, you're in luck! There is a plethora of apps to choose from, all with different features and user interfaces.\nThere are three main types of apps: standalone apps, service-based apps, and self-hosted apps. Most apps are standalone, meaning they fetch the RSS feeds only when open, and don't sync to your other devices. The service-based apps rely on a cloud service which will fetch the feeds around the clock, even when all your devices are off. They can also send you a summary mail if you forget to check for some time and they can sync your subscriptions across all your devices. Unfortunately, most service-based apps only offer a limited experience for free. The last category is self-hosted apps. They are similar to the service based apps but instead of some company running the service, you have to provide a server for the service to run yourself.\u003C/p>\n\u003Cp>I use a standalone app, because I do not want to rely on a service, but I also don't want to go through the hassle of setting up a self-hosted solution.\u003C/p>\n\u003Cp>If you are still unsure what RSS app you could try out, I provided a list below. Make sure to add the \u003Ca href=\"https://tiim.ch/blog/rss.xml\" rel=\"nofollow noopener noreferrer\">RSS feed for my blog\u003C/a> (\u003Ccode>https://tiim.ch/blog/rss.xml\u003C/code>) to test it out 😉\u003C/p>\n\u003Ch3>Standalone Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://thunderbird.net\" rel=\"nofollow noopener noreferrer\">Thunderbird\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://ravenreader.app\" rel=\"nofollow noopener noreferrer\">RavenReader\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://netnewswire.com\" rel=\"nofollow noopener noreferrer\">NetNewsWire\u003C/a> (Free, Integration with Services possible)\u003C/li>\n\u003Cli>\u003Ca href=\"https://vivaldi.com\" rel=\"nofollow noopener noreferrer\">Vivaldi Browser\u003C/a> (Free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://nodetics.com/feedbro/\" rel=\"nofollow noopener noreferrer\">feedbro browser extension\u003C/a> (Free)\u003C/li>\n\u003C/ul>\n\u003Ch3>Service-Based Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://feedreader.com\" rel=\"nofollow noopener noreferrer\">FeedReader\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://feeder.co\" rel=\"nofollow noopener noreferrer\">Feeder\u003C/a> (Freemium, 10 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.inoreader.com/pricing\" rel=\"nofollow noopener noreferrer\">Inoreader\u003C/a> (Freemium, Ads and 150 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://newsblur.com\" rel=\"nofollow noopener noreferrer\">NewsBlur\u003C/a> (Freemium, 64 feeds for free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://www.feedspot.com\" rel=\"nofollow noopener noreferrer\">Feedspot\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://feedly.com\" rel=\"nofollow noopener noreferrer\">Feedly\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://feedbin.com\" rel=\"nofollow noopener noreferrer\">Feedbin\u003C/a> (Non-free)\u003C/li>\n\u003Cli>\u003Ca href=\"https://theoldreader.com\" rel=\"nofollow noopener noreferrer\">TheOldReader\u003C/a>\u003C/li>\n\u003Cli>\u003Ca href=\"https://bazqux.com\" rel=\"nofollow noopener noreferrer\">BazQux\u003C/a>\u003C/li>\n\u003C/ul>\n\u003Ch3>Self-hosted Apps\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://www.commafeed.com/\" rel=\"nofollow noopener noreferrer\">CommaFeed\u003C/a> (Free, OSS)\u003C/li>\n\u003Cli>\u003Ca href=\"https://freshrss.org\" rel=\"nofollow noopener noreferrer\">FreshRSS\u003C/a> (Free, OSS)\u003C/li>\n\u003C/ul>","blog/2022-06-use-rss","ee633c0d-d668-48e5-af57-aaae9d243099",["Date","2022-06-05T00:00:00.000Z"],["Date","2022-06-04T22:01:15.123Z"],[9],null,"You should be using RSS",true,"Decide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read.","https://i.imgur.com/t3mebu7.png",[15,16,17],"rss","dev","software","\u003Cp>I often go to social media to get news about topics that interest me. Be it web development, gardening life hacks or political news, I can follow people or topics that interest me. But instead of reading about those topics, I often get sucked into an endless hole of content that I did not sign up for. Social media companies deliberately do not want you to limit what is shown to you. It would be too easy to leave and not spend your time watching their precious ads.\u003C/p>",[16,15,17],"article","blog",[23],{"id":24,"type":25,"replyTo":26,"timestamp":27,"page":4,"url":28,"content":29,"name":30},"31ea6d40-e8c2-4ada-ac16-028a3036d2de","webmention","","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/oatm9/","Liked\nYou should be using RSS\nPost detailsDecide exactly what you want to read and escape the social media algorithms. How an old protocol called RSS can give you back the autonomy about what you read. https://i.imgur.com/t3mebu7.png","Jamie Tanna","2023-09-02T19:26:59Z",{"html":33,"slug":34,"uuid":35,"title":36,"published":11,"date":37,"description":38,"cover_image":39,"content_tags":40,"abstract":45,"tags":46,"links":-1,"type":20,"folder":21,"comments":47,"latestComment":31},"\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>\n\u003Cp>\u003Cem>TLDR\u003C/em>:\u003C/p>\n\u003Cul>\n\u003Cli>Either use Bluetooth Audio Receiver from the Microsoft Store to connect you phone via Bluetooth,\u003C/li>\n\u003Cli>Or use an audio cable to connect the phone to the \"line-in\" on your PC.\u003C/li>\n\u003C/ul>\n\u003Ch2>Bluetooth (recommended)\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>: A PC with integrated Bluetooth or a Bluetooth dongle.\u003C/p>\n\u003Cp>I recommend this approach more than the wired one because it is way less effort, you don't have to deal with a USB or lightning to audio dongle and in my opinion it is more reliable.\u003C/p>\n\u003Cp>Pair your phone with your PC as normal, by opening the Bluetooth settings on your phone and on the PC and wait for the devices to show up. When you successfully paired the phone once you will not have to do this again. Now you need an app that will tell the phone that it can use the PC as a wireless speaker. The only app I found that will do this is the \u003Ca href=\"https://www.microsoft.com/de-de/p/bluetooth-audio-receiver/9n9wclwdqs5j\" rel=\"nofollow noopener noreferrer\">Bluetooth Audio Receiver\u003C/a> app from the Windows Store. Install and and open it. You should see your phone on the list of Bluetooth devices on the app. Select it and click on the \u003Ccode>Open Connection\u003C/code> button. It might take a moment but after it connected, you should hear all sounds from your phone on your PC.\u003C/p>\n\u003Ch2>Wired\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>:\u003C/p>\n\u003Cul>\n\u003Cli>Male-to-Male audio cable (3.5mm audio jack).\u003C/li>\n\u003Cli>A line-in port on your PC (usually blue audio jack on the back)\u003C/li>\n\u003Cli>USB-C to audio jack adapter (Optional)\u003C/li>\n\u003Cli>Lighting to audio jack adapter (Optional)\u003C/li>\n\u003C/ul>\n\u003Cp>This approach works if your PC doesn't support Bluetooth, or if the Bluetooth connection drops for some reason. Connect the audio cable to the blue line-in jack on the back of the computer. Then, connect the phone to the other end of the audio cable. If your phone does not have an audio jack, use the adapter on the USB-C or Lightning port. If your PC detects that you connected a new line-in device, it might open the audio settings automatically. If not, right-click on the volume icon on the taskbar next to the clock and select \u003Ccode>Sounds\u003C/code>. Navigate to the \u003Ccode>Input\u003C/code> tab and double click on the Line-In entry (the one with a cable icon). Navigate to the Monitor tab and select the check box for \"Use this device as a playback source\". This will tell windows it should play all sounds received through this input directly to the speakers. Usually this is used to monitor microphones but it works for this use case too. You should now hear any sound from your phone through your PC headphones or speakers. Make sure you turn this checkbox off when you disconnect your phone. Otherwise you might hear a crackle or other sounds when the loose cable gets touched.\u003C/p>\n\u003Cp>\u003Cem>Photo by Lisa Fotios from Pexels\u003C/em>\u003C/p>","blog/2022-02-phone-audio-to-pc","be57f2df-d58f-4b79-8a51-e20d482f46cf","How to Listen to Phone Audio on PC",["Date","2022-02-12T00:00:00.000Z"],"Learn how to connect your phone audio to your PC over wire or Bluetooth.","/assets/2022-02-phone-audio-to-pc.jpg",[41,42,43,44,17],"how-to","audio","windows","bluetooth","\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>",[42,44,41,17,43],[],{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/sql.html b/tags/sql.html new file mode 100644 index 00000000..62c2347f --- /dev/null +++ b/tags/sql.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️sql - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: sql

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/sql/__data.json b/tags/sql/__data.json new file mode 100644 index 00000000..a7b09817 --- /dev/null +++ b/tags/sql/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":22},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"description":8,"content_tags":9,"date":13,"cover_image":14,"abstract":15,"tags":16,"links":-1,"type":18,"folder":19,"comments":20,"latestComment":21},"\u003Ch2>The problem\u003C/h2>\n\u003Cp>Let's say you have a rest API with the following endpoint that returns all of the books in your database:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-rest\">GET /book/\n\u003C/code>\u003C/pre>\n\u003Cp>Your SQL query might look like something like this\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT *\nFROM books\n\u003C/code>\u003C/pre>\n\u003Cp>Sometimes you want to only list books, for example, from a specific author. How do we do this in SQL?\u003C/p>\n\u003Ch2>Naive solution: String concatenation ✂\u003C/h2>\n\u003Cp>One way would be to concatenate your sql query something like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const arguments = [];\nconst queryString = \"SELECT * FROM books WHERE true\";\nif (authorFilter != null) {\n queryString += \"AND author = ?\";\n arguments.push(authorFilter);\n}\ndb.query(queryString, arguments);\n\u003C/code>\u003C/pre>\n\u003Cp>I'm not much of a fan of manually concatenating strings.\u003C/p>\n\u003Ch2>The coalesce function 🌟\u003C/h2>\n\u003Cp>Most Databases have the function \u003Ccode>coalesce\u003C/code> which accepts a variable amount of arguments and returns the first argument that is not null.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- Examle\nSELECT coalesce(null, null, 'tiim.ch', null, '@TiimB') as example;\n\n-- Will return\n\nexample\n---------\ntiim.ch\n\u003C/code>\u003C/pre>\n\u003Cp>But how will this function help us?\u003C/p>\n\u003Ch2>Optional filters with the coalesce function\u003C/h2>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT *\nFROM books\nWHERE\n author = coalesce(?, author);\n\u003C/code>\u003C/pre>\n\u003Cp>If the filter value is null the coalesce expression will resolve to \u003Ccode>author\u003C/code>\nand the comparison \u003Ccode>author = author\u003C/code> will be true.\u003C/p>\n\u003Cp>If on the other hand the value is set for example to Shakespeare then the author will be compared to Shakespeare.\u003C/p>\n\u003Cp>I came across this way to implement optional filters only recently. If you have a more idiomatic way to do this let me know please ✨\u003C/p>\n\u003Cp>If you liked this post please follow me on here or on Twitter under \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> 😎\u003C/p>","blog/2019-07-sql-optional-filters-coalesce","899fb73c-a78e-4cd9-b712-1886715b2d56","How to write optional filters in SQL",true,"A simple way to filter by optional values in SQL with the COALESCE function.",[10,11,12],"SQL","quick-tip","dev",["Date","2019-07-11T00:00:00.000Z"],"/assets/2019-07-sql-optional-filters-coalesce.png","\u003Cp>Let's say you have a rest API with the following endpoint that returns all of the books in your database:\u003C/p>",[12,11,17],"sql","article","blog",[],"2023-09-02T19:26:59Z",{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/sqlite.html b/tags/sqlite.html new file mode 100644 index 00000000..9d030054 --- /dev/null +++ b/tags/sqlite.html @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️sqlite - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: sqlite

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/sqlite/__data.json b/tags/sqlite/__data.json new file mode 100644 index 00000000..4b5938a0 --- /dev/null +++ b/tags/sqlite/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":29},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"cover_image":11,"content_tags":12,"links":19,"abstract":22,"tags":23,"type":24,"description":25,"folder":26,"comments":27,"latestComment":28},"\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>\n\u003Cul>\n\u003Cli>The basic commenting system\u003C/li>\n\u003Cli>Sending and receiving webmentions\u003C/li>\n\u003Cli>Micropub server implementation\u003C/li>\n\u003Cli>IndieAuth (decentralized authentication standard based on OAuth)\u003C/li>\n\u003Cli>Admin dashboard\u003C/li>\n\u003Cli>Admin backup endpoint\u003C/li>\n\u003C/ul>\n\u003Cp>Currently I am working on supporting AcitvityPub, so people can follow my blog through the fediverse, and\ncomments through the fediverse show up back on my website.\u003C/p>\n\u003Cp>The architecture of the application is inspired by the Caddy webserver, where every feature is implemented as a plugin, and the core\nof the application is only concerned with initializing those plugins.\u003C/p>\n\u003Cp>If you have any questions, or want to run IndieGo yourself, don't hesitate to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>.\u003C/p>","projects/indiego","0cf125b3-a99a-4996-8f84-ec5105d64c57","IndieGo",["Date","2023-08-02T08:39:00.000Z"],null,"Projects",true,"/assets/2022-07-first-go-project-commenting-api.png",[13,14,15,16,17,18],"go","golang","indieweb","docker","sqlite","dev",[20,21],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">IndieGo Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://comments.tiim.ch\" rel=\"nofollow noopener noreferrer\">Admin Interface\u003C/a> - authentication required\u003C/p>","\u003Cp>I blogged about creating a comment system for my website \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\">a while ago\u003C/a>,\nand later how I \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\">implemented webmentions into that same project\u003C/a>.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:\u003C/p>",[18,16,13,14,15,17],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/ssh.html b/tags/ssh.html new file mode 100644 index 00000000..7722f5e7 --- /dev/null +++ b/tags/ssh.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️ssh - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: ssh

+ +

How to set up an SSH Server on Windows with WSL

+ 3/2/2022 +
How to set up an SSH Server on Windows with WSL +

It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/ssh/__data.json b/tags/ssh/__data.json new file mode 100644 index 00000000..7f3716a7 --- /dev/null +++ b/tags/ssh/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":44},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"date":8,"description":9,"cover_image":10,"content_tags":11,"abstract":16,"tags":17,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":43},"\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\" rel=\"nofollow noopener noreferrer\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\" rel=\"nofollow noopener noreferrer\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\" rel=\"nofollow noopener noreferrer\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\" rel=\"nofollow noopener noreferrer\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>\n\u003Ch2>Installing the OpenSSH Server\u003C/h2>\n\u003Cp>Windows has been shipping with an OpenSSH client and server for a long time. They are not installed by default but can be activated either in the settings as described \u003Ca href=\"https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse\" rel=\"nofollow noopener noreferrer\">in the official docs\u003C/a> or with the following PowerShell commands.\u003C/p>\n\u003Cp>\u003Cstrong>You will need to start PowerShell as Administrator\u003C/strong>\u003C/p>\n\u003Cp>First, install the OpenSSH client and server.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0\nAdd-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0\n\u003C/code>\u003C/pre>\n\u003Cp>Enable the SSH service and make sure the firewall rule is configured:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\"># Enable the service\nStart-Service sshd\nSet-Service -Name sshd -StartupType 'Automatic'\n\n# Confirm the firewall rule is configured. It should be created automatically by setup. Run the following to verify\nif (!(Get-NetFirewallRule -Name \"OpenSSH-Server-In-TCP\" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {\n Write-Output \"Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it...\"\n New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22\n} else {\n Write-Output \"Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists.\"\n}\n\u003C/code>\u003C/pre>\n\u003Cp>Congratulations, you have installed the SSH server on your Windows machine. And all without manually setting up a background service or modifying config files.\u003C/p>\n\u003Ch2>Setting WSL as Default Shell\u003C/h2>\n\u003Cp>To directly boot into WSL when connecting, we need to change the default shell from \u003Ccode>cmd.exe\u003C/code> or \u003Ccode>PowerShell.exe\u003C/code> to \u003Ccode>bash.exe\u003C/code>, which in turn runs the default WSL distribution. This can be done with the PowerShell command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-ItemProperty -Path \"HKLM:\\SOFTWARE\\OpenSSH\" -Name DefaultShell -Value \"C:\\WINDOWS\\System32\\bash.exe\" -PropertyType String -Force\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: even though the shell is running on the Linux side, the SSH server is still on windows. This means you have to use to windows username to log in, and the SCP command copies files relative to the user directory on windows.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (non-Admin User)\u003C/h2>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: If the user account has Admin permissions, read the next chapter, otherwise continue reading.\u003C/p>\n\u003Cp>Create the folder \u003Ccode>.ssh\u003C/code> in the users home directory on windows: (e.g. \u003Ccode>C:\\Users\\<username>\\.ssh\u003C/code>). Run the following commands in PowerShell (not as administrator).\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path ~\\.ssh -ItmeType \"directory\"\nNew-Item -Path ~\\.ssh\\authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>The file \u003Ccode>.ssh\\autzorized_keys\u003C/code> will contain a list of all public keys that shall be allowed to connect to the SSH server.\u003C/p>\n\u003Cp>Copy the contents of your public key file (usually stored in \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>) to the \u003Ccode>authorized_keys\u003C/code> file. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (Admin User)\u003C/h2>\n\u003Cp>If the user is in the Administrators group, it is not possible to have the \u003Ccode>authorized_keys\u003C/code> file in the user directory for security purposes.\nInstead, it needs to be located on the following path \u003Ccode>%ProgramData%\\ssh\\administrators_authorized_keys\u003C/code>. A second requirement is that it is only accessible to Administrator users, to prevent a normal user from gaining admin permissions.\u003C/p>\n\u003Cp>To create the file start PowerShell as administrator and run the following command.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path $env:programdata\\ssh\\administrators_authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>This will create the file with the correct permissions. Now open the file and paste your public key into it. The public key should be located at \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Verifying everything works\u003C/h2>\n\u003Cp>Verify that you can SSH into your machine by running the following inside WSL:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">IP=$(cat /etc/resolv.conf | grep nameserver | cut -d \" \" -f2) # get the windows host ip address\nssh <user>@$IP\n\u003C/code>\u003C/pre>\n\u003Cp>Or from PowerShell and cmd:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ssh <user>@localhost\n\u003C/code>\u003C/pre>\n\u003Ch2>Drawbacks\u003C/h2>\n\u003Cp>There are some drawbacks to this approach. If you rely on some programs or scripts to work over SSH, this might not be the method for you. Most scripts expect a unix machine on the other end, or if they expect a windows machine they will most likely not be configured to deal with WSL.\u003C/p>\n\u003Cp>If you however just want to connect to your pc to copy some files or change some settings this approach is perfectly fine.\u003C/p>","blog/2022-03-ssh-windows-wsl","03b5a86c-5f4d-4086-9f5f-e1e46b4bcf58","How to set up an SSH Server on Windows with WSL",true,["Date","2022-03-02T00:00:00.000Z"],"It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.","/assets/2022-03-ssh-windows-wsl.png",[12,13,14,15],"SSH","WSL","Windows","dev","\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>",[15,18,19,20],"ssh","windows","wsl","article","blog",[24,32,38],{"id":25,"type":26,"replyTo":27,"timestamp":28,"page":4,"url":29,"content":30,"name":31},"20f2c526-7466-4c21-83ac-51750f278328","comment","f7c1891b-e97b-4030-863e-19344ed84d32","2022-09-20T14:59:17Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#20f2c526-7466-4c21-83ac-51750f278328","Hi Tim, no problem. I had this error in \"Event Viewer > Applications and Services Logs > OpenSSH > Admin\" and figure it out that sshd seems to search the Administrators groups to operate, literal name and not properly localized by region.\n\nerroid:2 user:SYSTEM details:\"sshd: error: unable to resolve group administrators\"\n\nMaybe is not all the non-english windows with this problem, but I have it but after created the group works like a charm.","",{"id":27,"type":26,"replyTo":33,"timestamp":34,"page":4,"url":35,"content":36,"name":37},"5de01bc5-b7c7-4522-9dfe-c67f103d4c03","2022-09-20T12:58:29Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#f7c1891b-e97b-4030-863e-19344ed84d32","Hi FinderX\n\nThanks for the heads up. I don't remember having to create a new user group, even though my system language is German. Maybe I just forgot about that though.","Tim",{"id":33,"type":26,"replyTo":31,"timestamp":39,"page":4,"url":40,"content":41,"name":42},"2022-09-20T07:50:46Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#5de01bc5-b7c7-4522-9dfe-c67f103d4c03","Hi!\nI add some roundabouts about admin-users, if your windows ssh server system language is NOT english, you must create 'Administrators' group (without quotes) in your language equivalent of \"Users and Local Groups > Groups\", if your server is a DC (Domain Controller) create it in your language equivalent of \"Active Directories Users and Computers\".\n\nCreate the user group with name Administrators, description whatever, ex. \"Dummy group for sshd to work correctly.\", and in Members add your language equivalent of the user Administrator.\n\nThis is optional but I suggest you change these settings in \"%programdata%\\ssh\\sshd_config\" after you successfully copy your public key to the ssh server :\n\nStrictModes yes\nPubkeyAuthentication yes\nPasswordAuthentication no\n\nYou can see the log activity in your language equivalent of \"Applications and Services Logs > OpenSSH > Admin or Operational\"\n\nBest Regards.","FinderX","2023-09-02T19:26:59Z",{"tag":18}],"uses":{"params":["slug"]}}]} diff --git a/tags/ssr.html b/tags/ssr.html new file mode 100644 index 00000000..32a060a8 --- /dev/null +++ b/tags/ssr.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️ssr - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: ssr

+ +

SvelteKit Server-Side Rendering (SSR) with @urql/svelte

+ 9/26/2022 +
SvelteKit Server-Side Rendering (SSR) with @urql/svelte +

Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/ssr/__data.json b/tags/ssr/__data.json new file mode 100644 index 00000000..20fd4579 --- /dev/null +++ b/tags/ssr/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":34},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":33},"\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\" rel=\"nofollow noopener noreferrer\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>\n\u003Cp>Server-side rendering (SSR) is one of the great features of SvelteKit. I will try to keep this blog post short and will therefore not explain what server-side rendering is and why you should take advantage of it \u003Cem>(you really should!)\u003C/em>. If you want to know more about SSR you can take a look at this article: \u003Ca href=\"https://towardsdev.com/server-side-rendering-srr-in-javascript-a1b7298f0d04\" rel=\"nofollow noopener noreferrer\">A Deep Dive into Server-Side Rendering (SSR) in JavaScript\u003C/a>.\u003C/p>\n\u003Ch2>Background - SSR in SvelteKit\u003C/h2>\n\u003Cp>SvelteKit implements SSR by providing a \u003Ca href=\"https://kit.svelte.dev/docs/load\" rel=\"nofollow noopener noreferrer\">\u003Ccode>load\u003C/code> function\u003C/a> for every layout and page component. If a page or layout needs to perform some asynchronous operation, this should be done inside of this load function. SvelteKit executes this function asynchronously on the server side as well as on the client side and the return value of this function is assigned to the \u003Ccode>data\u003C/code> prop of the associated component. Usually, this asynchronous operation is loading data from an external service, like in the case of this blog post a GraphQL server.\nYou can of course load data directly in the component, but SvelteKit will not wait for this to complete when doing SSR, and the resulting HTML will not include the loaded data.\u003C/p>\n\u003Ch2>Background - @urql/svelte\u003C/h2>\n\u003Cp>The urql library allows us to easily issue GraphQL queries and mutations. Some of the functionality it has to make our lives easier include:\u003C/p>\n\u003Cul>\n\u003Cli>Reloading a query when a query variable changes\u003C/li>\n\u003Cli>Reloading a query after a mutation that touches the same data as the query\u003C/li>\n\u003C/ul>\n\u003Cp>We want to keep these features, even when using urql when doing SSR.\u003C/p>\n\u003Ch2>The Problem\u003C/h2>\n\u003Cp>When implementing SSR in my project, I ran into two problems. I couldn't find any documentation or any articles solving them, so I decided to write down my solutions to those problems in this blog post.\u003C/p>\n\u003Ch3>Problem 1 - Svelte and urql Reactivity\u003C/h3>\n\u003Cp>Let's say we have the following load function, which executes a GraphQL query to load a list of red cars:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\n\n/** @type {import('./$types').PageLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const carColor = \"red\";\n\n const cars = client\n .query(carsQuery, {\n color: carColor,\n })\n .toPromise()\n .then((c) => c.data?.car);\n\n return {\n cars,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>This example uses the urql method \u003Ccode>client.query\u003C/code> to start a query to get us a list of cars with a red colour (The GraphQL query is not shown but the exact query is not important for this example).\nThe client gets a \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">special fetch function\u003C/a> from the event which has a few nice properties, like preventing a second network request on the client side if that same request was just issued on the server-side.\u003C/p>\n\u003Cp>Since the query code is now located in the load function and not in a svelte component, there is no way to easily change the \u003Ccode>carColor\u003C/code> and have urql automatically reload the query. The only way to change the variable is to set the value as a query parameter and read that from the \u003Ccode>event\u003C/code> argument. This however means that we have to refresh the whole page just to reload this query.\u003C/p>\n\u003Cp>The other thing urql does for us, reloading the query when we do a mutation on the same data, will not work with the above code either.\u003C/p>\n\u003Ch3>The solution: A query in the load function and a query in the component\u003C/h3>\n\u003Cp>To fix those two drawbacks we have to add the same query as in the load function to our component code as well. Unfortunately, this means when a user loads the page, it sends a request from the client side, even though the same request got sent from the server side already.\u003C/p>\n\u003Cp>I created a small wrapper function \u003Ccode>queryStoreInitialData\u003C/code> that creates the query inside of the component and intelligently switches from the (possibly stale) data from the load function to the new data. Using this wrapper, the page or layout might look as follows:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-svelte\"><script>\n import { queryStoreInitialData } from \"@/lib/gql-client\"; // The helper function mentioned above\n import { getContextClient } from \"@urql/svelte\";\n import { carsQuery } from \"./query\"; // The query\n\n export let data;\n\n $: gqlStore = queryStoreInitialData(\n {\n client: getContextClient(),\n query: carsQuery,\n },\n data.cars\n );\n $: cars = $gqlStore?.data?.car;\n</script>\n\n<div>\n <pre>\n {JSON.stringify(cars, null, 2)}\n </pre>\n</div>\n\u003C/code>\u003C/pre>\n\u003Col>\n\u003Cli>The native \u003Ccode>queryStore\u003C/code> function gets replaced with the wrapper function.\u003C/li>\n\u003Cli>The initial value of the query is supplied to the wrapper\u003C/li>\n\u003C/ol>\n\u003Cp>Unfortunately, we can not return the query result from the load function directly like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const result = await client.query(cars, {}).toPromise();\n\nreturn {\n cars: toInitialValue(result),\n};\n\u003C/code>\u003C/pre>\n\u003Cp>This results in the following error:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-stacktrace\">Cannot stringify a function (data.events.operation.context.fetch)\nError: Cannot stringify a function (data.events.operation.context.fetch)\n at render_response (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/render.js:181:20)\n at runMicrotasks (<anonymous>)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async render_page (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/index.js:276:10)\n at async resolve (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:232:17)\n at async respond (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:284:20)\n at async file:///app/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:406:22\n\u003C/code>\u003C/pre>\n\u003Cp>This is because the query result contains data that is not serializable.\nTo fix this I created the \u003Ccode>toInitialValue\u003C/code> function, which deletes all non-serializable elements from the result. The load function now looks like follows;\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\nimport { createServerClient, toInitialValue } from \"@/lib/gql-client\";\nimport { parse } from \"cookie\";\nimport { carsQuery } from \"./query\";\n\n/** @type {import('./$types').PageServerLoad} */\nexport const load = async (event) => {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const result = await client.query(cars, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n};\n\u003C/code>\u003C/pre>\n\u003Ch3>Problem 2 - Authentication\u003C/h3>\n\u003Cp>We will look at the same \u003Ccode>load\u003C/code> function as #Problem 1 - Svelte and urql Reactivity: the function creates a urql client with the fetch function from the event object and uses this client to send a query.\u003C/p>\n\u003Cp>Sometimes however the GraphQL API requires authentication in the form of a cookie to allow access.\u003C/p>\n\u003Cp>Unfortunately, the \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">fetch function that we get from the load event\u003C/a> will only pass the cookies on if the requested domain is the same as the base domain or a more specific subdomain of it. This means if your SvelteKit site runs on \u003Ccode>example.com\u003C/code> and your GraphQL server runs on \u003Ccode>gql.example.com\u003C/code> then the cookies will get forwarded and everything is fine. This however is, in my experience, often not the case. Either you might use an external service for your GraphQL API or you host it yourself and want to use its internal domain.\u003C/p>\n\u003Cp>The only way to pass the cookies on to the GraphQL server, in this case, is by manually setting the cookie header when creating the urql client. This however forces us to use the server-only load function, as we do not have access to the cookie header in the normal load function.\u003C/p>\n\u003Cp>The new code now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch,\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // inject the cookie header\n // FIXME: change the cookie name\n Cookie: `gql-session=${event.cookies.get(\"gql-session\")}`,\n },\n },\n });\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>To keep the size of the load functions across my codebase smaller I created a small wrapper function \u003Ccode>createServerClient\u003C/code>:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createServerClient(event.cookies);\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Ch2>The Code\u003C/h2>\n\u003Cp>Below you can find the three functions \u003Ccode>createServerClient\u003C/code>, \u003Ccode>queryStoreInitialData\u003C/code> and \u003Ccode>toInitialValue\u003C/code> that we used above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/lib/gql-client.js\n\nimport { browser } from \"$app/environment\";\nimport { urls } from \"@/config\";\nimport { createClient, queryStore } from \"@urql/svelte\";\nimport { derived, readable } from \"svelte/store\";\n\n/**\n * Helper function to create an urql client for a server-side-only load function\n *\n *\n * @param {import('@sveltejs/kit').Cookies} cookies\n * @returns\n */\nexport function createServerClient(cookies) {\n return createClient({\n // FIXME: adjust your graphql url\n url: urls.gql,\n fetch,\n // FIXME: if you don't need to authenticate, delete the following object:\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // FIXME: if you want to set a cookie adjust the cookie name\n Cookie: `gql-session=${cookies.get(\"gql-session\")}`,\n },\n },\n });\n}\n\n/**\n * Helper method to send a GraphQL query but use the data from the SvelteKit load function initially.\n *\n *\n * @param {any} queryArgs\n * @param {any} initialValue\n * @returns\n */\nexport function queryStoreInitialData(queryArgs, initialValue) {\n if (!initialValue || (!initialValue.error && !initialValue.data)) {\n throw new Error(\"No initial value from server\");\n }\n\n let query = readable({ fetching: true });\n if (browser) {\n query = queryStore(queryArgs);\n }\n\n return derived(query, (value, set) => {\n if (value.fetching) {\n set({ ...initialValue, source: \"server\", fetching: true });\n } else {\n set({ ...value, source: \"client\" });\n }\n });\n}\n\n/**\n * Make the result object of a urql query serialisable.\n *\n *\n * @template T\n * @param {Promise<import('@urql/svelte').OperationResult<T, any >>|import('@urql/svelte').OperationResult<T, any >} result\n * @returns {Promise<{fetching:false, error: undefined | {name?: string, message?: string; graphQLErrors?: any[]; networkError?: Error; response?: any;}, data: T|undefined}>}\n */\nexport async function toInitialValue(result) {\n const { error, data } = await result;\n\n // required to turn class array into array of javascript objects\n const errorObject = error ? {} : undefined;\n if (errorObject) {\n console.warn(error);\n errorObject.graphQLErrors = error?.graphQLErrors?.map((e) => ({ ...e }));\n errorObject.networkError = { ...error?.networkError };\n errorObject.response = { value: \"response omitted\" };\n }\n\n return {\n fetching: false,\n error: { ...error, ...errorObject },\n data,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Ca href=\"https://gist.github.com/Tiim/1adeb4d74ce7ae09d0d0aa4176a6195d\" rel=\"nofollow noopener noreferrer\">Link to the Gist\u003C/a>\u003C/p>\n\u003Ch2>End remarks\u003C/h2>\n\u003Cp>Even though I think this solution is not too bad, I wish @urql/svelte would implement a better way to handle SSR with sveltekit. I posted a \u003Ca href=\"https://github.com/FormidableLabs/urql/discussions/2703\" rel=\"nofollow noopener noreferrer\">question on the urql GitHub discussions board\u003C/a>, but I have not gotten any response yet.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>This article was written with \u003Ccode>@svelte/kit\u003C/code> version \u003Ccode>1.0.0-next.499\u003C/code> and \u003Ccode>@urql/svelte\u003C/code> version \u003Ccode>3.0.1\u003C/code>.\nI will try to update this article as I update my codebase to newer versions.\u003C/p>\n\u003C/blockquote>\n\u003Cp>If this post helped you, or you found a better or different way to solve SSR with urql, please let me know in the comments, write me an email or tag me on twitter \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a>.\u003C/p>","blog/2022-09-27-sveltekit-ssr-with-urql","1e223cab-bca2-4b3b-a75a-71f158c90cba",["Date","2022-09-26T00:00:00.000Z"],["Date","2022-09-26T08:55:23.886Z"],[9],null,"SvelteKit Server-Side Rendering (SSR) with @urql/svelte",true,"Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.","https://i.imgur.com/5DBIbbT.png",[15,16,17,18],"urql","sveltekit","SSR","graphql","\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>",[18,21,16,15],"ssr","article","blog",[25],{"id":26,"type":27,"replyTo":28,"timestamp":29,"page":4,"url":30,"content":31,"name":32},"a38babef-2c4c-41e6-a748-8723b6cc34ef","comment","","2023-02-05T00:02:51Z","https://tiim.ch/blog/2022-09-27-sveltekit-ssr-with-urql#a38babef-2c4c-41e6-a748-8723b6cc34ef","Hi\n\ninspiring article. Anyway, inspired with it I tried to find more seamless integration - get SSR rendered queries but keep original interface. \n\nYou may be interested in my approach, it's dropped in discussion you open \nhttps://github.com/urql-graphql/urql/discussions/2703","Farin","2023-09-02T19:26:59Z",{"tag":21}],"uses":{"params":["slug"]}}]} diff --git a/tags/storj.html b/tags/storj.html new file mode 100644 index 00000000..a5195001 --- /dev/null +++ b/tags/storj.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️storj - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: storj

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/storj/__data.json b/tags/storj/__data.json new file mode 100644 index 00000000..67623507 --- /dev/null +++ b/tags/storj/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":41},[2],{"html":3,"slug":4,"uuid":5,"date":6,"aliases":7,"title":9,"published":10,"modified":8,"description":11,"cover_image":12,"cover_caption":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":25,"folder":26,"comments":27,"latestComment":40},"\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\" rel=\"nofollow noopener noreferrer\">TOS\u003C/a>:\u003C/p>\n\u003Cblockquote>\n\u003Cp>[...] Also, don't use Imgur to host image libraries you link to from elsewhere, content for your website, advertising, avatars, or anything else that turns us into your content delivery network.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Finally when I started \u003Ca href=\"https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1\" rel=\"nofollow noopener noreferrer\">indie-webifying my website\u003C/a>, and was implementing the micropub protocol (which I will blog about at a later time), I decided that it was at the time to host the images on a platform that was meant to do that. I looked at a few storage providers such as cloudinary and S3 based object storage and landed on \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">Storj.io\u003C/a>, mostly because of the generous free tier, which should suffice for this little blog for quite a while.\u003C/p>\n\u003Cp>One thing that bothered me slightly was that all storage providers I looked at charge for traffic. It's not the fact that it's an additional expense (if your not in the free tier anymore) that bothers me, but the fact that I don't have any control over how much this will cost me. In all likelihood this will never cost me anything since this blog has not much traffic, but if a post were to go viral (one can dream...), this could result in a surprise bill at the end of the month.\u003C/p>\n\u003Cp>To help with the traffic costs I decided to try to use the free CDN functionality of Cloudflare to reduce the traffic to Storj. In this blog post I will describe how I did that.\u003C/p>\n\u003Ch2>Is this the right solution for you?\u003C/h2>\n\u003Cp>If you are in a similar situation as me, and just want to have somewhere to host your images for a personal website or to share images or screenshots as links while still having control over all your data, this could be a good solution.\u003C/p>\n\u003Cp>If you want to build a robust image pipeline with resizing and image optimization, or you are building an enterprise website this is probably not the right way. You should take a look at cloudinary or one of the big cloud providers.\u003C/p>\n\u003Ch2>Prerequisites\u003C/h2>\n\u003Cp>To use Cloudflare as a CDN, you need to have Cloudflare setup as your DNS host for the domain you want to serve the images from. Even if you just want to use a subdomain like \u003Ccode>media.example.com\u003C/code>, the whole \u003Ccode>example.com\u003C/code> domain needs to be on cloudflare. For me this was not much of an issue, I followed the instructions from cloudflare and pointed the nameserver of my domain to cloudflare. Although I did have an issue during the migration, which resulted in my website being down for two hours. But I'm pretty sure this was caused by my previous nameserver provider.\u003C/p>\n\u003Ch2>Setting up Storj & Cloudflare\u003C/h2>\n\u003Cp>I assume you already have an account at \u003Ca href=\"https://storj.io/\" rel=\"nofollow noopener noreferrer\">storj.io\u003C/a>. The next step is creating a bucket for your images. A bucket is just a place for your files and folders to live in storj, just like in any other S3 compatible storage provider. (Actually there are no folders in storj and other S3 services, the folders are just prefixes of the filenames). When creating a bucket, make sure you save the passphrase securely, such as in your password manager. Whenever storj asks you for the passphrase, make sure you don't let storj generate a new one! Every new passphrase will create access to a new bucket.\u003C/p>\n\u003Cp>The next step is \u003Ca href=\"https://docs.storj.io/dcs/downloads/download-uplink-cli\" rel=\"nofollow noopener noreferrer\">installing the uplink cli\u003C/a>. Follow the quick start tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object\" rel=\"nofollow noopener noreferrer\">get an access grant\u003C/a>. Remember to use the same passphrase from above. Now follow the next quickstart tutorial to \u003Ca href=\"https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/uploading-your-first-object/set-up-uplink-cli\" rel=\"nofollow noopener noreferrer\">add the bucket to the uplink cli\u003C/a>. The file \u003Ccode>accessgrant.txt\u003C/code> in the tutorial only contains the access-grant string that you got from the last step.\u003C/p>\n\u003Cp>Finally we want to share the bucket so the images can be accessed from the web. For this you can run the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">uplink share --dns <domain> sj://<bucket>/<prefix> --not-after=none\n\u003C/code>\u003C/pre>\n\u003Cp>Replace \u003Ccode><domain>\u003C/code> with the domain you want to serve the images from. In my case I use \u003Ccode>media.tiim.ch\u003C/code>. Then replace \u003Ccode><bucket>\u003C/code> with the name of your bucket and \u003Ccode><prefix>\u003C/code> with the prefix.\u003C/p>\n\u003Cp>As mentioned above, you can think of a prefix as a folder. If you use for example \u003Ccode>media-site1\u003C/code> as a prefix, then every file in the \"folder\" \u003Ccode>media-site1\u003C/code> will be shared. This means you can use multiple prefixes to serve files for multiple websites in the same bucket.\u003C/p>\n\u003Cp>You will get the following output:\u003C/p>\n\u003Cpre>\u003Ccode>[...]\n=========== DNS INFO =====================================================================\nRemember to update the $ORIGIN with your domain name. You may also change the $TTL.\n$ORIGIN example.com.\n$TTL 3600\nmedia.example.com IN CNAME link.storjshare.io.\ntxt-media.example.com IN TXT storj-root:mybucket/myprefix\ntxt-media.example.com IN TXT storj-access:totallyrandomstringofnonsens\n\u003C/code>\u003C/pre>\n\u003Cp>Create the DNS entries in Cloudflare with the values printed in the last three lines. Make sure you enable the proxy setting when entering the CNAME entry to enable Cloudflares CDN service.\u003C/p>\n\u003Cp>And that's it. All files you put in the bucket with the correct prefix are now available under your domain! :)\u003C/p>\n\u003Cp>If this blog post helped you, or you have some issues or thoughts on this, leave a comment via the comment box below or via webmention.\u003C/p>","blog/2022-12-storj-cloudflare-image-hosting","6d5a964d-328e-43d7-9189-40280b012074",["Date","2022-12-03T13:37:33.000Z"],[8],null,"Hosting Images with Storj and Cloudflare",true,"Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.","https://media.tiim.ch/d280fad4-632a-4b5a-b6b2-6a5c0026b61c.jpg","Image generated by Dall-E: travel postcards scattered on grass, top down view, photoreal",[15,16,17,18],"CDN","IndieWeb","Cloudflare","Storj","\u003Cp>For a while now I have been looking for a way to put images on my website. At first I just embedded them in the website github repository, but this just doesn't feel right. Putting one or two image assets in a codebase is one thing, putting an ever growing list of images in there feels icky to me. For this reason I put the last few cover images of my blog posts on the imgur platform. This is slightly cleaner from a git standpoint but now i have to trust imgur to keep serving these images. Additionally, as I recently discovered, this seems to be against imgurs \u003Ca href=\"https://imgur.com/tos\">TOS\u003C/a>:\u003C/p>",[21,22,23,24],"cdn","cloudflare","indieweb","storj","article","blog",[28,35],{"id":29,"type":30,"replyTo":31,"timestamp":32,"page":4,"url":33,"content":31,"name":34},"edeb5ddb-357a-41cf-b2b2-704df571d70c","webmention","","2023-01-11T06:03:38Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/1570290779419017216","Laura Forster",{"id":36,"type":30,"replyTo":31,"timestamp":37,"page":4,"url":38,"content":31,"name":39},"2331980c-acee-4549-a260-61b4119c63a9","2022-12-05T06:12:20Z","https://brid.gy/like/twitter/tiimb/1599552042087120896/275384865","kevin","2023-09-02T19:26:59Z",{"tag":24}],"uses":{"params":["slug"]}}]} diff --git a/tags/svelte.html b/tags/svelte.html new file mode 100644 index 00000000..4b5d1204 --- /dev/null +++ b/tags/svelte.html @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️svelte - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: svelte

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/svelte/__data.json b/tags/svelte/__data.json new file mode 100644 index 00000000..ee71cc8c --- /dev/null +++ b/tags/svelte/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":55},[2,25,39],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":15,"abstract":18,"tags":19,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.\u003C/p>\n\u003Cp>I built this little page to help me count laps for my swimmers for some time based test sets. The first version of the app just had a grid of static buttons, one for each athlete. I quickly found that it is very hard to keep track which laps I already counted. As a way to visualise when each button was last pressed, they change colour. The buttons start green when pressed and slowly turn red over time, based on the average duration of a lap.\u003C/p>\n\u003Cp>The page works completely offline (after the page loads) and no data is sent to any server. It is also possible to export the data to a csv file.\u003C/p>","projects/lap-counter","871ebd58-0543-4d3a-9f3d-16cd85da9bf9","Lap Counter",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14],"svelte","swim","dev",[16,17],"\u003Cp>\u003Ca href=\"https://tiim.ch/lap-counter-js/\" rel=\"nofollow noopener noreferrer\">Open Lap Counter\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/laps-counter-8f0sb\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.\u003C/p>",[14,12,13],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"title":29,"date":30,"modified":31,"section":9,"published":10,"content_tags":32,"links":34,"abstract":36,"tags":37,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":38,"latestComment":24},"\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\" rel=\"nofollow noopener noreferrer\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/lenex-splits-sheet-creator.png\" alt=\"Screenshot of a split sheet\">\u003C/p>\n\u003Cp>The split sheet creator is a quick and easy way to create a split sheet from your Lenex sign-up file.\nThe split sheet creator does not send your Lenex file to any servers. The file is opened directly in your browser and never leaves your computer!\u003C/p>\n\u003Cp>I built this web app using my \u003Ca href=\"https://www.npmjs.com/package/js-lenex\" rel=\"nofollow noopener noreferrer\">lenex javascript library\u003C/a>.\u003C/p>\n\u003Ch2>What is a split sheet?\u003C/h2>\n\u003Cp>Swim coaches often write down the times of athletes after some fractions of a race (splits). Usually those splits are recorded every lap or every second lap.\u003C/p>","projects/lenex-split-sheet","a03b6eef-ffbe-428d-a559-42bd361ab88c","Lenex Splits Sheet Creator",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[13,33,12,14],"lenex",[35],"\u003Cp>\u003Ca href=\"https://tiim.ch/lenex-splits-sheet-creator/\" rel=\"nofollow noopener noreferrer\">Open Splits Sheet Creator\u003C/a>\u003C/p>","\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>",[14,33,12,13],[],{"html":40,"slug":41,"uuid":42,"title":43,"date":44,"modified":45,"section":9,"published":10,"content_tags":46,"links":49,"abstract":52,"tags":53,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":54,"latestComment":24},"\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\" rel=\"nofollow noopener noreferrer\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>","projects/woocommerce-order-explorer","8a52a1ad-a5cf-4191-947c-db6862746816","WooCommerce Order Explorer",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[12,47,48,14],"wordpress","woocommerce",[50,51],"\u003Cp>\u003Ca href=\"https://tiim.ch/woocommerce-order-explorer-js/\" rel=\"nofollow noopener noreferrer\">Open Order Explorer\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/woo-commerce-order-explorer-js-vmu3h\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>",[14,12,48,47],[],{"tag":12}],"uses":{"params":["slug"]}}]} diff --git a/tags/sveltekit.html b/tags/sveltekit.html new file mode 100644 index 00000000..f38f5e43 --- /dev/null +++ b/tags/sveltekit.html @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️sveltekit - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: sveltekit

+ +

SvelteKit Server-Side Rendering (SSR) with @urql/svelte

+ 9/26/2022 +
SvelteKit Server-Side Rendering (SSR) with @urql/svelte +

Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.

+
+
+ +

TeamKit

+ 11/27/2022 +
+

TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/sveltekit/__data.json b/tags/sveltekit/__data.json new file mode 100644 index 00000000..9b102ec6 --- /dev/null +++ b/tags/sveltekit/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":66},[2,34,52],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":33},"\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\" rel=\"nofollow noopener noreferrer\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>\n\u003Cp>Server-side rendering (SSR) is one of the great features of SvelteKit. I will try to keep this blog post short and will therefore not explain what server-side rendering is and why you should take advantage of it \u003Cem>(you really should!)\u003C/em>. If you want to know more about SSR you can take a look at this article: \u003Ca href=\"https://towardsdev.com/server-side-rendering-srr-in-javascript-a1b7298f0d04\" rel=\"nofollow noopener noreferrer\">A Deep Dive into Server-Side Rendering (SSR) in JavaScript\u003C/a>.\u003C/p>\n\u003Ch2>Background - SSR in SvelteKit\u003C/h2>\n\u003Cp>SvelteKit implements SSR by providing a \u003Ca href=\"https://kit.svelte.dev/docs/load\" rel=\"nofollow noopener noreferrer\">\u003Ccode>load\u003C/code> function\u003C/a> for every layout and page component. If a page or layout needs to perform some asynchronous operation, this should be done inside of this load function. SvelteKit executes this function asynchronously on the server side as well as on the client side and the return value of this function is assigned to the \u003Ccode>data\u003C/code> prop of the associated component. Usually, this asynchronous operation is loading data from an external service, like in the case of this blog post a GraphQL server.\nYou can of course load data directly in the component, but SvelteKit will not wait for this to complete when doing SSR, and the resulting HTML will not include the loaded data.\u003C/p>\n\u003Ch2>Background - @urql/svelte\u003C/h2>\n\u003Cp>The urql library allows us to easily issue GraphQL queries and mutations. Some of the functionality it has to make our lives easier include:\u003C/p>\n\u003Cul>\n\u003Cli>Reloading a query when a query variable changes\u003C/li>\n\u003Cli>Reloading a query after a mutation that touches the same data as the query\u003C/li>\n\u003C/ul>\n\u003Cp>We want to keep these features, even when using urql when doing SSR.\u003C/p>\n\u003Ch2>The Problem\u003C/h2>\n\u003Cp>When implementing SSR in my project, I ran into two problems. I couldn't find any documentation or any articles solving them, so I decided to write down my solutions to those problems in this blog post.\u003C/p>\n\u003Ch3>Problem 1 - Svelte and urql Reactivity\u003C/h3>\n\u003Cp>Let's say we have the following load function, which executes a GraphQL query to load a list of red cars:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\n\n/** @type {import('./$types').PageLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const carColor = \"red\";\n\n const cars = client\n .query(carsQuery, {\n color: carColor,\n })\n .toPromise()\n .then((c) => c.data?.car);\n\n return {\n cars,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>This example uses the urql method \u003Ccode>client.query\u003C/code> to start a query to get us a list of cars with a red colour (The GraphQL query is not shown but the exact query is not important for this example).\nThe client gets a \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">special fetch function\u003C/a> from the event which has a few nice properties, like preventing a second network request on the client side if that same request was just issued on the server-side.\u003C/p>\n\u003Cp>Since the query code is now located in the load function and not in a svelte component, there is no way to easily change the \u003Ccode>carColor\u003C/code> and have urql automatically reload the query. The only way to change the variable is to set the value as a query parameter and read that from the \u003Ccode>event\u003C/code> argument. This however means that we have to refresh the whole page just to reload this query.\u003C/p>\n\u003Cp>The other thing urql does for us, reloading the query when we do a mutation on the same data, will not work with the above code either.\u003C/p>\n\u003Ch3>The solution: A query in the load function and a query in the component\u003C/h3>\n\u003Cp>To fix those two drawbacks we have to add the same query as in the load function to our component code as well. Unfortunately, this means when a user loads the page, it sends a request from the client side, even though the same request got sent from the server side already.\u003C/p>\n\u003Cp>I created a small wrapper function \u003Ccode>queryStoreInitialData\u003C/code> that creates the query inside of the component and intelligently switches from the (possibly stale) data from the load function to the new data. Using this wrapper, the page or layout might look as follows:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-svelte\"><script>\n import { queryStoreInitialData } from \"@/lib/gql-client\"; // The helper function mentioned above\n import { getContextClient } from \"@urql/svelte\";\n import { carsQuery } from \"./query\"; // The query\n\n export let data;\n\n $: gqlStore = queryStoreInitialData(\n {\n client: getContextClient(),\n query: carsQuery,\n },\n data.cars\n );\n $: cars = $gqlStore?.data?.car;\n</script>\n\n<div>\n <pre>\n {JSON.stringify(cars, null, 2)}\n </pre>\n</div>\n\u003C/code>\u003C/pre>\n\u003Col>\n\u003Cli>The native \u003Ccode>queryStore\u003C/code> function gets replaced with the wrapper function.\u003C/li>\n\u003Cli>The initial value of the query is supplied to the wrapper\u003C/li>\n\u003C/ol>\n\u003Cp>Unfortunately, we can not return the query result from the load function directly like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const result = await client.query(cars, {}).toPromise();\n\nreturn {\n cars: toInitialValue(result),\n};\n\u003C/code>\u003C/pre>\n\u003Cp>This results in the following error:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-stacktrace\">Cannot stringify a function (data.events.operation.context.fetch)\nError: Cannot stringify a function (data.events.operation.context.fetch)\n at render_response (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/render.js:181:20)\n at runMicrotasks (<anonymous>)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async render_page (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/index.js:276:10)\n at async resolve (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:232:17)\n at async respond (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:284:20)\n at async file:///app/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:406:22\n\u003C/code>\u003C/pre>\n\u003Cp>This is because the query result contains data that is not serializable.\nTo fix this I created the \u003Ccode>toInitialValue\u003C/code> function, which deletes all non-serializable elements from the result. The load function now looks like follows;\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\nimport { createServerClient, toInitialValue } from \"@/lib/gql-client\";\nimport { parse } from \"cookie\";\nimport { carsQuery } from \"./query\";\n\n/** @type {import('./$types').PageServerLoad} */\nexport const load = async (event) => {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const result = await client.query(cars, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n};\n\u003C/code>\u003C/pre>\n\u003Ch3>Problem 2 - Authentication\u003C/h3>\n\u003Cp>We will look at the same \u003Ccode>load\u003C/code> function as #Problem 1 - Svelte and urql Reactivity: the function creates a urql client with the fetch function from the event object and uses this client to send a query.\u003C/p>\n\u003Cp>Sometimes however the GraphQL API requires authentication in the form of a cookie to allow access.\u003C/p>\n\u003Cp>Unfortunately, the \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">fetch function that we get from the load event\u003C/a> will only pass the cookies on if the requested domain is the same as the base domain or a more specific subdomain of it. This means if your SvelteKit site runs on \u003Ccode>example.com\u003C/code> and your GraphQL server runs on \u003Ccode>gql.example.com\u003C/code> then the cookies will get forwarded and everything is fine. This however is, in my experience, often not the case. Either you might use an external service for your GraphQL API or you host it yourself and want to use its internal domain.\u003C/p>\n\u003Cp>The only way to pass the cookies on to the GraphQL server, in this case, is by manually setting the cookie header when creating the urql client. This however forces us to use the server-only load function, as we do not have access to the cookie header in the normal load function.\u003C/p>\n\u003Cp>The new code now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch,\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // inject the cookie header\n // FIXME: change the cookie name\n Cookie: `gql-session=${event.cookies.get(\"gql-session\")}`,\n },\n },\n });\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>To keep the size of the load functions across my codebase smaller I created a small wrapper function \u003Ccode>createServerClient\u003C/code>:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createServerClient(event.cookies);\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Ch2>The Code\u003C/h2>\n\u003Cp>Below you can find the three functions \u003Ccode>createServerClient\u003C/code>, \u003Ccode>queryStoreInitialData\u003C/code> and \u003Ccode>toInitialValue\u003C/code> that we used above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/lib/gql-client.js\n\nimport { browser } from \"$app/environment\";\nimport { urls } from \"@/config\";\nimport { createClient, queryStore } from \"@urql/svelte\";\nimport { derived, readable } from \"svelte/store\";\n\n/**\n * Helper function to create an urql client for a server-side-only load function\n *\n *\n * @param {import('@sveltejs/kit').Cookies} cookies\n * @returns\n */\nexport function createServerClient(cookies) {\n return createClient({\n // FIXME: adjust your graphql url\n url: urls.gql,\n fetch,\n // FIXME: if you don't need to authenticate, delete the following object:\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // FIXME: if you want to set a cookie adjust the cookie name\n Cookie: `gql-session=${cookies.get(\"gql-session\")}`,\n },\n },\n });\n}\n\n/**\n * Helper method to send a GraphQL query but use the data from the SvelteKit load function initially.\n *\n *\n * @param {any} queryArgs\n * @param {any} initialValue\n * @returns\n */\nexport function queryStoreInitialData(queryArgs, initialValue) {\n if (!initialValue || (!initialValue.error && !initialValue.data)) {\n throw new Error(\"No initial value from server\");\n }\n\n let query = readable({ fetching: true });\n if (browser) {\n query = queryStore(queryArgs);\n }\n\n return derived(query, (value, set) => {\n if (value.fetching) {\n set({ ...initialValue, source: \"server\", fetching: true });\n } else {\n set({ ...value, source: \"client\" });\n }\n });\n}\n\n/**\n * Make the result object of a urql query serialisable.\n *\n *\n * @template T\n * @param {Promise<import('@urql/svelte').OperationResult<T, any >>|import('@urql/svelte').OperationResult<T, any >} result\n * @returns {Promise<{fetching:false, error: undefined | {name?: string, message?: string; graphQLErrors?: any[]; networkError?: Error; response?: any;}, data: T|undefined}>}\n */\nexport async function toInitialValue(result) {\n const { error, data } = await result;\n\n // required to turn class array into array of javascript objects\n const errorObject = error ? {} : undefined;\n if (errorObject) {\n console.warn(error);\n errorObject.graphQLErrors = error?.graphQLErrors?.map((e) => ({ ...e }));\n errorObject.networkError = { ...error?.networkError };\n errorObject.response = { value: \"response omitted\" };\n }\n\n return {\n fetching: false,\n error: { ...error, ...errorObject },\n data,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Ca href=\"https://gist.github.com/Tiim/1adeb4d74ce7ae09d0d0aa4176a6195d\" rel=\"nofollow noopener noreferrer\">Link to the Gist\u003C/a>\u003C/p>\n\u003Ch2>End remarks\u003C/h2>\n\u003Cp>Even though I think this solution is not too bad, I wish @urql/svelte would implement a better way to handle SSR with sveltekit. I posted a \u003Ca href=\"https://github.com/FormidableLabs/urql/discussions/2703\" rel=\"nofollow noopener noreferrer\">question on the urql GitHub discussions board\u003C/a>, but I have not gotten any response yet.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>This article was written with \u003Ccode>@svelte/kit\u003C/code> version \u003Ccode>1.0.0-next.499\u003C/code> and \u003Ccode>@urql/svelte\u003C/code> version \u003Ccode>3.0.1\u003C/code>.\nI will try to update this article as I update my codebase to newer versions.\u003C/p>\n\u003C/blockquote>\n\u003Cp>If this post helped you, or you found a better or different way to solve SSR with urql, please let me know in the comments, write me an email or tag me on twitter \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a>.\u003C/p>","blog/2022-09-27-sveltekit-ssr-with-urql","1e223cab-bca2-4b3b-a75a-71f158c90cba",["Date","2022-09-26T00:00:00.000Z"],["Date","2022-09-26T08:55:23.886Z"],[9],null,"SvelteKit Server-Side Rendering (SSR) with @urql/svelte",true,"Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.","https://i.imgur.com/5DBIbbT.png",[15,16,17,18],"urql","sveltekit","SSR","graphql","\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>",[18,21,16,15],"ssr","article","blog",[25],{"id":26,"type":27,"replyTo":28,"timestamp":29,"page":4,"url":30,"content":31,"name":32},"a38babef-2c4c-41e6-a748-8723b6cc34ef","comment","","2023-02-05T00:02:51Z","https://tiim.ch/blog/2022-09-27-sveltekit-ssr-with-urql#a38babef-2c4c-41e6-a748-8723b6cc34ef","Hi\n\ninspiring article. Anyway, inspired with it I tried to find more seamless integration - get SSR rendered queries but keep original interface. \n\nYou may be interested in my approach, it's dropped in discussion you open \nhttps://github.com/urql-graphql/urql/discussions/2703","Farin","2023-09-02T19:26:59Z",{"html":35,"slug":36,"uuid":37,"title":38,"date":39,"modified":40,"section":41,"published":11,"content_tags":42,"links":45,"abstract":48,"tags":49,"type":22,"cover_image":-1,"description":28,"folder":50,"comments":51,"latestComment":33},"\u003Cp>A github hosed wiki for all things 3D scanning: \u003Ca href=\"https://3dscanning.wiki/Photogrammetry\" rel=\"nofollow noopener noreferrer\">photogrammetry\u003C/a>, \u003Ca href=\"https://3dscanning.wiki/Lidar\" rel=\"nofollow noopener noreferrer\">lidar\u003C/a>, laser scanning and more. The page is a static website built from a github repository with markdown files.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Deprecated\u003C/span>\u003Cp>The 3D scanning wiki is now offline. But all pages are still available on github.\u003C/p>\n\u003C/blockquote>","projects/3d-scanning-wiki","fd95701b-6d38-4ff0-85a1-d1f919bf9251","The 3D Scanning Wiki",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-11-21T12:45:23.000Z"],"Projects",[16,43,44],"markdown","dev",[46,47],"\u003Cp>\u003Ca href=\"https://3dscanning.wiki/\" rel=\"nofollow noopener noreferrer\">3D Scanning Wiki\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://github.com/3dscanningwiki\" rel=\"nofollow noopener noreferrer\">Github Organisation\u003C/a>\u003C/p>","\u003Cp>A github hosed wiki for all things 3D scanning: \u003Ca href=\"https://3dscanning.wiki/Photogrammetry\">photogrammetry\u003C/a>, \u003Ca href=\"https://3dscanning.wiki/Lidar\">lidar\u003C/a>, laser scanning and more. The page is a static website built from a github repository with markdown files.\u003C/p>",[44,43,16],"projects",[],{"html":53,"slug":54,"uuid":55,"title":56,"date":57,"modified":9,"section":41,"published":11,"content_tags":58,"links":61,"abstract":63,"tags":64,"type":22,"cover_image":-1,"description":28,"folder":50,"comments":65,"latestComment":33},"\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>\n\u003Cp>I built TeamKit because the swim club I am a part of needed an easy way for coaches to have an overview of their teams, handle attendance and track their own hours (not implemented yet). I tried a bunch of existing apps and services, but all of them were either too clunky for us or required the team members to sign up as well. This was a dealbreaker for us because we have a bunch of kids teams which are too young to sign up to websites, and because many of the parents are not very tech literate.\u003C/p>\n\u003Cp>With TeamKit a coach can quickly add new team members to a team, create new members directly in the event view (useful for example a person that just wants to try out) without having to add more details than a name.\u003C/p>\n\u003Cp>In the latest update, TeamKit now allows users to create notes on events. This is useful for planning, writing quick notes for an event or a practice session and for sharing information with other coaches.\u003C/p>\n\u003Cp>If you are interested, TeamKit is currenlty free while it is still in beta.\u003C/p>","projects/teamkit","a3040709-cd2d-43cb-ab33-2061ba1ae061","TeamKit",["Date","2022-11-27T09:19:08.000Z"],[16,59,60,44],"hasura","postgres",[62],"\u003Cp>\u003Ca href=\"https://teamkit.cc\" rel=\"nofollow noopener noreferrer\">TeamKit\u003C/a>\u003C/p>","\u003Cp>TemKit makes it easy to organize any kind of teams. Built for sport clubs, coaches, youth groups and more. TeamKit supports taking attendance, planning practices or events and keeping track of what coaches/teachers are responsible for which team.\u003C/p>",[44,59,60,16],[],{"tag":16}],"uses":{"params":["slug"]}}]} diff --git a/tags/swim.html b/tags/swim.html new file mode 100644 index 00000000..cd5d11f1 --- /dev/null +++ b/tags/swim.html @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️swim - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: swim

+

🏊‍♀️ This is all the content related to my hobby swimming.

+
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/swim/__data.json b/tags/swim/__data.json new file mode 100644 index 00000000..d1f5e60a --- /dev/null +++ b/tags/swim/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":39},[2,25],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":15,"abstract":18,"tags":19,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.\u003C/p>\n\u003Cp>I built this little page to help me count laps for my swimmers for some time based test sets. The first version of the app just had a grid of static buttons, one for each athlete. I quickly found that it is very hard to keep track which laps I already counted. As a way to visualise when each button was last pressed, they change colour. The buttons start green when pressed and slowly turn red over time, based on the average duration of a lap.\u003C/p>\n\u003Cp>The page works completely offline (after the page loads) and no data is sent to any server. It is also possible to export the data to a csv file.\u003C/p>","projects/lap-counter","871ebd58-0543-4d3a-9f3d-16cd85da9bf9","Lap Counter",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14],"svelte","swim","dev",[16,17],"\u003Cp>\u003Ca href=\"https://tiim.ch/lap-counter-js/\" rel=\"nofollow noopener noreferrer\">Open Lap Counter\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/laps-counter-8f0sb\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small web app help coaches count laps for multiple athletes. Has an integrated stopwatch and calculates the split for each athlete automatically.\u003C/p>",[14,12,13],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":26,"slug":27,"uuid":28,"title":29,"date":30,"modified":31,"section":9,"published":10,"content_tags":32,"links":34,"abstract":36,"tags":37,"type":20,"cover_image":-1,"description":21,"folder":22,"comments":38,"latestComment":24},"\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\" rel=\"nofollow noopener noreferrer\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/lenex-splits-sheet-creator.png\" alt=\"Screenshot of a split sheet\">\u003C/p>\n\u003Cp>The split sheet creator is a quick and easy way to create a split sheet from your Lenex sign-up file.\nThe split sheet creator does not send your Lenex file to any servers. The file is opened directly in your browser and never leaves your computer!\u003C/p>\n\u003Cp>I built this web app using my \u003Ca href=\"https://www.npmjs.com/package/js-lenex\" rel=\"nofollow noopener noreferrer\">lenex javascript library\u003C/a>.\u003C/p>\n\u003Ch2>What is a split sheet?\u003C/h2>\n\u003Cp>Swim coaches often write down the times of athletes after some fractions of a race (splits). Usually those splits are recorded every lap or every second lap.\u003C/p>","projects/lenex-split-sheet","a03b6eef-ffbe-428d-a559-42bd361ab88c","Lenex Splits Sheet Creator",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[13,33,12,14],"lenex",[35],"\u003Cp>\u003Ca href=\"https://tiim.ch/lenex-splits-sheet-creator/\" rel=\"nofollow noopener noreferrer\">Open Splits Sheet Creator\u003C/a>\u003C/p>","\u003Cp>Generate useful splits sheets directly from your \u003Ca href=\"https://de.wikipedia.org/wiki/Lenex\">Lenex\u003C/a> \u003Cem>(.lxf, .lef)\u003C/em> file that you used to sign up the athletes for a meet.\u003C/p>",[14,33,12,13],[],{"html":40,"slug":41,"uuid":42,"published":10,"date":43,"modified":44,"abstract":40,"tags":45,"links":-1,"type":20,"cover_image":-1,"description":21,"folder":46,"tag":13},"\u003Cp>🏊‍♀️ This is all the content related to my hobby swimming.\u003C/p>","tags/swim","456825d8-b2f6-4c1a-8976-3358ed9dacba",["Date","2022-06-03T00:00:56.000Z"],["Date","2022-06-08T22:15:48.000Z"],[],"tags"],"uses":{"params":["slug"]}}]} diff --git a/tags/tiim.ch.html b/tags/tiim.ch.html new file mode 100644 index 00000000..cf59b64a --- /dev/null +++ b/tags/tiim.ch.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️tiim.ch - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: tiim.ch

+ +

First Go Project: A Jam-stack Commenting API

+ 7/12/2022 +
First Go Project: A Jam-stack Commenting API +

I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/tiim.ch/__data.json b/tags/tiim.ch/__data.json new file mode 100644 index 00000000..177225eb --- /dev/null +++ b/tags/tiim.ch/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":155},[2,77],{"html":3,"slug":4,"uuid":5,"date":6,"aliases":7,"title":9,"published":10,"modified":11,"description":12,"cover_image":13,"content_tags":14,"syndication":21,"abstract":23,"tags":24,"links":-1,"type":27,"folder":28,"comments":29,"latestComment":34},"\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\" rel=\"nofollow noopener noreferrer\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\" rel=\"nofollow noopener noreferrer\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>\n\u003Ch2>The IndieWeb\u003C/h2>\n\u003Cp>I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.\u003C/p>\n\u003Cp>The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.\u003C/p>\n\u003Cp>The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.\u003C/p>\n\u003Cp>The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.\u003C/p>\n\u003Cp>Some of the standards in the IndieWeb include:\u003C/p>\n\u003Cul>\n\u003Cli>Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.\u003C/li>\n\u003Cli>Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.\u003C/li>\n\u003Cli>IndieAuth, an OAuth2-based way to log in using only your domain name.\u003C/li>\n\u003C/ul>\n\u003Ch2>The implementation on my website\u003C/h2>\n\u003Cp>As explained in my earlier post \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">First Go Project: A Jam-stack Commenting API\u003C/a>, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.\u003C/p>\n\u003Ch3>Making the website machine-readable with Microformats\u003C/h3>\n\u003Cp>As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called \u003Ca href=\"http://microformats.org/wiki/h-entry\" rel=\"nofollow noopener noreferrer\">h-entry\u003C/a>. By adding the \u003Ccode>h-entry\u003C/code> class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as \u003Ccode>p-name\u003C/code>, \u003Ccode>p-author\u003C/code> or \u003Ccode>dt-published\u003C/code>.\u003C/p>\n\u003Cp>While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.\u003C/p>\n\u003Cp>Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.\u003C/p>\n\u003Ch3>Accepting comments and other interactions via Webmentions\u003C/h3>\n\u003Cp>The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.\u003C/p>\n\u003Cp>In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.\u003C/p>\n\u003Cp>Since I already have a \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">small custom service\u003C/a> running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions\u003C/p>\n\u003Cp>It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database.\nI found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.\u003C/p>\n\u003Cp>I have tested my webmentions implementation using \u003Ca href=\"https://webmention.rocks\" rel=\"nofollow noopener noreferrer\">webmention.rocks\u003C/a>, but I would appreciate it if you left me a mention as well 😃\u003C/p>\n\u003Ch3>Publishing short-form content such as replies, likes and bookmarks: A notes post type\u003C/h3>\n\u003Cp>The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">notes\u003C/a>. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.\u003C/p>\n\u003Cp>I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: \u003Ca href=\"https://tiim.ch/full-rss.xml\" rel=\"nofollow noopener noreferrer\">full-rss.xml\u003C/a>. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.\u003C/p>\n\u003Ch3>Notifying referenced websites: Sending Webmentions\u003C/h3>\n\u003Cp>Sending webmentions was easy compared to receiving webmentions:\u003C/p>\n\u003Cp>On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.\u003C/p>\n\u003Cp>Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library \u003Ca href=\"https://github.com/go-co-op/gocron\" rel=\"nofollow noopener noreferrer\">gocron\u003C/a> for scheduling the regular intervals, \u003Ca href=\"https://github.com/mmcdole/gofeed\" rel=\"nofollow noopener noreferrer\">gofeed\u003C/a> for parsing the RSS feed and \u003Ca href=\"https://willnorris.com/go/webmention\" rel=\"nofollow noopener noreferrer\">webmention\u003C/a> for extracting links and sending webmentions.\u003C/p>\n\u003Ch3>In the future: IndieAuth\u003C/h3>\n\u003Cp>The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.\u003C/p>\n\u003Cp>Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.\u003C/p>\u003Cdiv class=\"mf2\">\u003Cblockquote class=\"syndication\">This post is also on \u003Cul>\u003Cli>\u003Ca class=\"u-syndication\" href=\"https://news.indieweb.org/en\">news.indieweb.org\u003C/a>\u003C/li>\u003C/ul>\u003C/blockquote>\u003C/div>\n","blog/2022-12-indiewebifying-my-website-part-1","3b342241-c414-4670-bd22-03e13d6531b7",["Date","2022-11-12T10:55:14.000Z"],[8],null,"IndieWebifying my Website Part 1 - Microformats and Webmentions",true,["Date","2022-12-03T20:56:54.000Z"],"This site now supports sending and receiving webmentions and surfacing structured data using microformats2.","https://i.imgur.com/FpgIBxI.jpg",[15,16,17,18,19,20],"IndieWeb","Webmentions","mf2","tiim.ch","go","indiego",[22],"https://news.indieweb.org/en","\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>",[19,20,25,17,18,26],"indieweb","webmentions","article","blog",[30,37,43,48,54,59,65,71],{"id":31,"type":32,"replyTo":33,"timestamp":34,"page":4,"url":35,"content":33,"name":36},"41435fdd-5fd2-4175-b9ea-ef9ce0dec154","webmention","","2023-09-02T19:26:59Z","https://evgenykuznetsov.org/en/reactions/2022/like-337215655/","Evgeny Kuznetsov",{"id":38,"type":32,"replyTo":33,"timestamp":39,"page":4,"url":40,"content":41,"name":42},"68bd3601-4f00-42c1-b28c-1f2ef75ac851","2023-08-02T09:10:03Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...","Tim Bachmann",{"id":44,"type":32,"replyTo":33,"timestamp":45,"page":4,"url":46,"content":47,"name":42},"5f508a42-8b83-4c10-9f7e-9c1b80e23ab1","2022-12-09T10:49:06Z","https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting","Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.",{"id":49,"type":32,"replyTo":33,"timestamp":50,"page":4,"url":51,"content":52,"name":53},"dc8dbf30-ff1f-4e13-ba36-37f12666005c","2022-12-05T08:41:07Z","https://martymcgui.re/2022/12/05/033926/","★ Liked https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","https://martymcgui.re/",{"id":55,"type":32,"replyTo":33,"timestamp":56,"page":4,"url":57,"content":33,"name":58},"6f6c1f11-2ae0-41a4-b7d0-e343ef63aa52","2022-11-27T22:32:27Z","https://brid.gy/like/twitter/tiimb/1591417020557525003/48372745","Jimmy Lipham",{"id":60,"type":32,"replyTo":33,"timestamp":61,"page":4,"url":62,"content":63,"name":64},"5e1c4149-8fb9-48fb-b285-5efbd626b259","2022-11-27T22:31:57Z","https://brid.gy/repost/twitter/tiimb/1591417020557525003/1591418692008558592","I published a new blog post:\nIndieWebifying my Website Part 1 - Microformats and Webmentions\ntiim.ch/blog/2022-12-i…\n#indieweb #microformats #webmentions #golang","Golang Smart Bot",{"id":66,"type":32,"replyTo":33,"timestamp":67,"page":4,"url":68,"content":69,"name":70},"6e0bf830-1735-42a2-9aa2-ea4c40ab7a45","2022-11-15T12:47:35Z","https://webmention.rocks/receive/1/f0fa5421056e068fe902932ef98f6d71","This test verifies that you accept a Webmention request that contains a valid source and target URL. To pass this test, your Webmention endpoint must return either HTTP 200, 201 or 202 along with the appropriate headers.\nIf your endpoint returns HTTP 201, then it MUST also return a Location header. If it returns HTTP 200 or 202, then it MUST NOT include a Location header.","Webmention Rocks!",{"id":72,"type":32,"replyTo":33,"timestamp":73,"page":4,"url":74,"content":75,"name":76},"4e237d08-9f22-480e-a46e-8f40adf06c5e","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/rm8as/","Liked\nIndieWebifying my Website Part 1 - Microformats and Webmentions\nPost detailsThis site now supports sending and receiving webmentions and surfacing structured data using microformats2. https://i.imgur.com/FpgIBxI.jpg","Jamie Tanna",{"html":78,"slug":79,"uuid":80,"date":81,"created":82,"aliases":83,"title":84,"published":10,"modified":85,"description":86,"cover_image":87,"content_tags":88,"abstract":91,"tags":92,"links":-1,"type":27,"folder":28,"comments":93,"latestComment":34},"\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\" rel=\"nofollow noopener noreferrer\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>\n\u003Cp>I kept my eyes open for a service that I liked, but I did not want to just integrate any old service into my website, I did have some requirements:\u003C/p>\n\u003Cul>\n\u003Cli>The service should not cost anything. I would rather host something myself than sign up for another subscription (because I'm already paying for a VPS anyway).\u003C/li>\n\u003Cli>I want to control how the comments on my website are displayed. I quite like my website design and I don't want a generic comment box below my posts.\u003C/li>\n\u003Cli>The service should respect the privacy of the people using my website.\u003C/li>\n\u003Cli>There should be an option to comment without setting up an account with the service.\u003C/li>\n\u003C/ul>\n\u003Cp>While looking around for how other people integrated comments into their static websites, I found a nice \u003Ca href=\"https://averagelinuxuser.com/static-website-commenting/\" rel=\"nofollow noopener noreferrer\">blog post from Average Linux User\u003C/a> which compares a few popular commenting systems.\nUnfortunately, most systems either are not very privacy-friendly, cost money or store the comments as comments on Github issues..?\nAfter looking through the options I decided to use this opportunity to write my own commenting system and dabble with the Go programming language.\u003C/p>\n\u003Ch2>Writing a commenting API in Go\u003C/h2>\n\u003Cp>First thing first, if you want to take a look at the code, check out the \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">Github repo\u003C/a>.\u003C/p>\n\u003Cp>I decided to write the commenting system in Go because I have been looking for an excuse to practice Go for a while, and this seemed like the perfect fit. It is a small CRUD app, consisting of a storage component, an API component and a small event component in the middle to easily compose the functionality I want.\u003C/p>\n\u003Cp>Currently, it supports the following functionality:\u003C/p>\n\u003Cul>\n\u003Cli>Listing all comments (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Listing all comments for a specified page (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Posting comments through the API\u003C/li>\n\u003Cli>A simple admin dashboard that lists all comments and allows the admin to delete them\u003C/li>\n\u003Cli>Email notifications when someone comments\u003C/li>\n\u003Cli>Email notifications when someone replies to your comment\u003C/li>\n\u003Cli>SQLite storage for comments\u003C/li>\n\u003C/ul>\n\u003Cp>The code is built in a way to make it easy to customise the features.\nFor example to disable features like the email reply notifications you can just \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/main.go#L52\" rel=\"nofollow noopener noreferrer\">comment out the line in the main.go\u003C/a> file that registers that hook.\u003C/p>\n\u003Cp>To write custom hooks that get executed when a new comment gets submitted or one gets deleted, just implement the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/event/handler.go\" rel=\"nofollow noopener noreferrer\">Handler\u003C/a> interface and register it in the main method.\u003C/p>\n\u003Cp>You can also easily add other storage options like databases or file storage by implementing the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/model/store.go\" rel=\"nofollow noopener noreferrer\">Store and SubscribtionStore\u003C/a> interfaces.\u003C/p>\n\u003Ch2>Can it be used in production? 🚗💨\u003C/h2>\n\u003Cp>I currently use it on this website! Go test it out (I might delete the comments if they are rude though 🤔).\u003C/p>\n\u003Cp>In all seriousness, I would not use it for a website where the comments are critical. But for a personal blog or similar, I don't see why not.\u003C/p>\n\u003Cp>If you want to host your own version, there is a Dockerfile available. If you decide to integrate this into your website, please comment below, ping me \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> or shoot me an email \u003Ca href=\"mailto:hey@tiim.ch\">hey@tiim.ch\u003C/a>, I would love to check it out.\u003C/p>","blog/2022-07-12-first-go-project-commenting-api","bff14052-4f3f-4dcb-bcee-155ae1c6b09e",["Date","2022-07-12T00:00:00.000Z"],["Date","2022-07-08T16:24:37.766Z"],[8],"First Go Project: A Jam-stack Commenting API",["Date","2022-11-23T21:42:29.000Z"],"I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!","/assets/2022-07-first-go-project-commenting-api.png",[19,89,90,18,20],"web-api","project","\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>",[19,20,90,18,89],[94,97,104,111,116,122,126,131,138,144,149],{"id":95,"type":32,"replyTo":33,"timestamp":96,"page":79,"url":40,"content":41,"name":42},"31ec5f44-15b2-498a-890d-350e38b9a83e","2023-08-02T09:10:04Z",{"id":98,"type":99,"replyTo":33,"timestamp":100,"page":79,"url":101,"content":102,"name":103},"6d792d24-ba58-4408-83a4-3583667ff4ad","comment","2023-07-09T19:25:02Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#6d792d24-ba58-4408-83a4-3583667ff4ad","Heya just saw your post on Reddit about this comment feature, didn't want to leave without using it ^^. Nicely done!","Anonymous",{"id":105,"type":99,"replyTo":106,"timestamp":107,"page":79,"url":108,"content":109,"name":110},"99dd9ccf-5349-4f41-9553-67986e1a1074","1c8ba0da-10df-4a7a-b067-55875441de2d","2022-12-07T23:01:37Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#99dd9ccf-5349-4f41-9553-67986e1a1074","You are right, there is not any documentation in the readme yet. Although hopefully, I will work on that soon. I'm in the middle of refactoring the project.\n\nTo query the comments there are two rest endpoints [\"/comment\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L41-L71) and [\"/comment/:page\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L73-L107) which return all comments or the comments for a specific page. The comments are loaded from this API endpoint when the site is generated.\n\nTo display the comments without rebuilding the site, new comments are fetched in the browser with the \"?since=\u003Ctime-of-last-build>\" query parameter.","Tiim",{"id":106,"type":99,"replyTo":33,"timestamp":112,"page":79,"url":113,"content":114,"name":115},"2022-12-07T22:33:44Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#1c8ba0da-10df-4a7a-b067-55875441de2d","Good stuff. Always nice to see a site supporting comments and/or Webmentions. Maybe I missed it in the readme but I am curious as to how one queries the API for comments. Do you pull in the comments from the database when you generate the site?","Poorchop",{"id":117,"type":32,"replyTo":33,"timestamp":118,"page":79,"url":119,"content":120,"name":121},"e42756e4-d5a5-4727-8c58-434d285b7ab3","2022-11-27T22:31:58Z","https://brid.gy/repost/twitter/tiimb/1546801590593638400/1546801615264415745","I published a new blog post:\nFirst Go Project: A jam-stack Commenting API\ntiim.ch/blog/2022-07-1…\n#golang #jamstack #API","Golang Bot",{"id":123,"type":32,"replyTo":33,"timestamp":124,"page":79,"url":125,"content":12,"name":42},"b8d3ae8b-0059-4379-8c35-30c97269908f","2022-11-21T22:19:23Z","https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1",{"id":127,"type":99,"replyTo":33,"timestamp":128,"page":79,"url":129,"content":130,"name":130},"171e3444-f1d0-492d-8bc7-c0a133a41783","2022-07-18T08:44:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#171e3444-f1d0-492d-8bc7-c0a133a41783","hola",{"id":132,"type":99,"replyTo":133,"timestamp":134,"page":79,"url":135,"content":136,"name":137},"5875bba1-e69b-467a-bab5-23ef4160d257","621574fd-eea2-48d6-87c8-aebd0f05f1aa","2022-07-13T21:06:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#5875bba1-e69b-467a-bab5-23ef4160d257","And a polite reply","polite",{"id":139,"type":99,"replyTo":33,"timestamp":140,"page":79,"url":141,"content":142,"name":143},"8494c653-ef37-47a2-ae1b-00d00e4815a9","2022-07-13T18:31:31Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#8494c653-ef37-47a2-ae1b-00d00e4815a9","Pretty cool dudez","somGuy",{"id":133,"type":99,"replyTo":33,"timestamp":145,"page":79,"url":146,"content":147,"name":148},"2022-07-12T21:40:39Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#621574fd-eea2-48d6-87c8-aebd0f05f1aa","This is a rude comment ;)","rude",{"id":150,"type":99,"replyTo":33,"timestamp":151,"page":79,"url":152,"content":153,"name":154},"fb6278ae-e48c-4397-ba29-bec4e5cb3a57","2022-07-12T13:30:14Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#fb6278ae-e48c-4397-ba29-bec4e5cb3a57","Good job dude!","wdup",{"tag":18}],"uses":{"params":["slug"]}}]} diff --git a/tags/twitter.html b/tags/twitter.html new file mode 100644 index 00000000..a68b7069 --- /dev/null +++ b/tags/twitter.html @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️twitter - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: twitter

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/twitter/__data.json b/tags/twitter/__data.json new file mode 100644 index 00000000..f294a16f --- /dev/null +++ b/tags/twitter/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":49},[2,38],{"html":3,"slug":4,"name":5,"date":6,"content_tags":7,"like_of":11,"raw_data":13,"tags":30,"links":-1,"published":32,"type":33,"cover_image":-1,"description":34,"folder":35,"comments":36,"latestComment":37},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html\">https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/12/otewmj","I Was Wrong About Mastodon",["Date","2022-12-01T08:24:00.000Z"],[8,9,10],"Mastodon","twitter","moderation",{"url":12},"https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html",{"items":14,"rels":28,"relurls":29},[15],{"id":16,"value":16,"html":16,"type":17,"properties":19,"shape":16,"coords":16,"children":27},"",[18],"h-entry",{"category":20,"like-of":21,"name":22,"post-status":23,"published":25},[8,9,10],[12],[5],[24],"published",[26],"2022-12-01T09:24:00+0100",[],{},{},[31,10,9],"mastodon",true,"like","👍 Liked: https://escapingtech.com/tech/opinions/i-was-wrong-about-mastodon-moderation.html","mf2",[],"2023-09-02T19:26:59Z",{"html":39,"slug":40,"date":41,"content_tags":42,"like_of":44,"tags":46,"links":-1,"published":32,"type":33,"cover_image":-1,"description":47,"folder":35,"comments":48,"latestComment":37},"\u003Cdiv class=\"mf2\">\u003Cp>Liked \u003Ca class=\"u-like-of\" href=\"https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/\">https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/\u003C/a>\u003C/p>\u003C/div>\n","mf2/2022/11/mji1md",["Date","2022-11-22T10:28:00.000Z"],[9,43,31],"fediverse",{"url":45},"https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/",[43,31,9],"👍 Liked: https://www.zylstra.org/blog/2022/11/everyones-so-nice-around-here-best-before-see-back/",[],{"tag":9}],"uses":{"params":["slug"]}}]} diff --git a/tags/urql.html b/tags/urql.html new file mode 100644 index 00000000..2a7beebf --- /dev/null +++ b/tags/urql.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️urql - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: urql

+ +

SvelteKit Server-Side Rendering (SSR) with @urql/svelte

+ 9/26/2022 +
SvelteKit Server-Side Rendering (SSR) with @urql/svelte +

Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/urql/__data.json b/tags/urql/__data.json new file mode 100644 index 00000000..3351b443 --- /dev/null +++ b/tags/urql/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":34},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"content_tags":14,"abstract":19,"tags":20,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":33},"\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\" rel=\"nofollow noopener noreferrer\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>\n\u003Cp>Server-side rendering (SSR) is one of the great features of SvelteKit. I will try to keep this blog post short and will therefore not explain what server-side rendering is and why you should take advantage of it \u003Cem>(you really should!)\u003C/em>. If you want to know more about SSR you can take a look at this article: \u003Ca href=\"https://towardsdev.com/server-side-rendering-srr-in-javascript-a1b7298f0d04\" rel=\"nofollow noopener noreferrer\">A Deep Dive into Server-Side Rendering (SSR) in JavaScript\u003C/a>.\u003C/p>\n\u003Ch2>Background - SSR in SvelteKit\u003C/h2>\n\u003Cp>SvelteKit implements SSR by providing a \u003Ca href=\"https://kit.svelte.dev/docs/load\" rel=\"nofollow noopener noreferrer\">\u003Ccode>load\u003C/code> function\u003C/a> for every layout and page component. If a page or layout needs to perform some asynchronous operation, this should be done inside of this load function. SvelteKit executes this function asynchronously on the server side as well as on the client side and the return value of this function is assigned to the \u003Ccode>data\u003C/code> prop of the associated component. Usually, this asynchronous operation is loading data from an external service, like in the case of this blog post a GraphQL server.\nYou can of course load data directly in the component, but SvelteKit will not wait for this to complete when doing SSR, and the resulting HTML will not include the loaded data.\u003C/p>\n\u003Ch2>Background - @urql/svelte\u003C/h2>\n\u003Cp>The urql library allows us to easily issue GraphQL queries and mutations. Some of the functionality it has to make our lives easier include:\u003C/p>\n\u003Cul>\n\u003Cli>Reloading a query when a query variable changes\u003C/li>\n\u003Cli>Reloading a query after a mutation that touches the same data as the query\u003C/li>\n\u003C/ul>\n\u003Cp>We want to keep these features, even when using urql when doing SSR.\u003C/p>\n\u003Ch2>The Problem\u003C/h2>\n\u003Cp>When implementing SSR in my project, I ran into two problems. I couldn't find any documentation or any articles solving them, so I decided to write down my solutions to those problems in this blog post.\u003C/p>\n\u003Ch3>Problem 1 - Svelte and urql Reactivity\u003C/h3>\n\u003Cp>Let's say we have the following load function, which executes a GraphQL query to load a list of red cars:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\n\n/** @type {import('./$types').PageLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const carColor = \"red\";\n\n const cars = client\n .query(carsQuery, {\n color: carColor,\n })\n .toPromise()\n .then((c) => c.data?.car);\n\n return {\n cars,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>This example uses the urql method \u003Ccode>client.query\u003C/code> to start a query to get us a list of cars with a red colour (The GraphQL query is not shown but the exact query is not important for this example).\nThe client gets a \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">special fetch function\u003C/a> from the event which has a few nice properties, like preventing a second network request on the client side if that same request was just issued on the server-side.\u003C/p>\n\u003Cp>Since the query code is now located in the load function and not in a svelte component, there is no way to easily change the \u003Ccode>carColor\u003C/code> and have urql automatically reload the query. The only way to change the variable is to set the value as a query parameter and read that from the \u003Ccode>event\u003C/code> argument. This however means that we have to refresh the whole page just to reload this query.\u003C/p>\n\u003Cp>The other thing urql does for us, reloading the query when we do a mutation on the same data, will not work with the above code either.\u003C/p>\n\u003Ch3>The solution: A query in the load function and a query in the component\u003C/h3>\n\u003Cp>To fix those two drawbacks we have to add the same query as in the load function to our component code as well. Unfortunately, this means when a user loads the page, it sends a request from the client side, even though the same request got sent from the server side already.\u003C/p>\n\u003Cp>I created a small wrapper function \u003Ccode>queryStoreInitialData\u003C/code> that creates the query inside of the component and intelligently switches from the (possibly stale) data from the load function to the new data. Using this wrapper, the page or layout might look as follows:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-svelte\"><script>\n import { queryStoreInitialData } from \"@/lib/gql-client\"; // The helper function mentioned above\n import { getContextClient } from \"@urql/svelte\";\n import { carsQuery } from \"./query\"; // The query\n\n export let data;\n\n $: gqlStore = queryStoreInitialData(\n {\n client: getContextClient(),\n query: carsQuery,\n },\n data.cars\n );\n $: cars = $gqlStore?.data?.car;\n</script>\n\n<div>\n <pre>\n {JSON.stringify(cars, null, 2)}\n </pre>\n</div>\n\u003C/code>\u003C/pre>\n\u003Col>\n\u003Cli>The native \u003Ccode>queryStore\u003C/code> function gets replaced with the wrapper function.\u003C/li>\n\u003Cli>The initial value of the query is supplied to the wrapper\u003C/li>\n\u003C/ol>\n\u003Cp>Unfortunately, we can not return the query result from the load function directly like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">const result = await client.query(cars, {}).toPromise();\n\nreturn {\n cars: toInitialValue(result),\n};\n\u003C/code>\u003C/pre>\n\u003Cp>This results in the following error:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-stacktrace\">Cannot stringify a function (data.events.operation.context.fetch)\nError: Cannot stringify a function (data.events.operation.context.fetch)\n at render_response (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/render.js:181:20)\n at runMicrotasks (<anonymous>)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async render_page (file:///app/node_modules/@sveltejs/kit/src/runtime/server/page/index.js:276:10)\n at async resolve (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:232:17)\n at async respond (file:///app/node_modules/@sveltejs/kit/src/runtime/server/index.js:284:20)\n at async file:///app/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:406:22\n\u003C/code>\u003C/pre>\n\u003Cp>This is because the query result contains data that is not serializable.\nTo fix this I created the \u003Ccode>toInitialValue\u003C/code> function, which deletes all non-serializable elements from the result. The load function now looks like follows;\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// src/routes/car/+page.js\nimport { createServerClient, toInitialValue } from \"@/lib/gql-client\";\nimport { parse } from \"cookie\";\nimport { carsQuery } from \"./query\";\n\n/** @type {import('./$types').PageServerLoad} */\nexport const load = async (event) => {\n const client = createClient({\n url: config.url,\n fetch: event.fetch,\n });\n\n const result = await client.query(cars, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n};\n\u003C/code>\u003C/pre>\n\u003Ch3>Problem 2 - Authentication\u003C/h3>\n\u003Cp>We will look at the same \u003Ccode>load\u003C/code> function as #Problem 1 - Svelte and urql Reactivity: the function creates a urql client with the fetch function from the event object and uses this client to send a query.\u003C/p>\n\u003Cp>Sometimes however the GraphQL API requires authentication in the form of a cookie to allow access.\u003C/p>\n\u003Cp>Unfortunately, the \u003Ca href=\"https://kit.svelte.dev/docs/load#input-methods-fetch\" rel=\"nofollow noopener noreferrer\">fetch function that we get from the load event\u003C/a> will only pass the cookies on if the requested domain is the same as the base domain or a more specific subdomain of it. This means if your SvelteKit site runs on \u003Ccode>example.com\u003C/code> and your GraphQL server runs on \u003Ccode>gql.example.com\u003C/code> then the cookies will get forwarded and everything is fine. This however is, in my experience, often not the case. Either you might use an external service for your GraphQL API or you host it yourself and want to use its internal domain.\u003C/p>\n\u003Cp>The only way to pass the cookies on to the GraphQL server, in this case, is by manually setting the cookie header when creating the urql client. This however forces us to use the server-only load function, as we do not have access to the cookie header in the normal load function.\u003C/p>\n\u003Cp>The new code now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createClient({\n url: config.url,\n fetch,\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // inject the cookie header\n // FIXME: change the cookie name\n Cookie: `gql-session=${event.cookies.get(\"gql-session\")}`,\n },\n },\n });\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>To keep the size of the load functions across my codebase smaller I created a small wrapper function \u003Ccode>createServerClient\u003C/code>:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/routes/car/+page.server.js\n\n/** @type {import('./$types').PageServerLoad} */\nexport function load(event) {\n const client = createServerClient(event.cookies);\n\n const cars = client.query(carsQuery, {}).toPromise();\n\n return {\n cars: toInitialValue(result),\n };\n}\n\u003C/code>\u003C/pre>\n\u003Ch2>The Code\u003C/h2>\n\u003Cp>Below you can find the three functions \u003Ccode>createServerClient\u003C/code>, \u003Ccode>queryStoreInitialData\u003C/code> and \u003Ccode>toInitialValue\u003C/code> that we used above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-js\">// /src/lib/gql-client.js\n\nimport { browser } from \"$app/environment\";\nimport { urls } from \"@/config\";\nimport { createClient, queryStore } from \"@urql/svelte\";\nimport { derived, readable } from \"svelte/store\";\n\n/**\n * Helper function to create an urql client for a server-side-only load function\n *\n *\n * @param {import('@sveltejs/kit').Cookies} cookies\n * @returns\n */\nexport function createServerClient(cookies) {\n return createClient({\n // FIXME: adjust your graphql url\n url: urls.gql,\n fetch,\n // FIXME: if you don't need to authenticate, delete the following object:\n fetchOptions: {\n credentials: \"include\",\n headers: {\n // FIXME: if you want to set a cookie adjust the cookie name\n Cookie: `gql-session=${cookies.get(\"gql-session\")}`,\n },\n },\n });\n}\n\n/**\n * Helper method to send a GraphQL query but use the data from the SvelteKit load function initially.\n *\n *\n * @param {any} queryArgs\n * @param {any} initialValue\n * @returns\n */\nexport function queryStoreInitialData(queryArgs, initialValue) {\n if (!initialValue || (!initialValue.error && !initialValue.data)) {\n throw new Error(\"No initial value from server\");\n }\n\n let query = readable({ fetching: true });\n if (browser) {\n query = queryStore(queryArgs);\n }\n\n return derived(query, (value, set) => {\n if (value.fetching) {\n set({ ...initialValue, source: \"server\", fetching: true });\n } else {\n set({ ...value, source: \"client\" });\n }\n });\n}\n\n/**\n * Make the result object of a urql query serialisable.\n *\n *\n * @template T\n * @param {Promise<import('@urql/svelte').OperationResult<T, any >>|import('@urql/svelte').OperationResult<T, any >} result\n * @returns {Promise<{fetching:false, error: undefined | {name?: string, message?: string; graphQLErrors?: any[]; networkError?: Error; response?: any;}, data: T|undefined}>}\n */\nexport async function toInitialValue(result) {\n const { error, data } = await result;\n\n // required to turn class array into array of javascript objects\n const errorObject = error ? {} : undefined;\n if (errorObject) {\n console.warn(error);\n errorObject.graphQLErrors = error?.graphQLErrors?.map((e) => ({ ...e }));\n errorObject.networkError = { ...error?.networkError };\n errorObject.response = { value: \"response omitted\" };\n }\n\n return {\n fetching: false,\n error: { ...error, ...errorObject },\n data,\n };\n}\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Ca href=\"https://gist.github.com/Tiim/1adeb4d74ce7ae09d0d0aa4176a6195d\" rel=\"nofollow noopener noreferrer\">Link to the Gist\u003C/a>\u003C/p>\n\u003Ch2>End remarks\u003C/h2>\n\u003Cp>Even though I think this solution is not too bad, I wish @urql/svelte would implement a better way to handle SSR with sveltekit. I posted a \u003Ca href=\"https://github.com/FormidableLabs/urql/discussions/2703\" rel=\"nofollow noopener noreferrer\">question on the urql GitHub discussions board\u003C/a>, but I have not gotten any response yet.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>This article was written with \u003Ccode>@svelte/kit\u003C/code> version \u003Ccode>1.0.0-next.499\u003C/code> and \u003Ccode>@urql/svelte\u003C/code> version \u003Ccode>3.0.1\u003C/code>.\nI will try to update this article as I update my codebase to newer versions.\u003C/p>\n\u003C/blockquote>\n\u003Cp>If this post helped you, or you found a better or different way to solve SSR with urql, please let me know in the comments, write me an email or tag me on twitter \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a>.\u003C/p>","blog/2022-09-27-sveltekit-ssr-with-urql","1e223cab-bca2-4b3b-a75a-71f158c90cba",["Date","2022-09-26T00:00:00.000Z"],["Date","2022-09-26T08:55:23.886Z"],[9],null,"SvelteKit Server-Side Rendering (SSR) with @urql/svelte",true,"Learn why server-side rendering (SSR) using urql as a GraphQL client is not as straightforward as you might think and how to do it anyway.","https://i.imgur.com/5DBIbbT.png",[15,16,17,18],"urql","sveltekit","SSR","graphql","\u003Cp>In this blog post, I will explain why server-side rendering with the \u003Ca href=\"https://formidable.com/open-source/urql/docs/api/svelte/\">urql\u003C/a> GraphQL library is not as straightforward to do with SvelteKit, and how I solved this in my project anyway.\u003C/p>",[18,21,16,15],"ssr","article","blog",[25],{"id":26,"type":27,"replyTo":28,"timestamp":29,"page":4,"url":30,"content":31,"name":32},"a38babef-2c4c-41e6-a748-8723b6cc34ef","comment","","2023-02-05T00:02:51Z","https://tiim.ch/blog/2022-09-27-sveltekit-ssr-with-urql#a38babef-2c4c-41e6-a748-8723b6cc34ef","Hi\n\ninspiring article. Anyway, inspired with it I tried to find more seamless integration - get SSR rendered queries but keep original interface. \n\nYou may be interested in my approach, it's dropped in discussion you open \nhttps://github.com/urql-graphql/urql/discussions/2703","Farin","2023-09-02T19:26:59Z",{"tag":15}],"uses":{"params":["slug"]}}]} diff --git a/tags/vpn.html b/tags/vpn.html new file mode 100644 index 00000000..481cd21d --- /dev/null +++ b/tags/vpn.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️vpn - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: vpn

+ +

Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN

+ 3/15/2023 +
Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN +

I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/vpn/__data.json b/tags/vpn/__data.json new file mode 100644 index 00000000..ed78c4ff --- /dev/null +++ b/tags/vpn/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>\n\u003Cp>I will show you how I fixed this problem for me and explain what the commands I used do. This post is mostly for my reference, but I hope it helps anyone else as well.\u003C/p>\n\u003Ch2>Finding out what your problem is\u003C/h2>\n\u003Cp>Let's check first if we have internet access inside WSL2. For this run the ping command with an IP address as a destination:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping 8.8.8.8\n\u003C/code>\u003C/pre>\n\u003Cp>If you get something like this as the output, your internet connection is fine, and it's just the DNS nameserver addresses that are misconfigured, you can jump forward to Solution 2.\u003C/p>\n\u003Cpre>\u003Ccode>PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n64 bytes from 8.8.8.8: icmp_seq=1 ttl=108 time=4.53 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=108 time=3.94 ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=108 time=3.97 ms\n64 bytes from 8.8.8.8: icmp_seq=4 ttl=108 time=3.78 ms\n64 bytes from 8.8.8.8: icmp_seq=5 ttl=108 time=3.77 ms\n64 bytes from 8.8.8.8: icmp_seq=6 ttl=108 time=3.76 ms\n64 bytes from 8.8.8.8: icmp_seq=7 ttl=108 time=3.81 ms\n\u003C/code>\u003C/pre>\n\u003Cp>If you don't get any responses from the ping (i.e. no more output after the \u003Ccode>PING 8.8.8.8 (8.8.8.8) ...\u003C/code> line), you need to configure the WSL and the VPN network adapter metric. Go to Solution 1.\u003C/p>\n\u003Cp>To check if the DNS is working, we can again use the ping command, this time with a domain name:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping google.com\n\u003C/code>\u003C/pre>\n\u003Cp>If you get responses, the DNS and your internet connection are working! If not go to Section 2.\u003C/p>\n\u003Ch2>Solution 1: Fixing the Network Adapter\u003C/h2>\n\u003Cp>Run the following two commands in PowerShell as administrator:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match \"Cisco AnyConnect\"} | Set-NetIPInterface -InterfaceMetric 4000\n\nGet-NetIPInterface -InterfaceAlias \"vEthernet (WSL)\" | Set-NetIPInterface -InterfaceMetric 1\n\u003C/code>\u003C/pre>\n\u003Cp>Let me explain what those two commands do. Both follow the same pattern of listing all network adapters, selecting a specific adapter from the list and setting its \"metric\".\u003C/p>\n\u003Cp>You can imagine an adapter as a virtual network port on the back of your pc or laptop. But instead of sending packets through the wire, the driver for a specific port can do whatever it wants with those packets, in the case of a VPN, the packets get encrypted and forwarded to the internet via another adapter.\u003C/p>\n\u003Cp>The \u003Ca href=\"https://learn.microsoft.com/en-us/windows-server/networking/technologies/network-subsystem/net-sub-interface-metric\" rel=\"nofollow noopener noreferrer\">InterfaceMetric\u003C/a> is a value associated with each adapter that determines the order of those adapters. This allows windows to determine which adapter to prefer over another one.\u003C/p>\n\u003Cp>By setting the interface metric of the Cisco adapter to 4000 and the metric of the WSL adapter to one, we allow the traffic from WSL to flow through the Cisco adapter. To be honest I do not exactly understand why this works but it does.\u003C/p>\n\u003Ch2>Solution 2: Registering the VPN DNS inside of WSL\u003C/h2>\n\u003Cp>Setting the DNS servers is, unfortunately, a little bit more involved than just running two commands, we need to edit the files \u003Ccode>/etc/wsl.conf\u003C/code> and \u003Ccode>/etc/resolv.conf\u003C/code>, and restart wsl in between. Let's get to it:\u003C/p>\n\u003Cp>Edit the file \u003Ccode>/etc/wsl.conf\u003C/code> inside of WSL2 using a text editor. I suggest doing this through the terminal since you need root permissions to do that:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/wsl.conf\n# feel free to use another editor such as vim or emacs\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely this file does not exist yet, otherwise, I suggest you create a backup of the original file to preserve the settings.\u003C/p>\n\u003Cp>Add the following config settings into the file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-ini\">[network]\ngenerateResolvConf = false\n\u003C/code>\u003C/pre>\n\u003Cp>This will instruct WSL to not override the \u003Ccode>/etc/resolv.conf\u003C/code> file on every start-up. Save the file and restart WSL with the following command so that the changed config takes effect:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open a PowerShell terminal and list all network adapters with the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ipconfig /all\n\u003C/code>\u003C/pre>\n\u003Cp>Find the Cisco AnyConnect adapter and copy the IP addresses in the DNS-Server field. We will need those IPs in the next step.\u003C/p>\n\u003Cp>Start WSL again and edit the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely there is already something in this file, you can discard it. When undoing the changes, WSL will automatically regenerate this file anyway, so you don't need to back it up.\u003C/p>\n\u003Cp>Delete all the contents and enter the IP addresses you noted down in the last step in the following format:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-resolv\">nameserver xxx.xxx.xxx.xxx\n\u003C/code>\u003C/pre>\n\u003Cp>Put each address on a new line, preceded by the string \u003Ccode>nameserver\u003C/code>.\nSave the file and restart WSL with the same command as above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open up WSL for the last time and set the immutable flag for the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">chattr +i /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>And for the last time shut down WSL. Your DNS should now be working fine!\u003C/p>\n\u003Ch2>Undoing those changes\u003C/h2>\n\u003Cp>I did not have a need to undo the steps for \u003Ccode>Solution 1\u003C/code>, and I'm pretty sure the metric resets after each system reboot anyway so there is not much to do.\u003C/p>\n\u003Cp>To get DNS working again when not connected to the VPN run the following commands:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo chattr -i /etc/resolv.conf\nsudo rm /etc/resolv.conf\nsudo rm /etc/wsl.conf\nwsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>This will first clear the immutable flag off \u003Ccode>/etc/resolv.conf\u003C/code>, and delete it. Next, it will delete \u003Ccode>/etc/wsl.conf\u003C/code> if you have a backup of a previous \u003Ccode>wsl.conf\u003C/code> file, you can replace it with that. At last, we shutdown WSL again for the changes to take effect.\u003C/p>\n\u003Cp>Unfortunately, this is quite a procedure to get a VPN to work with WSL2, but I'm hopeful that this will soon not be necessairy anymore.\u003C/p>","blog/2023-03-21-anyconnect-wsl2","c67bc4dc-4c96-41b1-afb5-15a99457dedf",["Date","2023-03-15T15:22:04.511Z"],["Date","2023-03-15T15:22:04.511Z"],[9],null,"Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN",true,"I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.","https://media.tiim.ch/66ca4290-3fc0-450f-977b-f00f888e4af3.webp","Stable Diffusion - Anything V3.0 - 1boy, hacker, in front of computer, back of head visible, vintage neon color scheme, terminal, big monitor",[16,17,18,19],"wsl","vpn","networking","dns","\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>",[19,18,17,16],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/vue.html b/tags/vue.html new file mode 100644 index 00000000..aafc7e6b --- /dev/null +++ b/tags/vue.html @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️vue - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: vue

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/vue.js.html b/tags/vue.js.html new file mode 100644 index 00000000..603cd950 --- /dev/null +++ b/tags/vue.js.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️vue.js - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: vue.js

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/vue.js/__data.json b/tags/vue.js/__data.json new file mode 100644 index 00000000..057a1e68 --- /dev/null +++ b/tags/vue.js/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":25},[2],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"description":8,"content_tags":9,"date":14,"cover_image":15,"abstract":16,"tags":17,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":24},"\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\" rel=\"nofollow noopener noreferrer\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>\n\u003Cp>If you want to see an example of this method in action, go check out my \u003Ca href=\"https://tiimb.work\" rel=\"nofollow noopener noreferrer\">personal website\u003C/a> on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>\u003C/p>\n\u003Cp>I won't be explaining how to setup a Vue project. If you're looking for a Tutorial on that go check out the awesome \u003Ca href=\"https://vuejs.org/v2/guide/\" rel=\"nofollow noopener noreferrer\">Vue.js Guide\u003C/a>.\u003C/p>\n\u003Cp>So you have setup your awesome Vue project and want to host it on GitHub Pages. The way Muhammad explained it you would build the project using \u003Ccode>npm run build\u003C/code>, commit the \u003Ccode>dist/\u003C/code> folder along with your source files and point GitHub to the dist folder. This might get quite messy because you either have commit messages with the sole purpose of uploading the dist folder or you commit the code changes at the same time which makes it hard to find the relevant changes if you ever want to look at your commits again.\u003C/p>\n\u003Cp>So what can you do about this?\u003C/p>\n\u003Cp>Git to the rescue, let's use a branch that contains all the build files.\u003C/p>\n\u003Ch2>Step 1 - keeping our working branch clean 🛀\u003C/h2>\n\u003Cp>To make sure that the branch we are working from stays clean of any build files we are gonna add a \u003Ccode>.gitignore\u003C/code> file to the root.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># .gitignore\ndist/\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 2 - adding a second branch 🌳\u003C/h2>\n\u003Cp>We are not goint to branch off master like how we would do it if we were to modify our code with the intention to merge it back to the main branch. Instead we are gonna create a squeaky clean new branch that will only ever hold the dist files. After all we will not ever need to merge these two branches together.\u003C/p>\n\u003Cp>We do this by creating a new git repository inside the dist folder:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">cd dist/\ngit init\ngit add .\ngit commit -m 'Deploying my awesome vue app'\n\u003C/code>\u003C/pre>\n\u003Ch2>Step 3 - deploying 🚚\u003C/h2>\n\u003Cp>We are gonna force push our new git repository to a branch on GitHub. This might go against git best practices but since we won't ever checkout this branch we don't have to worry about that.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">git push -f git@github.com:<username>/<repo>.git <branch>\n\u003C/code>\u003C/pre>\n\u003Cp>⚠️ Make sure you double or tripple check your destination branch! You don't want to accidentally overwrite your working branch. Using the branch \u003Ccode>gh-pages\u003C/code> will most likely be a good idea.\u003C/p>\n\u003Ch2>Step 4 - pointing GitHub to the right place 👈\u003C/h2>\n\u003Cp>Now we are almost done. The only thing left is telling GitHub where our assets live.\u003C/p>\n\u003Cp>Go to your repo, on the top right navigate to \u003Ccode>Settings\u003C/code> and scroll down to GitHub pages. Enable it and set your source branch to the branch you force pushed to, for example \u003Ccode>gh-pages\u003C/code>.\u003C/p>\n\u003Ch2>Step 5 - automating everything 😴\u003C/h2>\n\u003Cp>If you don't mind doing this whole process (Step 2 and 3) every time you want to deploy you can stop now. If you're as lazy as me, here is the script I use to deploy with one command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\"># deploy.sh\n\n#!/usr/bin/env sh\n\n# abort on errors\nset -e\n\n# build\necho Linting..\nnpm run lint\necho Building. this may take a minute...\nnpm run build\n\n# navigate into the build output directory\ncd dist\n\n# if you are deploying to a custom domain\n# echo 'example.com' > CNAME\n\necho Deploying..\ngit init\ngit add -A\ngit commit -m 'deploy'\n\n# deploy\ngit push -f git@github.com:<username>/<repo>.git <branch>\n\ncd -\n\n\u003C/code>\u003C/pre>\n\u003Cp>If your on windows look into the Windows Subsystem for Linus (WSL) it will be worth it.\u003C/p>\n\u003Cp>If you are still reading, thank you very much. This is actually my first article and I'm really happy to hear about any opinions and criticisms.\nHappy Coding ♥\u003C/p>","blog/2019-05-vue-on-github-pages","96054292-eb45-4d8a-9aca-bb050175ff2a","How I use Vue.js on GitHub Pages",true,"How to properly deploy a Vue.js app on GitHub Pages",[10,11,12,13],"GitHub Pages","Vue.js","Javascript","dev",["Date","2019-05-04T00:00:00.000Z"],"/assets/2019-05-vue-on-github-pages.png","\u003Cp>I recently read the Article \u003Ca href=\"https://blog.usmanity.com/serving-vue-js-apps-on-github-pages/\">Serving Vue.js apps on GitHub Pages\u003C/a> and it inspired me to write about what I'm doing differently.\u003C/p>",[13,18,19,20],"github-pages","javascript","vue.js","article","blog",[],"2023-09-02T19:26:59Z",{"tag":20}],"uses":{"params":["slug"]}}]} diff --git a/tags/vue/__data.json b/tags/vue/__data.json new file mode 100644 index 00000000..bc5c2dad --- /dev/null +++ b/tags/vue/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":30},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":19,"abstract":23,"tags":24,"type":25,"cover_image":-1,"description":26,"folder":27,"comments":28,"latestComment":29},"\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\" rel=\"nofollow noopener noreferrer\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>\n\u003Cp>The web app allows swim instructors to track students attendance, rate their progress for objectives and provide written feedback to the parents.\nThe admin page has functionality for importing and exporting students, lessons, practice objectives as well as pdf documents suited for distribution to customers. The app is currently in use by over 10 swim instructors and back office admins.\u003C/p>\n\u003Cp>\u003Cimg src=\"/assets/aqualetics-coach-screenshot.png\" alt=\"Screenshot of the coaches view\">\u003C/p>\n\u003Cp>The app is built using a Node.js, PostgreSQL, Hasura and Vue.js tech stack and runs in docker containers. The project started without Hasura and the API was manually built in node. Fortunately Hasura provides most of that functionality out of the box, so I was able to replace 90% of the backend code with it.\u003C/p>","projects/aqualetics-coach","3210d289-1f9e-41b5-b1f9-d20f00f6a0c5","Aqualetics Coach",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14,15,16,17,18],"node","vue","graphql","hasura","postgresql","docker","dev",[20,21,22],"\u003Cp>\u003Ca href=\"https://sundrbi.ch/coach-application/\" rel=\"nofollow noopener noreferrer\">Overview\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch/2019/09/15/schwimmcoach-applikation-innovation/\" rel=\"nofollow noopener noreferrer\">Blog Post 🇩🇪\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://aqualetics.ch\" rel=\"nofollow noopener noreferrer\">Aqualetics Swim School\u003C/a>\u003C/p>","\u003Cp>An internal web app for swim schools. Developed specifically for the \"Kids\" program of \u003Ca href=\"https://www.swiss-aquatics.ch/sport-fuer-alle/kids-learn-to-swim/ausbildungssystem/\">Swiss Aquatics\u003C/a>. Live in production at the Aqualetics swim school since August 2019.\u003C/p>",[18,17,14,15,12,16,13],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":13}],"uses":{"params":["slug"]}}]} diff --git a/tags/web-api.html b/tags/web-api.html new file mode 100644 index 00000000..f964dd8c --- /dev/null +++ b/tags/web-api.html @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️web-api - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: web-api

+ +

First Go Project: A Jam-stack Commenting API

+ 7/12/2022 +
First Go Project: A Jam-stack Commenting API +

I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/web-api/__data.json b/tags/web-api/__data.json new file mode 100644 index 00000000..d5e1f96c --- /dev/null +++ b/tags/web-api/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":94},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":12,"description":13,"cover_image":14,"content_tags":15,"abstract":21,"tags":22,"links":-1,"type":23,"folder":24,"comments":25,"latestComment":93},"\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\" rel=\"nofollow noopener noreferrer\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>\n\u003Cp>I kept my eyes open for a service that I liked, but I did not want to just integrate any old service into my website, I did have some requirements:\u003C/p>\n\u003Cul>\n\u003Cli>The service should not cost anything. I would rather host something myself than sign up for another subscription (because I'm already paying for a VPS anyway).\u003C/li>\n\u003Cli>I want to control how the comments on my website are displayed. I quite like my website design and I don't want a generic comment box below my posts.\u003C/li>\n\u003Cli>The service should respect the privacy of the people using my website.\u003C/li>\n\u003Cli>There should be an option to comment without setting up an account with the service.\u003C/li>\n\u003C/ul>\n\u003Cp>While looking around for how other people integrated comments into their static websites, I found a nice \u003Ca href=\"https://averagelinuxuser.com/static-website-commenting/\" rel=\"nofollow noopener noreferrer\">blog post from Average Linux User\u003C/a> which compares a few popular commenting systems.\nUnfortunately, most systems either are not very privacy-friendly, cost money or store the comments as comments on Github issues..?\nAfter looking through the options I decided to use this opportunity to write my own commenting system and dabble with the Go programming language.\u003C/p>\n\u003Ch2>Writing a commenting API in Go\u003C/h2>\n\u003Cp>First thing first, if you want to take a look at the code, check out the \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">Github repo\u003C/a>.\u003C/p>\n\u003Cp>I decided to write the commenting system in Go because I have been looking for an excuse to practice Go for a while, and this seemed like the perfect fit. It is a small CRUD app, consisting of a storage component, an API component and a small event component in the middle to easily compose the functionality I want.\u003C/p>\n\u003Cp>Currently, it supports the following functionality:\u003C/p>\n\u003Cul>\n\u003Cli>Listing all comments (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Listing all comments for a specified page (optionally since a specified timestamp)\u003C/li>\n\u003Cli>Posting comments through the API\u003C/li>\n\u003Cli>A simple admin dashboard that lists all comments and allows the admin to delete them\u003C/li>\n\u003Cli>Email notifications when someone comments\u003C/li>\n\u003Cli>Email notifications when someone replies to your comment\u003C/li>\n\u003Cli>SQLite storage for comments\u003C/li>\n\u003C/ul>\n\u003Cp>The code is built in a way to make it easy to customise the features.\nFor example to disable features like the email reply notifications you can just \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/main.go#L52\" rel=\"nofollow noopener noreferrer\">comment out the line in the main.go\u003C/a> file that registers that hook.\u003C/p>\n\u003Cp>To write custom hooks that get executed when a new comment gets submitted or one gets deleted, just implement the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/event/handler.go\" rel=\"nofollow noopener noreferrer\">Handler\u003C/a> interface and register it in the main method.\u003C/p>\n\u003Cp>You can also easily add other storage options like databases or file storage by implementing the \u003Ca href=\"https://github.com/Tiim/IndieGo/blob/master/model/store.go\" rel=\"nofollow noopener noreferrer\">Store and SubscribtionStore\u003C/a> interfaces.\u003C/p>\n\u003Ch2>Can it be used in production? 🚗💨\u003C/h2>\n\u003Cp>I currently use it on this website! Go test it out (I might delete the comments if they are rude though 🤔).\u003C/p>\n\u003Cp>In all seriousness, I would not use it for a website where the comments are critical. But for a personal blog or similar, I don't see why not.\u003C/p>\n\u003Cp>If you want to host your own version, there is a Dockerfile available. If you decide to integrate this into your website, please comment below, ping me \u003Ca href=\"https://twitter.com/TiimB\" rel=\"nofollow noopener noreferrer\">@TiimB\u003C/a> or shoot me an email \u003Ca href=\"mailto:hey@tiim.ch\">hey@tiim.ch\u003C/a>, I would love to check it out.\u003C/p>","blog/2022-07-12-first-go-project-commenting-api","bff14052-4f3f-4dcb-bcee-155ae1c6b09e",["Date","2022-07-12T00:00:00.000Z"],["Date","2022-07-08T16:24:37.766Z"],[9],null,"First Go Project: A Jam-stack Commenting API",true,["Date","2022-11-23T21:42:29.000Z"],"I built my first project using the Go programming language: A commenting API for the jam-stack. It is simple but easily extensible. And it powers the commenting feature of this website!","/assets/2022-07-first-go-project-commenting-api.png",[16,17,18,19,20],"go","web-api","project","tiim.ch","indiego","\u003Cp>I recently have been looking around for a simple commenting system to integrate into my website. Since my website is a pre-rendered static Html site hosted on \u003Ca href=\"https://pages.github.com\">Github Pages\u003C/a>, there is no way for it to directly store comments because it does not have a database. The only option for dynamic content to be stored is with an external service.\u003C/p>",[16,20,18,19,17],"article","blog",[26,34,41,48,53,59,64,69,76,82,87],{"id":27,"type":28,"replyTo":29,"timestamp":30,"page":4,"url":31,"content":32,"name":33},"31ec5f44-15b2-498a-890d-350e38b9a83e","webmention","","2023-08-02T09:10:04Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...","Tim Bachmann",{"id":35,"type":36,"replyTo":29,"timestamp":37,"page":4,"url":38,"content":39,"name":40},"6d792d24-ba58-4408-83a4-3583667ff4ad","comment","2023-07-09T19:25:02Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#6d792d24-ba58-4408-83a4-3583667ff4ad","Heya just saw your post on Reddit about this comment feature, didn't want to leave without using it ^^. Nicely done!","Anonymous",{"id":42,"type":36,"replyTo":43,"timestamp":44,"page":4,"url":45,"content":46,"name":47},"99dd9ccf-5349-4f41-9553-67986e1a1074","1c8ba0da-10df-4a7a-b067-55875441de2d","2022-12-07T23:01:37Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#99dd9ccf-5349-4f41-9553-67986e1a1074","You are right, there is not any documentation in the readme yet. Although hopefully, I will work on that soon. I'm in the middle of refactoring the project.\n\nTo query the comments there are two rest endpoints [\"/comment\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L41-L71) and [\"/comment/:page\"](https://github.com/Tiim/IndieGo/blob/044b58e96dae112ceaca509f8541c84db3ef50f3/api/comment.go#L73-L107) which return all comments or the comments for a specific page. The comments are loaded from this API endpoint when the site is generated.\n\nTo display the comments without rebuilding the site, new comments are fetched in the browser with the \"?since=\u003Ctime-of-last-build>\" query parameter.","Tiim",{"id":43,"type":36,"replyTo":29,"timestamp":49,"page":4,"url":50,"content":51,"name":52},"2022-12-07T22:33:44Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#1c8ba0da-10df-4a7a-b067-55875441de2d","Good stuff. Always nice to see a site supporting comments and/or Webmentions. Maybe I missed it in the readme but I am curious as to how one queries the API for comments. Do you pull in the comments from the database when you generate the site?","Poorchop",{"id":54,"type":28,"replyTo":29,"timestamp":55,"page":4,"url":56,"content":57,"name":58},"e42756e4-d5a5-4727-8c58-434d285b7ab3","2022-11-27T22:31:58Z","https://brid.gy/repost/twitter/tiimb/1546801590593638400/1546801615264415745","I published a new blog post:\nFirst Go Project: A jam-stack Commenting API\ntiim.ch/blog/2022-07-1…\n#golang #jamstack #API","Golang Bot",{"id":60,"type":28,"replyTo":29,"timestamp":61,"page":4,"url":62,"content":63,"name":33},"b8d3ae8b-0059-4379-8c35-30c97269908f","2022-11-21T22:19:23Z","https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","This site now supports sending and receiving webmentions and surfacing structured data using microformats2.",{"id":65,"type":36,"replyTo":29,"timestamp":66,"page":4,"url":67,"content":68,"name":68},"171e3444-f1d0-492d-8bc7-c0a133a41783","2022-07-18T08:44:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#171e3444-f1d0-492d-8bc7-c0a133a41783","hola",{"id":70,"type":36,"replyTo":71,"timestamp":72,"page":4,"url":73,"content":74,"name":75},"5875bba1-e69b-467a-bab5-23ef4160d257","621574fd-eea2-48d6-87c8-aebd0f05f1aa","2022-07-13T21:06:11Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#5875bba1-e69b-467a-bab5-23ef4160d257","And a polite reply","polite",{"id":77,"type":36,"replyTo":29,"timestamp":78,"page":4,"url":79,"content":80,"name":81},"8494c653-ef37-47a2-ae1b-00d00e4815a9","2022-07-13T18:31:31Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#8494c653-ef37-47a2-ae1b-00d00e4815a9","Pretty cool dudez","somGuy",{"id":71,"type":36,"replyTo":29,"timestamp":83,"page":4,"url":84,"content":85,"name":86},"2022-07-12T21:40:39Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#621574fd-eea2-48d6-87c8-aebd0f05f1aa","This is a rude comment ;)","rude",{"id":88,"type":36,"replyTo":29,"timestamp":89,"page":4,"url":90,"content":91,"name":92},"fb6278ae-e48c-4397-ba29-bec4e5cb3a57","2022-07-12T13:30:14Z","https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api#fb6278ae-e48c-4397-ba29-bec4e5cb3a57","Good job dude!","wdup","2023-09-02T19:26:59Z",{"tag":17}],"uses":{"params":["slug"]}}]} diff --git a/tags/webmentions.html b/tags/webmentions.html new file mode 100644 index 00000000..37b2b59d --- /dev/null +++ b/tags/webmentions.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️webmentions - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: webmentions

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/webmentions/__data.json b/tags/webmentions/__data.json new file mode 100644 index 00000000..0069c4ba --- /dev/null +++ b/tags/webmentions/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":77},[2],{"html":3,"slug":4,"uuid":5,"date":6,"aliases":7,"title":9,"published":10,"modified":11,"description":12,"cover_image":13,"content_tags":14,"syndication":21,"abstract":23,"tags":24,"links":-1,"type":27,"folder":28,"comments":29,"latestComment":34},"\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\" rel=\"nofollow noopener noreferrer\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\" rel=\"nofollow noopener noreferrer\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>\n\u003Ch2>The IndieWeb\u003C/h2>\n\u003Cp>I will start with a brief introduction for the uninitiated. If you have already heard about the IndieWeb, feel free to skip to the next section.\u003C/p>\n\u003Cp>The IndieWeb is a collection of standards, intending to make the web social, without the user giving up ownership of their data. While on social media platforms (or as called in IndieWeb terms: silos) you can easily communicate with others, you are always subject to the whims of those platforms.\u003C/p>\n\u003Cp>The IndieWeb wants to solve this by defining standards that, once implemented in a website, allow it to communicate with other websites that are also part of the IndieWeb.\u003C/p>\n\u003Cp>The most important concept of the IndieWeb is, you have control over your data. All of your shared data lives on a domain you control.\u003C/p>\n\u003Cp>Some of the standards in the IndieWeb include:\u003C/p>\n\u003Cul>\n\u003Cli>Microformats2: a way to add structured data to the HTML source code of a website so machines can interpret the data.\u003C/li>\n\u003Cli>Webmentions: a simple communication protocol between websites. It can be used to show comments, likes, bookmarks and more on one website, while the data stays on another website.\u003C/li>\n\u003Cli>IndieAuth, an OAuth2-based way to log in using only your domain name.\u003C/li>\n\u003C/ul>\n\u003Ch2>The implementation on my website\u003C/h2>\n\u003Cp>As explained in my earlier post \u003Ca href=\"https://tiim.ch/blog/2022-07-12-first-go-project-commenting-api\" rel=\"nofollow noopener noreferrer\">First Go Project: A Jam-stack Commenting API\u003C/a>, my website is a statically built SvelteKit app hosted on GitHub Pages. This means the most important part of the IndieWeb is already implemented: I own this domain and post my content here.\u003C/p>\n\u003Ch3>Making the website machine-readable with Microformats\u003C/h3>\n\u003Cp>As mentioned above, the microformats2 standard allows websites to encode data about the page in a machine-readable format. This is accomplished by annotating HTML elements with some predefined class names. For example, the microformat for a blog post, note and other content is called \u003Ca href=\"http://microformats.org/wiki/h-entry\" rel=\"nofollow noopener noreferrer\">h-entry\u003C/a>. By adding the \u003Ccode>h-entry\u003C/code> class to a div, its content is marked as belonging to that post. Children of this div can in turn have other microformat elements such as \u003Ccode>p-name\u003C/code>, \u003Ccode>p-author\u003C/code> or \u003Ccode>dt-published\u003C/code>.\u003C/p>\n\u003Cp>While these CSS classes make the data machine-interpretable, the same data is still available to the user. There is no duplication like for example the meta tags in OpenGraph.\u003C/p>\n\u003Cp>Since my page is a custom SvelteKit app, it was easy enough to add the CSS classes to the right places. I even took the opportunity to add some more information to the pages, like the author card you see if you scroll to the bottom of this post.\u003C/p>\n\u003Ch3>Accepting comments and other interactions via Webmentions\u003C/h3>\n\u003Cp>The standard I wanted to play around with the most are webmentions. A webmention is a sort of notification sent from one website A to another website B, telling B that A has a page linking to it.\u003C/p>\n\u003Cp>In the IndieWeb all types of interactions are just web pages. The microformats2 specification for example allows replies, quotes, likes, bookmarks and many other types of interactions. The receiver of the webmention is free to extract any relevant information from the sender page and might display it, for example as a comment.\u003C/p>\n\u003Cp>Since I already have a \u003Ca href=\"https://github.com/Tiim/IndieGo\" rel=\"nofollow noopener noreferrer\">small custom service\u003C/a> running for the comment section on this site, I decided to add support to it for receiving webmentions. I refactored the comment system quite a bit to make it more modular and extendable, to allow me to add webmentions\u003C/p>\n\u003Cp>It currently supports all the required and some optional features for receiving webmentions: The first thing it does is validate the mention. A mention is only valid if the source and target URLs are valid and if the page from the source URL links to the target URL. The next step is extracting some microformat content from the source URL and saving it to the database.\nI found some things unexpectedly tricky to implement: for example, a repeated webmention with the same source URL should update the previously saved webmention if the link to the target page is still there, but delete the webmention if the link was removed.\u003C/p>\n\u003Cp>I have tested my webmentions implementation using \u003Ca href=\"https://webmention.rocks\" rel=\"nofollow noopener noreferrer\">webmention.rocks\u003C/a>, but I would appreciate it if you left me a mention as well 😃\u003C/p>\n\u003Ch3>Publishing short-form content such as replies, likes and bookmarks: A notes post type\u003C/h3>\n\u003Cp>The next thing I wanted to add to my website was sending webmentions. But before I implemented that, I wanted a way to publish short content without spamming my blog feed. For this, I created a new post type called \u003Ca href=\"https://tiim.ch/mf2\" rel=\"nofollow noopener noreferrer\">notes\u003C/a>. The list of notes lives on the /mf2 page because I plan to mostly use it to publish notes that contain microformats2 classes such as replies and likes. Another reason I didn't want to make it accessible as the /notes page is that I plan to publish my Zettelkasten notes eventually, but this is a story for another post.\u003C/p>\n\u003Cp>I also used the opportunity to add an RSS feed for all my posts, pages, projects, and notes: \u003Ca href=\"https://tiim.ch/full-rss.xml\" rel=\"nofollow noopener noreferrer\">full-rss.xml\u003C/a>. I do not recommend you subscribe to it unless you are curious about all changes to the content on my website.\u003C/p>\n\u003Ch3>Notifying referenced websites: Sending Webmentions\u003C/h3>\n\u003Cp>Sending webmentions was easy compared to receiving webmentions:\u003C/p>\n\u003Cp>On a regular interval (and on page builds), the server loads the full RSS feed and checks what items have a newer timestamp than the last time. It then extracts a list of all URLs from that feed item and loads the list of URLs that it extracted last time. Then a webmention is sent to all the URLs.\u003C/p>\n\u003Cp>Luckily I did not have to implement any of this myself apart from some glue code to fit it together: I used the library \u003Ca href=\"https://github.com/go-co-op/gocron\" rel=\"nofollow noopener noreferrer\">gocron\u003C/a> for scheduling the regular intervals, \u003Ca href=\"https://github.com/mmcdole/gofeed\" rel=\"nofollow noopener noreferrer\">gofeed\u003C/a> for parsing the RSS feed and \u003Ca href=\"https://willnorris.com/go/webmention\" rel=\"nofollow noopener noreferrer\">webmention\u003C/a> for extracting links and sending webmentions.\u003C/p>\n\u003Ch3>In the future: IndieAuth\u003C/h3>\n\u003Cp>The next thing on my roadmap is implementing IndieAuth. Although not because I have a real use case for it, but because I'm interested in OAuth, the underlying standard, and this seems like a good opportunity to get a deeper understanding of the protocol.\u003C/p>\n\u003Cp>Although, before I start implementing the next things, I should probably focus on writing blog posts first. There is no use in the most advanced blogging system if I can't be bothered to write anything.\u003C/p>\u003Cdiv class=\"mf2\">\u003Cblockquote class=\"syndication\">This post is also on \u003Cul>\u003Cli>\u003Ca class=\"u-syndication\" href=\"https://news.indieweb.org/en\">news.indieweb.org\u003C/a>\u003C/li>\u003C/ul>\u003C/blockquote>\u003C/div>\n","blog/2022-12-indiewebifying-my-website-part-1","3b342241-c414-4670-bd22-03e13d6531b7",["Date","2022-11-12T10:55:14.000Z"],[8],null,"IndieWebifying my Website Part 1 - Microformats and Webmentions",true,["Date","2022-12-03T20:56:54.000Z"],"This site now supports sending and receiving webmentions and surfacing structured data using microformats2.","https://i.imgur.com/FpgIBxI.jpg",[15,16,17,18,19,20],"IndieWeb","Webmentions","mf2","tiim.ch","go","indiego",[22],"https://news.indieweb.org/en","\u003Cp>A few weeks ago, I stumbled on one of \u003Ca href=\"https://www.jvt.me/posts/2019/08/21/rsvp-from-your-website/\">Jamie Tanna's blog posts about microformats2\u003C/a> by accident. That is when I first learned about the wonderful world of the \u003Ca href=\"https://indieweb.org/why\">IndieWeb\u003C/a>. It took me a while to read through some of the concepts of the IndieWeb like webmentions, IndieAuth, microformats and all the other standards, but the more I found out about it the more I wanted to play around with it. And what better place to try out new technology than on a personal website?\u003C/p>",[19,20,25,17,18,26],"indieweb","webmentions","article","blog",[30,37,43,48,54,59,65,71],{"id":31,"type":32,"replyTo":33,"timestamp":34,"page":4,"url":35,"content":33,"name":36},"41435fdd-5fd2-4175-b9ea-ef9ce0dec154","webmention","","2023-09-02T19:26:59Z","https://evgenykuznetsov.org/en/reactions/2022/like-337215655/","Evgeny Kuznetsov",{"id":38,"type":32,"replyTo":33,"timestamp":39,"page":4,"url":40,"content":41,"name":42},"68bd3601-4f00-42c1-b28c-1f2ef75ac851","2023-08-02T09:10:03Z","https://tiim.ch/projects/indiego","I blogged about creating a comment system for my website a while ago,\nand later how I implemented webmentions into that same project.\nSince then this little go program has grown quite a bit, and it has turned into a modular platform\nthat supports quite a few technologies:...","Tim Bachmann",{"id":44,"type":32,"replyTo":33,"timestamp":45,"page":4,"url":46,"content":47,"name":42},"5f508a42-8b83-4c10-9f7e-9c1b80e23ab1","2022-12-09T10:49:06Z","https://tiim.ch/blog/2022-12-storj-cloudflare-image-hosting","Learn how to setup affordable image hosting for your personal website with Storj.io and Cloudflare.",{"id":49,"type":32,"replyTo":33,"timestamp":50,"page":4,"url":51,"content":52,"name":53},"dc8dbf30-ff1f-4e13-ba36-37f12666005c","2022-12-05T08:41:07Z","https://martymcgui.re/2022/12/05/033926/","★ Liked https://tiim.ch/blog/2022-12-indiewebifying-my-website-part-1","https://martymcgui.re/",{"id":55,"type":32,"replyTo":33,"timestamp":56,"page":4,"url":57,"content":33,"name":58},"6f6c1f11-2ae0-41a4-b7d0-e343ef63aa52","2022-11-27T22:32:27Z","https://brid.gy/like/twitter/tiimb/1591417020557525003/48372745","Jimmy Lipham",{"id":60,"type":32,"replyTo":33,"timestamp":61,"page":4,"url":62,"content":63,"name":64},"5e1c4149-8fb9-48fb-b285-5efbd626b259","2022-11-27T22:31:57Z","https://brid.gy/repost/twitter/tiimb/1591417020557525003/1591418692008558592","I published a new blog post:\nIndieWebifying my Website Part 1 - Microformats and Webmentions\ntiim.ch/blog/2022-12-i…\n#indieweb #microformats #webmentions #golang","Golang Smart Bot",{"id":66,"type":32,"replyTo":33,"timestamp":67,"page":4,"url":68,"content":69,"name":70},"6e0bf830-1735-42a2-9aa2-ea4c40ab7a45","2022-11-15T12:47:35Z","https://webmention.rocks/receive/1/f0fa5421056e068fe902932ef98f6d71","This test verifies that you accept a Webmention request that contains a valid source and target URL. To pass this test, your Webmention endpoint must return either HTTP 200, 201 or 202 along with the appropriate headers.\nIf your endpoint returns HTTP 201, then it MUST also return a Location header. If it returns HTTP 200 or 202, then it MUST NOT include a Location header.","Webmention Rocks!",{"id":72,"type":32,"replyTo":33,"timestamp":73,"page":4,"url":74,"content":75,"name":76},"4e237d08-9f22-480e-a46e-8f40adf06c5e","2022-11-13T08:34:12Z","https://www.jvt.me/mf2/2022/11/rm8as/","Liked\nIndieWebifying my Website Part 1 - Microformats and Webmentions\nPost detailsThis site now supports sending and receiving webmentions and surfacing structured data using microformats2. https://i.imgur.com/FpgIBxI.jpg","Jamie Tanna",{"tag":26}],"uses":{"params":["slug"]}}]} diff --git a/tags/weechat.html b/tags/weechat.html new file mode 100644 index 00000000..33e8f44b --- /dev/null +++ b/tags/weechat.html @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️weechat - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: weechat

+ +

Weechat Notifications with ntfy.sh

+ 3/28/2023 +
Weechat Notifications with ntfy.sh +

Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/weechat/__data.json b/tags/weechat/__data.json new file mode 100644 index 00000000..27159072 --- /dev/null +++ b/tags/weechat/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":66},[2,26],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\" rel=\"nofollow noopener noreferrer\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\" rel=\"nofollow noopener noreferrer\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>\n\u003Cp>Also, I recently found the web service \u003Ca href=\"https://ntfy.sh\" rel=\"nofollow noopener noreferrer\">ntfy.sh\u003C/a>. It sends push notifications whenever you send an HTTP post request to a certain URL. I already have ntfy.sh installed on my android phone, and I also found a minimal and lightweight \u003Ca href=\"https://github.com/lucas-bortoli/ntfysh-windows\" rel=\"nofollow noopener noreferrer\">desktop client\u003C/a>.\u003C/p>\n\u003Cp>I managed to set a WeeChat trigger up that fires every time I get mentioned (highlighted in WeeChat terminology), and a trigger that fires every time I get a private message. Both of those triggers execute the \u003Ccode>/exec\u003C/code> command which runs an arbitrary shell command. The exec command runs the \u003Ccode>wget\u003C/code> program to send a post request to the ntfy.sh server, which in turn sends a notification to all apps that subscribe to the same URL as the post request was sent. I would usually use the curl program for this instead of wget, but the docker default docker image doesn't contain a curl install.\u003C/p>\n\u003Cp>Here you can see the two \u003Ccode>/trigger\u003C/code> commands:\u003C/p>\n\u003Cp>\u003Cem>trigger on mention\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode>/trigger addreplace notify_highlight print '' '${tg_highlight}' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"- -header=Title: New highlight: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cem>trigger on private message\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-weechat\">/trigger addreplace notify_privmsg print '' '${tg_tag_notify} == private && ${buffer.notify} > 0' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"--header=Title: New private message: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Ch2>The trigger commands in detail\u003C/h2>\n\u003Cp>In case you don't just want to copy and paste some random command from the internet into your WeeChat (which you shouldn't do anyway), I will try to explain the trigger command that fires when you get mentioned in a message:\u003C/p>\n\u003Cp>Let's first look at the trigger command itself:\n\u003Ccode>/trigger addreplace <name> <hook> <argument> <condition> <variable-replace> <command>\u003C/code>\nWe call the \u003Ccode>/trigger\u003C/code> command with the \u003Ccode>addreplace\u003C/code> subcommand. This subcommand will either register a new trigger or replace it if one with the same name already exists.\u003C/p>\n\u003Cul>\n\u003Cli>\u003Ccode>name\u003C/code> - This argument is self-explanatory, the name of the trigger. In our case I called it \u003Ccode>notify_highlight\u003C/code>, but you could call it whatever you want.\u003C/li>\n\u003Cli>\u003Ccode>hook\u003C/code> - This argument specifies which hook or event the trigger should listen for. WeeChat is built as an event-driven platform, so pretty much anything from mouse movements to IRC messages are handled via events. In this case, we want to trigger on the \u003Ccode>print\u003C/code> event, which is fired every time a new message gets received from IRC.\u003C/li>\n\u003Cli>\u003Ccode>argument\u003C/code> - The argument is needed for some hooks, but not for the \u003Ccode>print\u003C/code> hook, so we are going to ignore that one for now and just set it to an empty string \u003Ccode>''\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>condition\u003C/code> - The condition must evaluate to \u003Ccode>true\u003C/code> for the trigger to fire. This is helpful because the \u003Ccode>print\u003C/code> trigger fires for every new message, but we only want to be notified when the new message mentions our nick. The condition for this is \u003Ccode>${tg_highlight}\u003C/code>. You can find the list of variables that you can access with the command \u003Ccode>/trigger monitor\u003C/code>, which prints all variables for every trigger that gets executed.\u003C/li>\n\u003Cli>\u003Ccode>variable-replace\u003C/code> - This took me a while to understand. This command is used to manipulate data and save it to a variable. The syntax is inspired by the sed command. Explaining it fully is out of the scope of this blog post, but you can take a look at the \u003Ca href=\"https://weechat.org/files/doc/devel/weechat_user.en.html#trigger_regex\" rel=\"nofollow noopener noreferrer\">docs\u003C/a>. In our example, we replace the whole content of the variable \u003Ccode>tg_message\u003C/code> with the format string \u003Ccode>${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}\u003C/code> which results in a sting like \u003Ccode><tiim> Hello world!\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>command\u003C/code> - The last argument is the command that gets executed whenever this trigger fires. In our case, we use the \u003Ccode>/execute\u003C/code> command, which starts the wget command which in turn sends a post request to ntfy.sh. Make sure you set the ntfy topic (the part after \u003Ccode>https://ntfy.sh/\u003C/code>) to something private and long enough so that nobody else is going to guess it by accident.\u003C/li>\n\u003C/ul>\n\u003Cp>Don't forget to subscribe to the ntfy topic on your phone or whatever device you want to receive the notification on.\u003C/p>\n\u003Cp>The possibilities with the trigger plugin are endless, I hope this inspires you to build your own customizations using weechat.\u003C/p>","blog/2023-03-28-weechat-notification-ntfy","ef51e944-86fa-44b0-ab9d-be7f8d8e569a",["Date","2023-03-28T10:05:19.000Z"],["Date","2023-01-15T20:35:40.643Z"],[9],null,"Weechat Notifications with ntfy.sh",true,"Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.","https://media.tiim.ch/97833b1d-d602-4d9a-9689-3077e96e45ba.webp","stable diffusion - Anything V3.0 - boy using an old DOS computer, 90s vibes, muted pastel colors, stylized, thick lines, IRC, console",[16,17,18,19],"weechat","ntfy.sh","wget","irc","\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>",[19,17,16,18],"article","blog",[],"2023-09-02T19:26:59Z",{"html":27,"slug":28,"uuid":29,"date":30,"created":31,"aliases":32,"title":33,"published":11,"modified":34,"description":35,"cover_image":36,"cover_image_txt":37,"content_tags":38,"abstract":40,"tags":41,"links":-1,"type":22,"folder":23,"comments":42,"latestComment":25},"\u003Cp>I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used \u003Ca href=\"https://hexchat.github.io/\" rel=\"nofollow noopener noreferrer\">HexChat\u003C/a> in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.\u003C/p>\n\u003Cp>The one client I have seen mentioned a lot is \u003Ca href=\"https://weechat.org/\" rel=\"nofollow noopener noreferrer\">WeeChat\u003C/a> (not to be confused with WeChat, the Chinese instant messenger). WeeChat runs in the terminal as a \u003Ca href=\"https://en.wikipedia.org/wiki/Text-based_user_interface\" rel=\"nofollow noopener noreferrer\">TUI\u003C/a> and after a while of getting used to (and after enabling 'mouse mode') it seems intuitive enough.\u003C/p>\n\u003Cp>The nice thing about WeeChat running not as a graphical application, is that it makes it possible to run on a server and access it from anywhere over ssh.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>INFO\u003C/span>\u003Cp>Except on mobile devices, but weechat has mobile apps that can connect to it directly.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Since I pretty much host all my selfhosted software in docker on a VPS, I was looking if someone already published a docker image for WeeChat. There is a bunch of them, but only \u003Ca href=\"https://hub.docker.com/r/weechat/weechat\" rel=\"nofollow noopener noreferrer\">weechat/weechat\u003C/a> (the official image) is still updated regularly. The docker hub page does not have any documentation, but I managed to find it in the \u003Ca href=\"https://github.com/weechat/weechat-container\" rel=\"nofollow noopener noreferrer\">weechat/weechat-container\u003C/a> github repo.\u003C/p>\n\u003Cp>As it says in the readme on github, you can start the container with\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">docker run -it weechat/weechat\n\u003C/code>\u003C/pre>\n\u003Cp>which will run weechat directly in the foreground.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Info\u003C/span>\u003Cp>Don't skip the \u003Ccode>-it\u003C/code> command line flags. The \u003Ccode>-i\u003C/code> or \u003Ccode>--interactive\u003C/code> keeps stdin open, which is required to send input to weechat. Weechat also closes immediately if the stdin gets closed, which took me a while to figure out.\nThe \u003Ccode>-t\u003C/code> or \u003Ccode>--tty\u003C/code> flag is required to provide a fake tty to the container. I don't really understand what that means but without this you won't see the user interface of weechat.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Running in the foreground is not really that helpful if we want to run weechat on a server, so we need to detach (let it run in the background) from the container with the \u003Ccode>-d\u003C/code> or \u003Ccode>--detach\u003C/code> flag. It also helps to specify a name for the container with the \u003Ccode>--name <name>\u003C/code> argument, so we can quickly find the container again later. The docker command now looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">docker run -it -d --name weechat weechat/weechat\n\u003C/code>\u003C/pre>\n\u003Cp>When we run this command, we will notice that weechat is running in the background. To access it we can run \u003Ccode>docker attach weechat\u003C/code>. To detach from weechat without exiting the container, we can press \u003Ccode>CTRL-p CTRL-q\u003C/code> as described in the \u003Ca href=\"https://docs.docker.com/engine/reference/commandline/attach/#description\" rel=\"nofollow noopener noreferrer\">docker attach reference\u003C/a>\u003C/p>\n\u003Cp>I noticed that there are two versions of the weechat image: a debian version and an alpine linux version. Generally the Alpine Linux versions of containers are smaller than the Debian versions, so I decided to use the alpine version: \u003Ccode>weechat/weechat:latest-alpine\u003C/code>.\u003C/p>\n\u003Cp>With this we are practically done, but if we ever remove and restart the container, all of the chat logs and customisations to weechat will be gone. To prevent this we need to add the config and log files to a volume.\u003C/p>\n\u003Cp>I generally use the folder \u003Ccode>~/docker/(service)\u003C/code> to point my docker volumes to, so I have a convenient place to inspect, modify and back up the data.\u003C/p>\n\u003Cp>Let's create the folder and add the volume to the docker container. I also added the \u003Ccode>--restart unless-stopped\u003C/code> flag to make sure the container gets restarted if it either exits for some reason of if docker restarts.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">mkdir -p ~/docker/weechat/data\nmkdir -p ~/docker/weechat/config\n\ndocker run -it -d --restart unless-stopped \\\n -v \"~/docker/weechat/data:/home/user/.weechat\" \\\n -v \"~/docker/weechat/config:/home/user/.config/weechat\" \\\n --name weechat weechat/weechat:latest-alpine`\n\u003C/code>\u003C/pre>\n\u003Cp>Running this command on the server is all we need to have weechat running in docker.\u003C/p>\n\u003Cblockquote>\n\u003Cp>But how do I quickly connect to weechat? Do I always have to first ssh into the server and then run docker attach?\u003C/p>\n\u003C/blockquote>\n\u003Cp>Yes but, as almost always, we can simplify this with a bash script:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-bash\">#!/usr/bin/env bash\n\nHOST=<ssh host>\nssh -t \"${HOST}\" docker attach weechat\n\u003C/code>\u003C/pre>\n\u003Cp>This bash script starts ssh with the \u003Ccode>-t\u003C/code> flag which tells ssh that the command is interactive.\nCopy this script into your \u003Ccode>~/.local/bin\u003C/code> folder and make it executable.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">nano ~/.local/bin/weechat.sh\nchmod +x weechat.sh\n\u003C/code>\u003C/pre>\n\u003Cp>And that's it! Running \u003Ccode>weechat.sh\u003C/code> will open an ssh session to your server and attach to the weechat container. Happy Chatting!\u003C/p>\n\u003Cp>If you liked this post, consider subscribing to my blog via \u003Ca href=\"https://tiim.ch/blog/rss.xml\" rel=\"nofollow noopener noreferrer\">RSS\u003C/a>, or on \u003Ca href=\"https://tiim.ch/follow\" rel=\"nofollow noopener noreferrer\">social media\u003C/a>. If you have any questions, feel free to \u003Ca href=\"https://tiim.ch/contact\" rel=\"nofollow noopener noreferrer\">contact me\u003C/a>. I also usually hang out in \u003Ca href=\"irc://irc.libera.chat/##tiim\">\u003Ccode>##tiim\u003C/code> on irc.libera.chat\u003C/a>. My name on IRC is \u003Ccode>tiim\u003C/code>.\u003C/p>\n\u003Cblockquote class=\"callout callout-info\">\n\u003Cspan class=\"callout-title\">\u003Cspan class=\"callout-icon\">\u003Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\">\u003Cpath d=\"M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0 0 114.6 0 256s114.6 256 256 256zm-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z\">\u003C/path>\u003C/svg>\u003C/span>Update 2022-01-18\u003C/span>\u003Cp>I have found that at the beginning of a session, the input to weechat doesn't seem to work. Sometimes weechat refuses to let me type anything and/or doesn't recognize mouse events.\nAfter a while of spamming keys and \u003Ccode>Alt-m\u003C/code> (toggle mouse mode), it seems to fix itself most of the time.\nI have no idea if thats a problem with weechat, with docker or with ssh, and so far have not found a solution for this. If you have the same problem or even know how to fix it, feel free to reach out.\u003C/p>\n\u003C/blockquote>","blog/2023-01-15-weechat-docker","889ff4db-3ccb-4ab1-9676-a2b0ea8f19eb",["Date","2023-01-15T00:00:00.000Z"],["Date","2023-01-15T00:17:07.000Z"],[9],"Running the WeeChat IRC Client on a VPS in Docker",["Date","2023-01-18T11:34:27.000Z"],"Walkthrough on how to setup the WeeChat IRC client in docker.","https://media.tiim.ch/a28c65a1-ed95-43d3-af87-a2ad222bee7f.jpg","Stable Diffusion - anime landscape, pastel colors, thick outlines, forest, mountains, golden light",[19,16,39],"docker","\u003Cp>I have recently gotten interested in IRC for some reason and have been looking for a client that I like. I have used \u003Ca href=\"https://hexchat.github.io/\">HexChat\u003C/a> in the past, but I don't really fancy having yet another communications program running on my PC next to discord, zoom, telegram and thunderbird. I have been trying to use the IRC feature of thunderbird, but even though it works, it feels very much like an afterthought.\u003C/p>",[39,19,16],[43,50,55,60],{"id":44,"type":45,"replyTo":46,"timestamp":47,"page":28,"url":48,"content":12,"name":49},"52b7b3e6-e233-4379-8f0c-3332aed562a6","webmention","","2023-03-28T10:10:56Z","https://tiim.ch/blog/2023-03-28-weechat-notification-ntfy","Tim Bachmann",{"id":51,"type":45,"replyTo":46,"timestamp":52,"page":28,"url":53,"content":46,"name":54},"f6f58ebf-a68f-415e-baed-cb8bf38189fd","2023-03-02T00:05:49Z","https://brid.gy/like/twitter/tiimb/1614403118258601987/1557313575072546816","DM Cyber Security",{"id":56,"type":45,"replyTo":46,"timestamp":57,"page":28,"url":58,"content":46,"name":59},"45d40e9f-6498-4432-bdb0-01210e55d092","2023-01-25T18:20:43Z","https://brid.gy/like/twitter/tiimb/1614403118258601987/8717982","Christopher Scott",{"id":61,"type":45,"replyTo":46,"timestamp":62,"page":28,"url":63,"content":64,"name":65},"979c42d0-a8fc-4a52-a85d-bedb672fb144","2023-01-25T09:22:51Z","https://brid.gy/repost/twitter/tiimb/1614403118258601987/1618133427139792898","New blog post: A walkthrough on how to set up the WeeChat IRC client in docker. #irc #docker @WeeChatClient\ntiim.ch/blog/2023-01-1…","WeeChat",{"tag":16}],"uses":{"params":["slug"]}}]} diff --git a/tags/wget.html b/tags/wget.html new file mode 100644 index 00000000..8d07ac5a --- /dev/null +++ b/tags/wget.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️wget - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: wget

+ +

Weechat Notifications with ntfy.sh

+ 3/28/2023 +
Weechat Notifications with ntfy.sh +

Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/wget/__data.json b/tags/wget/__data.json new file mode 100644 index 00000000..d8217904 --- /dev/null +++ b/tags/wget/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\" rel=\"nofollow noopener noreferrer\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\" rel=\"nofollow noopener noreferrer\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>\n\u003Cp>Also, I recently found the web service \u003Ca href=\"https://ntfy.sh\" rel=\"nofollow noopener noreferrer\">ntfy.sh\u003C/a>. It sends push notifications whenever you send an HTTP post request to a certain URL. I already have ntfy.sh installed on my android phone, and I also found a minimal and lightweight \u003Ca href=\"https://github.com/lucas-bortoli/ntfysh-windows\" rel=\"nofollow noopener noreferrer\">desktop client\u003C/a>.\u003C/p>\n\u003Cp>I managed to set a WeeChat trigger up that fires every time I get mentioned (highlighted in WeeChat terminology), and a trigger that fires every time I get a private message. Both of those triggers execute the \u003Ccode>/exec\u003C/code> command which runs an arbitrary shell command. The exec command runs the \u003Ccode>wget\u003C/code> program to send a post request to the ntfy.sh server, which in turn sends a notification to all apps that subscribe to the same URL as the post request was sent. I would usually use the curl program for this instead of wget, but the docker default docker image doesn't contain a curl install.\u003C/p>\n\u003Cp>Here you can see the two \u003Ccode>/trigger\u003C/code> commands:\u003C/p>\n\u003Cp>\u003Cem>trigger on mention\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode>/trigger addreplace notify_highlight print '' '${tg_highlight}' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"- -header=Title: New highlight: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cem>trigger on private message\u003C/em>\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-weechat\">/trigger addreplace notify_privmsg print '' '${tg_tag_notify} == private && ${buffer.notify} > 0' '/.*/${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}/' '/exec -norc -nosw -bg wget -O- --post-data \"${tg_message}\" \"--header=Title: New private message: ${buffer.full_name}\" https://ntfy.sh/my_ntfy_topic_1234'\n\u003C/code>\u003C/pre>\n\u003Ch2>The trigger commands in detail\u003C/h2>\n\u003Cp>In case you don't just want to copy and paste some random command from the internet into your WeeChat (which you shouldn't do anyway), I will try to explain the trigger command that fires when you get mentioned in a message:\u003C/p>\n\u003Cp>Let's first look at the trigger command itself:\n\u003Ccode>/trigger addreplace <name> <hook> <argument> <condition> <variable-replace> <command>\u003C/code>\nWe call the \u003Ccode>/trigger\u003C/code> command with the \u003Ccode>addreplace\u003C/code> subcommand. This subcommand will either register a new trigger or replace it if one with the same name already exists.\u003C/p>\n\u003Cul>\n\u003Cli>\u003Ccode>name\u003C/code> - This argument is self-explanatory, the name of the trigger. In our case I called it \u003Ccode>notify_highlight\u003C/code>, but you could call it whatever you want.\u003C/li>\n\u003Cli>\u003Ccode>hook\u003C/code> - This argument specifies which hook or event the trigger should listen for. WeeChat is built as an event-driven platform, so pretty much anything from mouse movements to IRC messages are handled via events. In this case, we want to trigger on the \u003Ccode>print\u003C/code> event, which is fired every time a new message gets received from IRC.\u003C/li>\n\u003Cli>\u003Ccode>argument\u003C/code> - The argument is needed for some hooks, but not for the \u003Ccode>print\u003C/code> hook, so we are going to ignore that one for now and just set it to an empty string \u003Ccode>''\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>condition\u003C/code> - The condition must evaluate to \u003Ccode>true\u003C/code> for the trigger to fire. This is helpful because the \u003Ccode>print\u003C/code> trigger fires for every new message, but we only want to be notified when the new message mentions our nick. The condition for this is \u003Ccode>${tg_highlight}\u003C/code>. You can find the list of variables that you can access with the command \u003Ccode>/trigger monitor\u003C/code>, which prints all variables for every trigger that gets executed.\u003C/li>\n\u003Cli>\u003Ccode>variable-replace\u003C/code> - This took me a while to understand. This command is used to manipulate data and save it to a variable. The syntax is inspired by the sed command. Explaining it fully is out of the scope of this blog post, but you can take a look at the \u003Ca href=\"https://weechat.org/files/doc/devel/weechat_user.en.html#trigger_regex\" rel=\"nofollow noopener noreferrer\">docs\u003C/a>. In our example, we replace the whole content of the variable \u003Ccode>tg_message\u003C/code> with the format string \u003Ccode>${weechat.look.nick_prefix}${tg_prefix_nocolor}${weechat.look.nick_suffix} ${tg_message_nocolor}\u003C/code> which results in a sting like \u003Ccode><tiim> Hello world!\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>command\u003C/code> - The last argument is the command that gets executed whenever this trigger fires. In our case, we use the \u003Ccode>/execute\u003C/code> command, which starts the wget command which in turn sends a post request to ntfy.sh. Make sure you set the ntfy topic (the part after \u003Ccode>https://ntfy.sh/\u003C/code>) to something private and long enough so that nobody else is going to guess it by accident.\u003C/li>\n\u003C/ul>\n\u003Cp>Don't forget to subscribe to the ntfy topic on your phone or whatever device you want to receive the notification on.\u003C/p>\n\u003Cp>The possibilities with the trigger plugin are endless, I hope this inspires you to build your own customizations using weechat.\u003C/p>","blog/2023-03-28-weechat-notification-ntfy","ef51e944-86fa-44b0-ab9d-be7f8d8e569a",["Date","2023-03-28T10:05:19.000Z"],["Date","2023-01-15T20:35:40.643Z"],[9],null,"Weechat Notifications with ntfy.sh",true,"Using the weechat trigger plugin to notify yourself about new private messages and mentions through the ntfy.sh notification service.","https://media.tiim.ch/97833b1d-d602-4d9a-9689-3077e96e45ba.webp","stable diffusion - Anything V3.0 - boy using an old DOS computer, 90s vibes, muted pastel colors, stylized, thick lines, IRC, console",[16,17,18,19],"weechat","ntfy.sh","wget","irc","\u003Cp>In one of my last blog posts I \u003Ca href=\"https://tiim.ch/blog/2023-01-15-weechat-docker\">set up WeeChat in docker\u003C/a>, which works mostly pretty great for me so far. Although, it started to bug me that I felt the need to regularly check IRC in case I missed someone potentially tagging or private-messaging me. While looking around at how I could be notified on mentions and private messages, I found the \u003Ca href=\"https://weechat.org/files/doc/stable/weechat_user.en.html#trigger\">trigger plugin\u003C/a>. A powerful plugin that comes pre-installed on WeeChat. It lets the user specify a WeeChat command that will be executed when a specific event occurs. This plugin is probably powerful enough to build a small IRC bot, directly in WeeChat.\u003C/p>",[19,17,16,18],"article","blog",[],"2023-09-02T19:26:59Z",{"tag":18}],"uses":{"params":["slug"]}}]} diff --git a/tags/widget.html b/tags/widget.html new file mode 100644 index 00000000..054eb6a3 --- /dev/null +++ b/tags/widget.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️widget - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: widget

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/widget/__data.json b/tags/widget/__data.json new file mode 100644 index 00000000..b9fd2575 --- /dev/null +++ b/tags/widget/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":28},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":18,"abstract":21,"tags":22,"type":23,"cover_image":-1,"description":24,"folder":25,"comments":26,"latestComment":27},"\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>\n\u003Cp>Android widgets are handled by the operating system and only support a limited set of features for rendering.\nTo display markdown, the app displays a screenshot of a temporary web view, that displays the rendered markdown.\u003C/p>","projects/markdown-widget","c6a11779-cf98-4983-8744-9b1effae8d7a","Android Markdown Widget",["Date","2023-08-02T08:59:00.000Z"],null,"Projects",true,[12,13,14,15,16,17],"android","java","kotlin","markdown","widget","dev",[19,20],"\u003Cp>\u003Ca href=\"https://github.com/Tiim/Android-Markdown-Widget\" rel=\"nofollow noopener noreferrer\">Android Markdown Widget Github\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://f-droid.org/packages/ch.tiim.markdown_widget/\" rel=\"nofollow noopener noreferrer\">Download on F-Droid\u003C/a>\u003C/p>","\u003Cp>A seemingly simple android widget that renders a markdown file from your phone as a widget on the home screen.\u003C/p>",[12,17,13,14,15,16],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":16}],"uses":{"params":["slug"]}}]} diff --git a/tags/windows.html b/tags/windows.html new file mode 100644 index 00000000..b7fe022e --- /dev/null +++ b/tags/windows.html @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️windows - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: windows

+ +

How to set up an SSH Server on Windows with WSL

+ 3/2/2022 +
How to set up an SSH Server on Windows with WSL +

It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/windows/__data.json b/tags/windows/__data.json new file mode 100644 index 00000000..60d18970 --- /dev/null +++ b/tags/windows/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":60},[2,44],{"html":3,"slug":4,"uuid":5,"title":6,"published":7,"date":8,"description":9,"cover_image":10,"content_tags":11,"abstract":16,"tags":17,"links":-1,"type":21,"folder":22,"comments":23,"latestComment":43},"\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\" rel=\"nofollow noopener noreferrer\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\" rel=\"nofollow noopener noreferrer\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\" rel=\"nofollow noopener noreferrer\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\" rel=\"nofollow noopener noreferrer\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>\n\u003Ch2>Installing the OpenSSH Server\u003C/h2>\n\u003Cp>Windows has been shipping with an OpenSSH client and server for a long time. They are not installed by default but can be activated either in the settings as described \u003Ca href=\"https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse\" rel=\"nofollow noopener noreferrer\">in the official docs\u003C/a> or with the following PowerShell commands.\u003C/p>\n\u003Cp>\u003Cstrong>You will need to start PowerShell as Administrator\u003C/strong>\u003C/p>\n\u003Cp>First, install the OpenSSH client and server.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0\nAdd-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0\n\u003C/code>\u003C/pre>\n\u003Cp>Enable the SSH service and make sure the firewall rule is configured:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\"># Enable the service\nStart-Service sshd\nSet-Service -Name sshd -StartupType 'Automatic'\n\n# Confirm the firewall rule is configured. It should be created automatically by setup. Run the following to verify\nif (!(Get-NetFirewallRule -Name \"OpenSSH-Server-In-TCP\" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {\n Write-Output \"Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it...\"\n New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22\n} else {\n Write-Output \"Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists.\"\n}\n\u003C/code>\u003C/pre>\n\u003Cp>Congratulations, you have installed the SSH server on your Windows machine. And all without manually setting up a background service or modifying config files.\u003C/p>\n\u003Ch2>Setting WSL as Default Shell\u003C/h2>\n\u003Cp>To directly boot into WSL when connecting, we need to change the default shell from \u003Ccode>cmd.exe\u003C/code> or \u003Ccode>PowerShell.exe\u003C/code> to \u003Ccode>bash.exe\u003C/code>, which in turn runs the default WSL distribution. This can be done with the PowerShell command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-ItemProperty -Path \"HKLM:\\SOFTWARE\\OpenSSH\" -Name DefaultShell -Value \"C:\\WINDOWS\\System32\\bash.exe\" -PropertyType String -Force\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: even though the shell is running on the Linux side, the SSH server is still on windows. This means you have to use to windows username to log in, and the SCP command copies files relative to the user directory on windows.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (non-Admin User)\u003C/h2>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: If the user account has Admin permissions, read the next chapter, otherwise continue reading.\u003C/p>\n\u003Cp>Create the folder \u003Ccode>.ssh\u003C/code> in the users home directory on windows: (e.g. \u003Ccode>C:\\Users\\<username>\\.ssh\u003C/code>). Run the following commands in PowerShell (not as administrator).\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path ~\\.ssh -ItmeType \"directory\"\nNew-Item -Path ~\\.ssh\\authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>The file \u003Ccode>.ssh\\autzorized_keys\u003C/code> will contain a list of all public keys that shall be allowed to connect to the SSH server.\u003C/p>\n\u003Cp>Copy the contents of your public key file (usually stored in \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>) to the \u003Ccode>authorized_keys\u003C/code> file. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (Admin User)\u003C/h2>\n\u003Cp>If the user is in the Administrators group, it is not possible to have the \u003Ccode>authorized_keys\u003C/code> file in the user directory for security purposes.\nInstead, it needs to be located on the following path \u003Ccode>%ProgramData%\\ssh\\administrators_authorized_keys\u003C/code>. A second requirement is that it is only accessible to Administrator users, to prevent a normal user from gaining admin permissions.\u003C/p>\n\u003Cp>To create the file start PowerShell as administrator and run the following command.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path $env:programdata\\ssh\\administrators_authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>This will create the file with the correct permissions. Now open the file and paste your public key into it. The public key should be located at \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Verifying everything works\u003C/h2>\n\u003Cp>Verify that you can SSH into your machine by running the following inside WSL:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">IP=$(cat /etc/resolv.conf | grep nameserver | cut -d \" \" -f2) # get the windows host ip address\nssh <user>@$IP\n\u003C/code>\u003C/pre>\n\u003Cp>Or from PowerShell and cmd:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ssh <user>@localhost\n\u003C/code>\u003C/pre>\n\u003Ch2>Drawbacks\u003C/h2>\n\u003Cp>There are some drawbacks to this approach. If you rely on some programs or scripts to work over SSH, this might not be the method for you. Most scripts expect a unix machine on the other end, or if they expect a windows machine they will most likely not be configured to deal with WSL.\u003C/p>\n\u003Cp>If you however just want to connect to your pc to copy some files or change some settings this approach is perfectly fine.\u003C/p>","blog/2022-03-ssh-windows-wsl","03b5a86c-5f4d-4086-9f5f-e1e46b4bcf58","How to set up an SSH Server on Windows with WSL",true,["Date","2022-03-02T00:00:00.000Z"],"It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.","/assets/2022-03-ssh-windows-wsl.png",[12,13,14,15],"SSH","WSL","Windows","dev","\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>",[15,18,19,20],"ssh","windows","wsl","article","blog",[24,32,38],{"id":25,"type":26,"replyTo":27,"timestamp":28,"page":4,"url":29,"content":30,"name":31},"20f2c526-7466-4c21-83ac-51750f278328","comment","f7c1891b-e97b-4030-863e-19344ed84d32","2022-09-20T14:59:17Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#20f2c526-7466-4c21-83ac-51750f278328","Hi Tim, no problem. I had this error in \"Event Viewer > Applications and Services Logs > OpenSSH > Admin\" and figure it out that sshd seems to search the Administrators groups to operate, literal name and not properly localized by region.\n\nerroid:2 user:SYSTEM details:\"sshd: error: unable to resolve group administrators\"\n\nMaybe is not all the non-english windows with this problem, but I have it but after created the group works like a charm.","",{"id":27,"type":26,"replyTo":33,"timestamp":34,"page":4,"url":35,"content":36,"name":37},"5de01bc5-b7c7-4522-9dfe-c67f103d4c03","2022-09-20T12:58:29Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#f7c1891b-e97b-4030-863e-19344ed84d32","Hi FinderX\n\nThanks for the heads up. I don't remember having to create a new user group, even though my system language is German. Maybe I just forgot about that though.","Tim",{"id":33,"type":26,"replyTo":31,"timestamp":39,"page":4,"url":40,"content":41,"name":42},"2022-09-20T07:50:46Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#5de01bc5-b7c7-4522-9dfe-c67f103d4c03","Hi!\nI add some roundabouts about admin-users, if your windows ssh server system language is NOT english, you must create 'Administrators' group (without quotes) in your language equivalent of \"Users and Local Groups > Groups\", if your server is a DC (Domain Controller) create it in your language equivalent of \"Active Directories Users and Computers\".\n\nCreate the user group with name Administrators, description whatever, ex. \"Dummy group for sshd to work correctly.\", and in Members add your language equivalent of the user Administrator.\n\nThis is optional but I suggest you change these settings in \"%programdata%\\ssh\\sshd_config\" after you successfully copy your public key to the ssh server :\n\nStrictModes yes\nPubkeyAuthentication yes\nPasswordAuthentication no\n\nYou can see the log activity in your language equivalent of \"Applications and Services Logs > OpenSSH > Admin or Operational\"\n\nBest Regards.","FinderX","2023-09-02T19:26:59Z",{"html":45,"slug":46,"uuid":47,"title":48,"published":7,"date":49,"description":50,"cover_image":51,"content_tags":52,"abstract":57,"tags":58,"links":-1,"type":21,"folder":22,"comments":59,"latestComment":43},"\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>\n\u003Cp>\u003Cem>TLDR\u003C/em>:\u003C/p>\n\u003Cul>\n\u003Cli>Either use Bluetooth Audio Receiver from the Microsoft Store to connect you phone via Bluetooth,\u003C/li>\n\u003Cli>Or use an audio cable to connect the phone to the \"line-in\" on your PC.\u003C/li>\n\u003C/ul>\n\u003Ch2>Bluetooth (recommended)\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>: A PC with integrated Bluetooth or a Bluetooth dongle.\u003C/p>\n\u003Cp>I recommend this approach more than the wired one because it is way less effort, you don't have to deal with a USB or lightning to audio dongle and in my opinion it is more reliable.\u003C/p>\n\u003Cp>Pair your phone with your PC as normal, by opening the Bluetooth settings on your phone and on the PC and wait for the devices to show up. When you successfully paired the phone once you will not have to do this again. Now you need an app that will tell the phone that it can use the PC as a wireless speaker. The only app I found that will do this is the \u003Ca href=\"https://www.microsoft.com/de-de/p/bluetooth-audio-receiver/9n9wclwdqs5j\" rel=\"nofollow noopener noreferrer\">Bluetooth Audio Receiver\u003C/a> app from the Windows Store. Install and and open it. You should see your phone on the list of Bluetooth devices on the app. Select it and click on the \u003Ccode>Open Connection\u003C/code> button. It might take a moment but after it connected, you should hear all sounds from your phone on your PC.\u003C/p>\n\u003Ch2>Wired\u003C/h2>\n\u003Cp>\u003Cstrong>Requirements\u003C/strong>:\u003C/p>\n\u003Cul>\n\u003Cli>Male-to-Male audio cable (3.5mm audio jack).\u003C/li>\n\u003Cli>A line-in port on your PC (usually blue audio jack on the back)\u003C/li>\n\u003Cli>USB-C to audio jack adapter (Optional)\u003C/li>\n\u003Cli>Lighting to audio jack adapter (Optional)\u003C/li>\n\u003C/ul>\n\u003Cp>This approach works if your PC doesn't support Bluetooth, or if the Bluetooth connection drops for some reason. Connect the audio cable to the blue line-in jack on the back of the computer. Then, connect the phone to the other end of the audio cable. If your phone does not have an audio jack, use the adapter on the USB-C or Lightning port. If your PC detects that you connected a new line-in device, it might open the audio settings automatically. If not, right-click on the volume icon on the taskbar next to the clock and select \u003Ccode>Sounds\u003C/code>. Navigate to the \u003Ccode>Input\u003C/code> tab and double click on the Line-In entry (the one with a cable icon). Navigate to the Monitor tab and select the check box for \"Use this device as a playback source\". This will tell windows it should play all sounds received through this input directly to the speakers. Usually this is used to monitor microphones but it works for this use case too. You should now hear any sound from your phone through your PC headphones or speakers. Make sure you turn this checkbox off when you disconnect your phone. Otherwise you might hear a crackle or other sounds when the loose cable gets touched.\u003C/p>\n\u003Cp>\u003Cem>Photo by Lisa Fotios from Pexels\u003C/em>\u003C/p>","blog/2022-02-phone-audio-to-pc","be57f2df-d58f-4b79-8a51-e20d482f46cf","How to Listen to Phone Audio on PC",["Date","2022-02-12T00:00:00.000Z"],"Learn how to connect your phone audio to your PC over wire or Bluetooth.","/assets/2022-02-phone-audio-to-pc.jpg",[53,54,19,55,56],"how-to","audio","bluetooth","software","\u003Cp>Did you ever want to listen to your phone audio on your PC? I do it all the time to listen to podcasts on my PC without paying for a podcast app that syncs the episodes over the cloud. In this short article I will show you two easy ways to do this with a windows PC.\u003C/p>",[54,55,53,56,19],[],{"tag":19}],"uses":{"params":["slug"]}}]} diff --git a/tags/winter.html b/tags/winter.html new file mode 100644 index 00000000..c6469f3b --- /dev/null +++ b/tags/winter.html @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️winter - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: winter

+ +

+ 12/9/2022 +
 title image +

The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/winter/__data.json b/tags/winter/__data.json new file mode 100644 index 00000000..88f753f1 --- /dev/null +++ b/tags/winter/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":38},[2],{"html":3,"slug":4,"date":5,"content_tags":6,"photos":9,"raw_data":12,"abstract":3,"tags":31,"links":-1,"published":33,"type":34,"cover_image":11,"description":15,"folder":35,"comments":36,"latestComment":37},"\u003Cp>The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once.\u003C/p>","mf2/2022/12/ota4mt",["Date","2022-12-09T10:50:00.000Z"],[7,8],"Photo","winter",[10],{"url":11},"https://media.tiim.ch/47537749-f79a-4603-92a8-42c71d6b96ec.jpg",{"items":13,"rels":29,"relurls":30},[14],{"id":15,"value":15,"html":15,"type":16,"properties":18,"shape":15,"coords":15,"children":28},"",[17],"h-entry",{"category":19,"content":20,"mp-photo-alt":22,"post-status":24,"published":26},[7,8],[21],"The first snow of this winter! Even though its way too cold for my taste, I hope we get some more snow again this year. And maybe even white holidays for once. ",[23],"A snowy field near Basel, Switzerland. ",[25],"published",[27],"2022-12-09T11:50:00+0100",[],{},{},[32,8],"photo",true,"note","mf2",[],"2023-09-02T19:26:59Z",{"tag":8}],"uses":{"params":["slug"]}}]} diff --git a/tags/woocommerce.html b/tags/woocommerce.html new file mode 100644 index 00000000..87b37197 --- /dev/null +++ b/tags/woocommerce.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️woocommerce - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: woocommerce

+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/woocommerce/__data.json b/tags/woocommerce/__data.json new file mode 100644 index 00000000..7a7ab0d6 --- /dev/null +++ b/tags/woocommerce/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":26},[2],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":16,"abstract":19,"tags":20,"type":21,"cover_image":-1,"description":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\" rel=\"nofollow noopener noreferrer\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>","projects/woocommerce-order-explorer","8a52a1ad-a5cf-4191-947c-db6862746816","WooCommerce Order Explorer",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14,15],"svelte","wordpress","woocommerce","dev",[17,18],"\u003Cp>\u003Ca href=\"https://tiim.ch/woocommerce-order-explorer-js/\" rel=\"nofollow noopener noreferrer\">Open Order Explorer\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/woo-commerce-order-explorer-js-vmu3h\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>",[15,12,14,13],"article","","projects",[],"2023-09-02T19:26:59Z",{"tag":14}],"uses":{"params":["slug"]}}]} diff --git a/tags/wordpress.html b/tags/wordpress.html new file mode 100644 index 00000000..09d0574d --- /dev/null +++ b/tags/wordpress.html @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️wordpress - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: wordpress

+ +

Swim Club Birsfelden Website

+ 3/5/2022 +
+

The website of the "ScBirs" swim club. This website serves as the center of all information distribution for the swimclub.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/wordpress/__data.json b/tags/wordpress/__data.json new file mode 100644 index 00000000..f597920c --- /dev/null +++ b/tags/wordpress/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":39},[2,23],{"html":3,"slug":4,"uuid":5,"title":6,"date":7,"modified":8,"section":9,"published":10,"content_tags":11,"links":15,"abstract":3,"tags":17,"type":18,"cover_image":-1,"description":19,"folder":20,"comments":21,"latestComment":22},"\u003Cp>The website of the \"ScBirs\" swim club. This website serves as the center of all information distribution for the swimclub.\u003C/p>","projects/scbirs-website","32787e66-2678-435f-be39-c27265bc9a6f","Swim Club Birsfelden Website",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],"Projects",true,[12,13,14],"wordpress","php","dev",[16],"\u003Cp>\u003Ca href=\"https://scbirs.ch\" rel=\"nofollow noopener noreferrer\">Website\u003C/a>\u003C/p>",[14,13,12],"article","","projects",[],"2023-09-02T19:26:59Z",{"html":24,"slug":25,"uuid":26,"title":27,"date":28,"modified":29,"section":9,"published":10,"content_tags":30,"links":33,"abstract":36,"tags":37,"type":18,"cover_image":-1,"description":19,"folder":20,"comments":38,"latestComment":22},"\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\" rel=\"nofollow noopener noreferrer\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>","projects/woocommerce-order-explorer","8a52a1ad-a5cf-4191-947c-db6862746816","WooCommerce Order Explorer",["Date","2022-03-05T20:21:10.000Z"],["Date","2022-06-08T20:15:48.000Z"],[31,12,32,14],"svelte","woocommerce",[34,35],"\u003Cp>\u003Ca href=\"https://tiim.ch/woocommerce-order-explorer-js/\" rel=\"nofollow noopener noreferrer\">Open Order Explorer\u003C/a>\u003C/p>","\u003Cp>\u003Ca href=\"https://codesandbox.io/s/woo-commerce-order-explorer-js-vmu3h\" rel=\"nofollow noopener noreferrer\">Source Code\u003C/a>\u003C/p>","\u003Cp>A small client side only web app to browse all open orders on your \u003Ca href=\"https://woocommerce.com/\">WooCommerce\u003C/a> store. All data is stored in the browser.\u003C/p>",[14,31,32,12],[],{"tag":12}],"uses":{"params":["slug"]}}]} diff --git a/tags/wsl.html b/tags/wsl.html new file mode 100644 index 00000000..5c692c61 --- /dev/null +++ b/tags/wsl.html @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + Tim Bachmann + + + + + + + + + + + + + 🏷️wsl - Tim Bachmann + + +
+ + +
+
+ +

🏷️ Tag: wsl

+ +

Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN

+ 3/15/2023 +
Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN +

I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.

+
+
+ +

How to set up an SSH Server on Windows with WSL

+ 3/2/2022 +
How to set up an SSH Server on Windows with WSL +

It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.

+
+
+ +
+ +
+
+

Built with SvelteKit and hosted on GitHub Pages.

+

View this website on GitHub!

+

Other pages

+
+ +
+ + + + +
+ + diff --git a/tags/wsl/__data.json b/tags/wsl/__data.json new file mode 100644 index 00000000..1d1594ab --- /dev/null +++ b/tags/wsl/__data.json @@ -0,0 +1 @@ +{"type":"data","nodes":[{"type":"data","data":[{"footer":1},{"html":2,"slug":3,"uuid":4,"created":5,"date":6,"published":7,"abstract":8,"tags":9,"links":-1,"type":10,"cover_image":-1,"description":11,"folder":12},"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>\n\u003Cp>View this website on \u003Ca href=\"https://github.com/Tiim/Tiim.github.io\" rel=\"nofollow noopener noreferrer\">GitHub\u003C/a>!\u003C/p>\n\u003Ch2>Other pages\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https://tiim.ch/pages/links\" rel=\"nofollow noopener noreferrer\">Links and Blogroll\u003C/a>\u003C/li>\n\u003C/ul>","footer","e556fd14-3acd-4a7b-9b31-929fdd6d2b7a",["Date","2023-07-31T20:16:19.000Z"],["Date","2023-07-31T20:16:19.000Z"],true,"\u003Cp>Built with SvelteKit and hosted on GitHub Pages.\u003C/p>",[],"article","","metadata"],"uses":{}},{"type":"data","data":[{"posts":1,"details":63},[2,26],{"html":3,"slug":4,"uuid":5,"date":6,"created":7,"aliases":8,"title":10,"published":11,"modified":9,"description":12,"cover_image":13,"cover_image_txt":14,"content_tags":15,"abstract":20,"tags":21,"links":-1,"type":22,"folder":23,"comments":24,"latestComment":25},"\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>\n\u003Cp>I will show you how I fixed this problem for me and explain what the commands I used do. This post is mostly for my reference, but I hope it helps anyone else as well.\u003C/p>\n\u003Ch2>Finding out what your problem is\u003C/h2>\n\u003Cp>Let's check first if we have internet access inside WSL2. For this run the ping command with an IP address as a destination:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping 8.8.8.8\n\u003C/code>\u003C/pre>\n\u003Cp>If you get something like this as the output, your internet connection is fine, and it's just the DNS nameserver addresses that are misconfigured, you can jump forward to Solution 2.\u003C/p>\n\u003Cpre>\u003Ccode>PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n64 bytes from 8.8.8.8: icmp_seq=1 ttl=108 time=4.53 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=108 time=3.94 ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=108 time=3.97 ms\n64 bytes from 8.8.8.8: icmp_seq=4 ttl=108 time=3.78 ms\n64 bytes from 8.8.8.8: icmp_seq=5 ttl=108 time=3.77 ms\n64 bytes from 8.8.8.8: icmp_seq=6 ttl=108 time=3.76 ms\n64 bytes from 8.8.8.8: icmp_seq=7 ttl=108 time=3.81 ms\n\u003C/code>\u003C/pre>\n\u003Cp>If you don't get any responses from the ping (i.e. no more output after the \u003Ccode>PING 8.8.8.8 (8.8.8.8) ...\u003C/code> line), you need to configure the WSL and the VPN network adapter metric. Go to Solution 1.\u003C/p>\n\u003Cp>To check if the DNS is working, we can again use the ping command, this time with a domain name:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">ping google.com\n\u003C/code>\u003C/pre>\n\u003Cp>If you get responses, the DNS and your internet connection are working! If not go to Section 2.\u003C/p>\n\u003Ch2>Solution 1: Fixing the Network Adapter\u003C/h2>\n\u003Cp>Run the following two commands in PowerShell as administrator:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match \"Cisco AnyConnect\"} | Set-NetIPInterface -InterfaceMetric 4000\n\nGet-NetIPInterface -InterfaceAlias \"vEthernet (WSL)\" | Set-NetIPInterface -InterfaceMetric 1\n\u003C/code>\u003C/pre>\n\u003Cp>Let me explain what those two commands do. Both follow the same pattern of listing all network adapters, selecting a specific adapter from the list and setting its \"metric\".\u003C/p>\n\u003Cp>You can imagine an adapter as a virtual network port on the back of your pc or laptop. But instead of sending packets through the wire, the driver for a specific port can do whatever it wants with those packets, in the case of a VPN, the packets get encrypted and forwarded to the internet via another adapter.\u003C/p>\n\u003Cp>The \u003Ca href=\"https://learn.microsoft.com/en-us/windows-server/networking/technologies/network-subsystem/net-sub-interface-metric\" rel=\"nofollow noopener noreferrer\">InterfaceMetric\u003C/a> is a value associated with each adapter that determines the order of those adapters. This allows windows to determine which adapter to prefer over another one.\u003C/p>\n\u003Cp>By setting the interface metric of the Cisco adapter to 4000 and the metric of the WSL adapter to one, we allow the traffic from WSL to flow through the Cisco adapter. To be honest I do not exactly understand why this works but it does.\u003C/p>\n\u003Ch2>Solution 2: Registering the VPN DNS inside of WSL\u003C/h2>\n\u003Cp>Setting the DNS servers is, unfortunately, a little bit more involved than just running two commands, we need to edit the files \u003Ccode>/etc/wsl.conf\u003C/code> and \u003Ccode>/etc/resolv.conf\u003C/code>, and restart wsl in between. Let's get to it:\u003C/p>\n\u003Cp>Edit the file \u003Ccode>/etc/wsl.conf\u003C/code> inside of WSL2 using a text editor. I suggest doing this through the terminal since you need root permissions to do that:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/wsl.conf\n# feel free to use another editor such as vim or emacs\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely this file does not exist yet, otherwise, I suggest you create a backup of the original file to preserve the settings.\u003C/p>\n\u003Cp>Add the following config settings into the file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-ini\">[network]\ngenerateResolvConf = false\n\u003C/code>\u003C/pre>\n\u003Cp>This will instruct WSL to not override the \u003Ccode>/etc/resolv.conf\u003C/code> file on every start-up. Save the file and restart WSL with the following command so that the changed config takes effect:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open a PowerShell terminal and list all network adapters with the following command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ipconfig /all\n\u003C/code>\u003C/pre>\n\u003Cp>Find the Cisco AnyConnect adapter and copy the IP addresses in the DNS-Server field. We will need those IPs in the next step.\u003C/p>\n\u003Cp>Start WSL again and edit the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo nano /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>Most likely there is already something in this file, you can discard it. When undoing the changes, WSL will automatically regenerate this file anyway, so you don't need to back it up.\u003C/p>\n\u003Cp>Delete all the contents and enter the IP addresses you noted down in the last step in the following format:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-resolv\">nameserver xxx.xxx.xxx.xxx\n\u003C/code>\u003C/pre>\n\u003Cp>Put each address on a new line, preceded by the string \u003Ccode>nameserver\u003C/code>.\nSave the file and restart WSL with the same command as above:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">wsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>Now open up WSL for the last time and set the immutable flag for the \u003Ccode>/etc/resolv.conf\u003C/code> file:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">chattr +i /etc/resolv.conf\n\u003C/code>\u003C/pre>\n\u003Cp>And for the last time shut down WSL. Your DNS should now be working fine!\u003C/p>\n\u003Ch2>Undoing those changes\u003C/h2>\n\u003Cp>I did not have a need to undo the steps for \u003Ccode>Solution 1\u003C/code>, and I'm pretty sure the metric resets after each system reboot anyway so there is not much to do.\u003C/p>\n\u003Cp>To get DNS working again when not connected to the VPN run the following commands:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo chattr -i /etc/resolv.conf\nsudo rm /etc/resolv.conf\nsudo rm /etc/wsl.conf\nwsl.exe --shutdown\n\u003C/code>\u003C/pre>\n\u003Cp>This will first clear the immutable flag off \u003Ccode>/etc/resolv.conf\u003C/code>, and delete it. Next, it will delete \u003Ccode>/etc/wsl.conf\u003C/code> if you have a backup of a previous \u003Ccode>wsl.conf\u003C/code> file, you can replace it with that. At last, we shutdown WSL again for the changes to take effect.\u003C/p>\n\u003Cp>Unfortunately, this is quite a procedure to get a VPN to work with WSL2, but I'm hopeful that this will soon not be necessairy anymore.\u003C/p>","blog/2023-03-21-anyconnect-wsl2","c67bc4dc-4c96-41b1-afb5-15a99457dedf",["Date","2023-03-15T15:22:04.511Z"],["Date","2023-03-15T15:22:04.511Z"],[9],null,"Fix Network Connectivity in WSL2 with Cisco AnyConnect VPN",true,"I ran into problems using Cisco AnyConnect VPN from inside of WSL2. I'm sharing my solution as a step-by-step guide for my reference and to help anyone with the same problem.","https://media.tiim.ch/66ca4290-3fc0-450f-977b-f00f888e4af3.webp","Stable Diffusion - Anything V3.0 - 1boy, hacker, in front of computer, back of head visible, vintage neon color scheme, terminal, big monitor",[16,17,18,19],"wsl","vpn","networking","dns","\u003Cp>I recently ran into the problem that when the Cisco AnyConnect VPN is connected, the network connectivity inside of WSL2 stops working. I found a bunch of solutions online for it: most just focus on the fact that the VPN DNS settings are not applied inside WSL2 and therefore no domain names can be resolved. I additionally had the issue that the WSL2 network interface somehow gets disconnected when the VPN starts.\u003C/p>",[19,18,17,16],"article","blog",[],"2023-09-02T19:26:59Z",{"html":27,"slug":28,"uuid":29,"title":30,"published":11,"date":31,"description":32,"cover_image":33,"content_tags":34,"abstract":39,"tags":40,"links":-1,"type":22,"folder":23,"comments":43,"latestComment":25},"\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\" rel=\"nofollow noopener noreferrer\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\" rel=\"nofollow noopener noreferrer\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\" rel=\"nofollow noopener noreferrer\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\" rel=\"nofollow noopener noreferrer\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>\n\u003Ch2>Installing the OpenSSH Server\u003C/h2>\n\u003Cp>Windows has been shipping with an OpenSSH client and server for a long time. They are not installed by default but can be activated either in the settings as described \u003Ca href=\"https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse\" rel=\"nofollow noopener noreferrer\">in the official docs\u003C/a> or with the following PowerShell commands.\u003C/p>\n\u003Cp>\u003Cstrong>You will need to start PowerShell as Administrator\u003C/strong>\u003C/p>\n\u003Cp>First, install the OpenSSH client and server.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0\nAdd-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0\n\u003C/code>\u003C/pre>\n\u003Cp>Enable the SSH service and make sure the firewall rule is configured:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\"># Enable the service\nStart-Service sshd\nSet-Service -Name sshd -StartupType 'Automatic'\n\n# Confirm the firewall rule is configured. It should be created automatically by setup. Run the following to verify\nif (!(Get-NetFirewallRule -Name \"OpenSSH-Server-In-TCP\" -ErrorAction SilentlyContinue | Select-Object Name, Enabled)) {\n Write-Output \"Firewall Rule 'OpenSSH-Server-In-TCP' does not exist, creating it...\"\n New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22\n} else {\n Write-Output \"Firewall rule 'OpenSSH-Server-In-TCP' has been created and exists.\"\n}\n\u003C/code>\u003C/pre>\n\u003Cp>Congratulations, you have installed the SSH server on your Windows machine. And all without manually setting up a background service or modifying config files.\u003C/p>\n\u003Ch2>Setting WSL as Default Shell\u003C/h2>\n\u003Cp>To directly boot into WSL when connecting, we need to change the default shell from \u003Ccode>cmd.exe\u003C/code> or \u003Ccode>PowerShell.exe\u003C/code> to \u003Ccode>bash.exe\u003C/code>, which in turn runs the default WSL distribution. This can be done with the PowerShell command:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-ItemProperty -Path \"HKLM:\\SOFTWARE\\OpenSSH\" -Name DefaultShell -Value \"C:\\WINDOWS\\System32\\bash.exe\" -PropertyType String -Force\n\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: even though the shell is running on the Linux side, the SSH server is still on windows. This means you have to use to windows username to log in, and the SCP command copies files relative to the user directory on windows.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (non-Admin User)\u003C/h2>\n\u003Cp>\u003Cstrong>Note\u003C/strong>: If the user account has Admin permissions, read the next chapter, otherwise continue reading.\u003C/p>\n\u003Cp>Create the folder \u003Ccode>.ssh\u003C/code> in the users home directory on windows: (e.g. \u003Ccode>C:\\Users\\<username>\\.ssh\u003C/code>). Run the following commands in PowerShell (not as administrator).\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path ~\\.ssh -ItmeType \"directory\"\nNew-Item -Path ~\\.ssh\\authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>The file \u003Ccode>.ssh\\autzorized_keys\u003C/code> will contain a list of all public keys that shall be allowed to connect to the SSH server.\u003C/p>\n\u003Cp>Copy the contents of your public key file (usually stored in \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>) to the \u003Ccode>authorized_keys\u003C/code> file. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Enable Key-based Authentication (Admin User)\u003C/h2>\n\u003Cp>If the user is in the Administrators group, it is not possible to have the \u003Ccode>authorized_keys\u003C/code> file in the user directory for security purposes.\nInstead, it needs to be located on the following path \u003Ccode>%ProgramData%\\ssh\\administrators_authorized_keys\u003C/code>. A second requirement is that it is only accessible to Administrator users, to prevent a normal user from gaining admin permissions.\u003C/p>\n\u003Cp>To create the file start PowerShell as administrator and run the following command.\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">New-Item -Path $env:programdata\\ssh\\administrators_authorized_keys\n\u003C/code>\u003C/pre>\n\u003Cp>This will create the file with the correct permissions. Now open the file and paste your public key into it. The public key should be located at \u003Ccode>~/.ssh/id_rsa.pub\u003C/code>. If a key is already present, paste your key on a new line.\u003C/p>\n\u003Ch2>Verifying everything works\u003C/h2>\n\u003Cp>Verify that you can SSH into your machine by running the following inside WSL:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">IP=$(cat /etc/resolv.conf | grep nameserver | cut -d \" \" -f2) # get the windows host ip address\nssh <user>@$IP\n\u003C/code>\u003C/pre>\n\u003Cp>Or from PowerShell and cmd:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-PowerShell\">ssh <user>@localhost\n\u003C/code>\u003C/pre>\n\u003Ch2>Drawbacks\u003C/h2>\n\u003Cp>There are some drawbacks to this approach. If you rely on some programs or scripts to work over SSH, this might not be the method for you. Most scripts expect a unix machine on the other end, or if they expect a windows machine they will most likely not be configured to deal with WSL.\u003C/p>\n\u003Cp>If you however just want to connect to your pc to copy some files or change some settings this approach is perfectly fine.\u003C/p>","blog/2022-03-ssh-windows-wsl","03b5a86c-5f4d-4086-9f5f-e1e46b4bcf58","How to set up an SSH Server on Windows with WSL",["Date","2022-03-02T00:00:00.000Z"],"It can be very helpful to be able to connect to your laptop or desktop PC from anywhere using SSH. I will show you how to easily set this up on Windows with WSL.","/assets/2022-03-ssh-windows-wsl.png",[35,36,37,38],"SSH","WSL","Windows","dev","\u003Cp>There \u003Ca href=\"https://gist.github.com/dentechy/de2be62b55cfd234681921d5a8b6be11\">are\u003C/a> \u003Ca href=\"https://medium.com/@thinkbynumbers/automatically-start-wsl-ssh-and-various-services-on-windows-845dfda89690\">many\u003C/a> \u003Ca href=\"https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc\">guides\u003C/a> on the \u003Ca href=\"https://superuser.com/questions/1112007/how-to-run-ubuntu-service-on-windows-at-startup\">internet\u003C/a> showing how to set up an SSH server \u003Cstrong>inside\u003C/strong> WSL. This is currently not that easy and in my experience, it is not really stable. An alternative to this is to run the SSH server outside of WSL on the windows side and set its default shell to the WSL shell (or any other shell for that matter).\u003C/p>",[38,41,42,16],"ssh","windows",[44,52,58],{"id":45,"type":46,"replyTo":47,"timestamp":48,"page":28,"url":49,"content":50,"name":51},"20f2c526-7466-4c21-83ac-51750f278328","comment","f7c1891b-e97b-4030-863e-19344ed84d32","2022-09-20T14:59:17Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#20f2c526-7466-4c21-83ac-51750f278328","Hi Tim, no problem. I had this error in \"Event Viewer > Applications and Services Logs > OpenSSH > Admin\" and figure it out that sshd seems to search the Administrators groups to operate, literal name and not properly localized by region.\n\nerroid:2 user:SYSTEM details:\"sshd: error: unable to resolve group administrators\"\n\nMaybe is not all the non-english windows with this problem, but I have it but after created the group works like a charm.","",{"id":47,"type":46,"replyTo":53,"timestamp":54,"page":28,"url":55,"content":56,"name":57},"5de01bc5-b7c7-4522-9dfe-c67f103d4c03","2022-09-20T12:58:29Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#f7c1891b-e97b-4030-863e-19344ed84d32","Hi FinderX\n\nThanks for the heads up. I don't remember having to create a new user group, even though my system language is German. Maybe I just forgot about that though.","Tim",{"id":53,"type":46,"replyTo":51,"timestamp":59,"page":28,"url":60,"content":61,"name":62},"2022-09-20T07:50:46Z","https://tiim.ch/blog/2022-03-ssh-windows-wsl#5de01bc5-b7c7-4522-9dfe-c67f103d4c03","Hi!\nI add some roundabouts about admin-users, if your windows ssh server system language is NOT english, you must create 'Administrators' group (without quotes) in your language equivalent of \"Users and Local Groups > Groups\", if your server is a DC (Domain Controller) create it in your language equivalent of \"Active Directories Users and Computers\".\n\nCreate the user group with name Administrators, description whatever, ex. \"Dummy group for sshd to work correctly.\", and in Members add your language equivalent of the user Administrator.\n\nThis is optional but I suggest you change these settings in \"%programdata%\\ssh\\sshd_config\" after you successfully copy your public key to the ssh server :\n\nStrictModes yes\nPubkeyAuthentication yes\nPasswordAuthentication no\n\nYou can see the log activity in your language equivalent of \"Applications and Services Logs > OpenSSH > Admin or Operational\"\n\nBest Regards.","FinderX",{"tag":16}],"uses":{"params":["slug"]}}]}