Warning: Undefined array key "HTTP_ACCEPT_LANGUAGE" in /www/htdocs/v156395/marcus-obst.de/plugins/Meta.php on line 243
Marcus Obst 🍋 Webdesign und -anwendungen mit Vitaminen HTML PHP JS CSS SVG. - Digitale Produke und Lösungen aus einer (oder mehr Händen). https://marcus-obst.de/ Wed, 22 Jan 2025 17:02:27 +0000 Wed, 22 Jan 2025 17:02:27 +0000 Pico Moving from Tumblr to Self-Hosted Wordpress Pt. II <h1>Moving from Tumblr to Self-Hosted Wordpress Pt. II</h1> <p>Since the move from Tumblr and my new blogging aspirations meant to deal more with WordPress, I have a few more things to share.</p> <p>My goal was</p> <ol> <li>Serve public and private posts in the same stream</li> <li>Hide Private posts in the listing, but if you hit the private post directly (via rss, notification), show a message to register/login.</li> <li>New registrations have to be approved</li> <li>Spam Registration/Login Protection</li> <li>Subscribers should be notified by mail when a new post happened</li> <li>Free extensions</li> </ol> <h2>1. Membership Extensions</h2> <p>I first thought I should use Wordpress <a href="https://jetpack.com/support/newsletter/">Jetpack Subscriber</a> because it came with the whole &quot;content protection, registration, and login as well as notification&quot; jazz. But I figured, for a semi private blog I didn't want to ditch tumblr just to be depended on Wordpress servers (same thing, I know).</p> <p>What speaks for the Jetpack solution, it just works out of the box and looks good. No unstyled forms and elements, no short codes, everything is wired up correctly.</p> <p>Other membership plugins offer similar things, but often stick out like a sore thumb if you don't get the CSS right. Or the templating is weird.</p> <p>Anyway, I ditched Jetpack because the subscribers have to go through Wordpress.com, a hard to sell and ahgain, Wordpress is going to change their service if they want and suddenly it's paid only.</p> <p>For a work project, I used <a href="https://wordpress.org/plugins/ultimate-member/">Ultimate Member</a> before, but in order to learn something new, I tried out <a href="https://wordpress.org/plugins/simple-membership/">Simple Membership</a>.</p> <p>Both offer protected content, login and registration, user management and some paid content stuff (usually in their PRO offering).</p> <p>Simple Membership did what I wanted in the free version, but I ended up debugging their code, just to find out the registration form can't be overwritten in a child-theme. Odd inconsistency, but it looked like they were improving this old code.</p> <p>Anyway I wasn't happy and went back to Ultimate Member, since their forms look decent by default and are easier to modify (form builder is included).</p> <p>The whole admin UI is also a bit nicer and easier to follow.</p> <h3>How to protect several posts without going into each post?</h3> <p>The trick is to create a WordPress category “Private” for example and protect this category. This is the way it works in both plugins. This way, I was able to add posts to the “private” category in bulk!</p> <h2>2. Notifications about new posts</h2> <p><a href="https://wordpress.org/plugins/notification-master/">Notification Master</a> is offering that. It works fine for email.</p> <h2>3. Filter out protected posts from listing</h2> <p><a href="https://wordpress.org/plugins/advanced-query-loop/">Advanced Query Loop</a> extends the WP Query Loop component and offers more ways to filter content.</p> <p><img src="assets/2024-12-09.moving-from-tumblr-to-self-hosted-wordpress-2/2024-12-09.moving-from-tumblr-to-self-hosted-wordpress-2-20241209092048731.png" alt="2024-12-09.moving-from-tumblr-to-self-hosted-wordpress-2-20241209092048731.png" /></p> <h2>Conclusion</h2> <p>I have to say, I was a WP hater in the past, but despite some annoying things (WP drama 2024, the constant upselling of extensions etc.) I really like it to quickly set up a complex webapp/website that you can't come up with, with Kirby or Statamic or Craft CMS (who like to take some of WP's share). If the out-of-the-box stuff is good enough, go for it. Damn, even the native template builder is pretty good, once I understood it. So, not the old PHP blog anymore, yet all there is needed is a simple shared PHP 8.x.</p> Mon, 09 Dec 2024 00:00:00 +0000 https://marcus-obst.de/blog/moving-from-tumblr-to-self-hosted-wordpress-2 https://marcus-obst.de/blog/moving-from-tumblr-to-self-hosted-wordpress-2 Moving from Tumblr to Self-Hosted Wordpress <h1>Moving from Tumblr to Self-Hosted Wordpress</h1> <p>I'm a former Wordpress hater, but it's undeniable how fast one can setup a working website. And especially a blog (even if nobody uses that anymore).</p> <p>First, I love Tumblr. I love the tech. It's still the easiest and most flexible network to post. Photo-galleries, media, text. It just works, and it's not a walled garden, but a public blog/website if you want to.</p> <p>What I don't care about is the community and the ads are annoying, but more so the fact that I don't want my private travel blog being on there. My travel blog that only two people cared read, maybe just one that managed to bookmark the site.</p> <p>Anyway, here is what I learned about how to importing a tumblr blog worth of 12 years of traveling the US and Europe.</p> <h2>1. Wordpress importer</h2> <p>If you google for a tumblr to Wordpress importer you'll get a ton of results talking about this plugin: <a href="https://wordpress.org/plugins/wordpress-importer/">https://wordpress.org/plugins/wordpress-importer/</a></p> <p>It's tried and tested and does what it claims to do. Transfers your Tumblr Blog to Wordpress.</p> <p>Where it gets tricky is the following:</p> <h2>2. (no title) Post Titles</h2> <p>Tumblr didn't use post titles as a specific field. It's just a formatting, so the list of posts in WP backend looks non-descriptive with all the <strong>(no title)</strong> posts.</p> <p>The solution to that was a script in <code>functions.php</code> that writes the title.</p> <pre><code class="language-php">/** * Automatically set empty post titles using first 13 words of content */ function auto_set_empty_titles($post_ID) { // Get the post $post = get_post($post_ID); // Only proceed if the title is empty and we have content if (empty(trim($post-&gt;post_title)) &amp;&amp; !empty($post-&gt;post_content)) { // Strip any HTML tags and shortcodes $content = wp_strip_all_tags($post-&gt;post_content); $content = strip_shortcodes($content); // Get first 13 words $words = explode(' ', $content); $first_13_words = array_slice($words, 0, 13); $new_title = implode(' ', $first_13_words); // Add ellipsis if content was truncated if (count($words) &gt; 13) { $new_title .= '...'; } // Update the post title wp_update_post(array( 'ID' =&gt; $post_ID, 'post_title' =&gt; $new_title )); } } // Hook the function to run when posts are saved //add_action('save_post', 'auto_set_empty_titles', 10, 1); // Optional: Run this once to update all existing posts with empty titles function update_all_empty_titles() { $args = array( 'post_type' =&gt; 'post', 'posts_per_page' =&gt; -1, 'post_status' =&gt; 'any', 'title' =&gt; '' ); $posts = get_posts($args); foreach ($posts as $post) { auto_set_empty_titles($post-&gt;ID); } } //update_all_empty_titles();</code></pre> <p>Uncomment <code>update_all_empty_titles();</code> and then reload your page. It took a while and the script pooped (timed) out, but another reload just took care of the rest of the empty title fields.</p> <p>The script was written by AI, don't @ me if you set your wordpress on fire!</p> <p>Btw. <a href="https://chriscoyier.net/2024/03/03/11148/">Chris Coyier made a case for not using post titles</a>.</p> <h2>3. Galleries</h2> <p>Over the 12 years, tumblr changed a lot and so did the tech behind. I think from 2019 or so on, they fully embrace the block editor. Not like Gutenberg Blocks, but some sort of WYSIWYG. Before you could write markdown, html or some WYSIWYG html with special tags. It was bad.</p> <p>Then they invented some weird JSON markup called the <a href="https://www.tumblr.com/docs/npf">Tumblr Neue Post Format</a> - no clue why so complicated, but for some time it wasn't possible to edit a post in the browser, made with the app and vice versa.</p> <p>What I want to say is, the imported Markup is weird. Earlier post just write in a plain WP <code>[gallery]</code> short code.</p> <p>Later galleries are imported as HTML (which is better).</p> <p>To deal with the <code>[gallery]</code> short code I found a script here <a href="https://nickgreen.info/filter-classic-wordpress-gallery-shortcode-attributes/">Filter classic WordPress gallery shortcode attributes</a>. This makes the gallery a little bit more presentable. It still looks ugly, but at least it's not just the cropped thumbnails.</p> <p>Other gallery pics are just loaded fully one after another. You can go in, convert the classic editor field to blocks, delete a few blocks and convert the images to a WP or Jetpack gallery. But I would do that only to posts that I edit.</p> <pre><code class="language-php">/* filter gallery output*/ add_filter( 'shortcode_atts_gallery', 'my_shortcode_atts_gallery', 10, 4 ); function my_shortcode_atts_gallery( $out, $pairs, $atts, $shortcode ) { if ( ! isset( $atts['columns'] ) ) { $out['columns'] = 1; } if ( ! isset( $atts['size'] ) ) { $out['size'] = 'full'; } if ( ! isset( $atts['link'] ) ) { $out['link'] = 'none'; } return $out; }</code></pre> <p>Alternatively, here's a script to convert plain <code>[gallery]</code> shortcodes to tiled Jetpack galleries.</p> <p>Check the box under <strong>Settings &gt; Media &gt; Tiled Galleries</strong> and if they appear too small (500px by default) set the content width of the template. See <a href="https://jetpack.com/support/jetpack-blocks/tiled-galleries/">https://jetpack.com/support/jetpack-blocks/tiled-galleries/</a></p> <pre><code class="language-php">// twentytwentyfive template uses a content width of 645px if ( ! isset( $content_width ) ) { $content_width = 645; } /* filter gallery output*/ add_filter( 'shortcode_atts_gallery', 'my_shortcode_atts_gallery', 10, 4 ); function my_shortcode_atts_gallery( $out, $pairs, $atts, $shortcode ) { if ( ! isset( $atts['columns'] ) ) { $out['columns'] = 3; } if ( ! isset( $atts['size'] ) ) { $out['size'] = 'full'; } if ( ! isset( $atts['link'] ) ) { $out['link'] = 'file'; } if ( ! isset( $atts['type'] ) ) { $out['type'] = 'rectangular'; } return $out; }</code></pre> <p>Another option to deal with plain <code>[gallery]</code> tags would be to do a YOLO style search &amp; replace directly in the database.</p> <pre><code class="language-sql">UPDATE wp_posts SET post_content = REPLACE(post_content, '[gallery]', '[gallery link="file" size="large" type="rectangular"]') where post_content LIKE '%[gallery]%' and post_status = 'publish'</code></pre> <p>Don't @ me if you messed up something. </p> <p>Also, this way you <strong>can't</strong> convert the gallery in the <strong>Classic block</strong> to a <strong>Gutenberg gallery block</strong>.</p> <h2>4. Hot linking images to Tumblr</h2> <p>Some images were uploaded to my WordPress, others (newer ones?) were hot-linked to the tumblr cdn.</p> <p>To download all the externally linked images I tried out two plugins:</p> <p><a href="https://wordpress.org/plugins/download-external-images-in-posts/">Download External Images In Posts – WordPress plugin | WordPress.org</a> this one downloads all images in a subfolder of the media library and you can't see them there.</p> <p><a href="https://wordpress.org/plugins/auto-upload-images/">Auto Upload Images – WordPress plugin | WordPress.org</a> and this one does the same, just that images are supposed to be available in the media library. I'm not sure if it worked, since the plugin wasn't tested on newer WordPress versions.</p> <h2>Conclusion</h2> <p>It's not as straightforward as the plugin is trying to sell itself. But's it's possible to transfer the Tumblr blog to a self-hosted WordPress instance.</p> <p>Main reason was to have some sort of subscriber(my mom)-based travel blog, so my data is not public to anyone. That level of control war not possible in Tumblr.</p> Fri, 06 Dec 2024 00:00:00 +0000 https://marcus-obst.de/blog/moving-from-tumblr-to-wordpress https://marcus-obst.de/blog/moving-from-tumblr-to-wordpress Moving from Flickr to Self-Hosted Piwigo <h1>Moving from Flickr to Self-Hosted Piwigo</h1> <p>See my personal motivation for moving away from <a href="https://www.flickr.com/photos/fieldmuzick/">Flickr</a> to self-hosted <a href="https://photos.marcus-obst.de/">Piwigo</a> at the end of the page. Here are a few points to outline what to expect from Piwigo:</p> <ul> <li>It's album-based; there's no real loose photo stream as we know it from Flickr or Google Photos.</li> <li>It's mature software that's battle tested but rough around the edges with a lot of baggage (coding style, jQuery).</li> <li>There is a neat iOS and Android client</li> <li>It needs some adjustments, and you better know a thing or two about how to fix things yourself.</li> </ul> <h2>Install Piwigo</h2> <p>That should be straightforward. It's old-timey FTP deployment, either by one php script or by uploading the package. It's described on their <a href="https://piwigo.org/get-started">get-started</a> page.</p> <h2>Setup</h2> <p>Once the thing is running, and you uploaded a few pictures to check it out you might think, “How can I get my photos from Flickr into Piwigo, and how do I make it look at least a bit prettier?”</p> <p>The answer is a bunch of plugins!</p> <h3>1. Activate the LocalFiles Editor Plugin.</h3> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012170905578.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012170905578.png" /></p> <p>This provides access to a few files you might want to change, mainly the config file.</p> <p>Here I set the following value to suppress PHP notices or warnings in case a plugin doesn't like the latest PHP version (8.2 in my case)</p> <pre><code class="language-php">&lt;?php $conf['show_php_errors'] = E_ALL &amp; ~E_NOTICE &amp; ~E_DEPRECATED &amp; ~E_WARNING;</code></pre> <p>Other config settings can be discovered in the default config file. <img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012171238041.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012171238041.png" /></p> <h3>2. Install the flickr2piwigo plugin.</h3> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012172338315.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012172338315.png" /></p> <ul> <li>Activate it</li> <li>put in your Flickr API key and API secret</li> <li>start importing either album by album manually or all images (albums will be created)</li> </ul> <p>For this plugin to work, I needed the PHP error suppression because one lib in the plugin is not compatible with PHP 8.2 (see the <a href="https://github.com/Piwigo/Flickr2Piwigo/issues/73#issue-2582648510">GitHub issue</a>)</p> <p>The import was relatively straightforward.</p> <p>Now you should have a working self-hosted photo gallery that you can browse in the frontend. And there you see: it looks dated af and what are these URLs <code>/index.php?/category/album-42</code>?</p> <h3>3. Search engine Friendly URLs</h3> <p>Add the following to your config file as described before:</p> <pre><code class="language-php">&lt;?php $conf['show_php_errors'] = E_ALL &amp; ~E_NOTICE &amp; ~E_DEPRECATED &amp; ~E_WARNING; $conf['question_mark_in_urls'] = false; $conf['php_extension_in_urls'] = false; $conf['category_url_style'] = 'id-name'; $conf['picture_url_style'] = 'file'; ?&gt;</code></pre> <p>And also place a <code>.htaccess</code> file into the root and add this:</p> <pre><code>AcceptPathInfo On Options +MultiViews</code></pre> <p>Make sure you have no index.html or index.htm in your root, only index.php!</p> <p>Check to see if your public gallery works!</p> <h3>4. Change the theme to Bootstrap Darkroom.</h3> <p><a href="https://github.com/Piwigo/piwigo-bootstrap-darkroom">Bootstrap Darkroom</a> can be installed and setup from the backend. It's the most modern looking theme. Here are some screenshots:</p> <p>1. Install it via the “Add a new theme” tab: 2. Activate and set it as the default, then configure the theme.</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012175713410.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012175713410.png" /></p> <p>3. Select a color theme you like.</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012175943296.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012175943296.png" /></p> <p>4. Play with the settings; here are mine as a reference:</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012180223033.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012180223033.png" /> <img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012180503848.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012180503848.png" /> <img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012180535131.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012180535131.png" /></p> <p>That should make the page look like this:</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012180408853.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012180408853.png" /><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012180433449.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012180433449.png" /></p> <p>For the secondary zoom into pictures via PhotoSwipe to work, set your image sizes as follows:</p> <p>Go to Photo sizes and “show details”</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012180849597.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012180849597.png" /></p> <p>Change the &quot;XXL - huge&quot; to something larger than the default value.</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012181005076.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012181005076.png" /></p> <p>Then you can do the following:</p> <p>Set full-screen (lightbox mode or whatever you want to call it).</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012181051809.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012181051809.png" /></p> <p>Click on the image or the magnifying glass</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012181219452.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012181219452.png" /></p> <p>And the image scales up to the XXL-huge size, as you know it from Flickr:</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012181254207.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012181254207.png" /></p> <p>5. Justified image gallery.</p> <p>That's what they call the “bricklayer” effect, you know from Flickr or Google Photos. It looks much better than the even cards of images in the Bootstrap Darkroom theme.</p> <p>For that, you need another plugin called <a href="https://piwigo.org/ext/index.php?eid=771">gdThumb</a>, easy to install via the backend.</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012181524510.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012181524510.png" /></p> <p>Play with the settings, here are mine:</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012181745543.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012181745543.png" />to make it look like this:</p> <p><img src="assets/2024-10-12.moving-from-flickr-to-piwigo/2024-10-12.moving-from-flickr-to-piwigo-20241012181803434.png" alt="2024-10-12.moving-from-flickr-to-piwigo-20241012181803434.png" /></p> <h2>Conclusion</h2> <p>These were the basics to make it work for me, or better yet, to make it look more modern. Most themes are from 2015 or older. It works, but it doesn't look so good.</p> <p>For the rest of the setup, you are on your own.</p> <h2>Why did I leave Flickr and moved to Piwigo?</h2> <p>I've been on Flickr since 2007, when it was a cool photo-sharing community. Then it got sold to Yahoo, which offered unlimited storage, which turned it into a low-quality image dump, at least partially. Yahoo sold it to Verizon who didn't care too much about it (along with tumblr)? Anyway, it got saved by SmugMug, who put an end to the 1 TB of storage, and that forced me to buy a subscription. 35 USD for a year was okay. From then on, they raised the price and did only little things to keep the platform running and evolving. The <a href="https://en.wikipedia.org/wiki/Flickr">Wikipedia article about Flickr</a> explains it all.</p> <p>Personally, I didn't expect much. I documented places (outsider art environments and things like that), and the free exposure was a nice add-on, but primarily I liked to work with the API and embedded my pictures on my website this way. At some point, I set all my images to a Creative Commons license for people to use (I ended up on Atlas Obscura and in some Kentucky tourism magazine)</p> <p>This year, they raised the price again. Just 1 EUR, but I looked at it, looked at alternatives, and pulled the plug. No Flickr PRO for me.</p> <p>What they offer is mainly unlimited hosting of files — I already pay for web hosting, and now for Google Photos — so I can't justify Flickr as a photo backup. I don't need any of the perks they offer (some small discounts on photo books that I can't order anyway). I also don't want to subscribe to Adobe. The community is dead or not interested in my stuff, so I don't miss anything in that regard. SmugMug made a big deal out of improving the statistics, but I couldn't care less whether my photo has been seen 33 times or 44 times. One of the arguments for using Flickr was that it doesn't use your data like Google Photos. Ok, true. And? Nobody cares about this anymore. We accept all cookies everywhere because maintaining privacy is a chore, nobody wants to deal with it. They use my photo of a dog to show my dog food ads? I don't care. I'm more annoyed by Cookie-Banner than the ads I'm blocking anyway.</p> <p>Again, I liked their API. Flickr was very developer-friendly in the beginning, but all the apps people built 15 years ago are dead, and nothing new happens in that space.</p> <p>Piwigo was always on my radar, and since my web host offered a one-click install, I tried it. First, I was unsure because, even though the Piwigo websites look pretty nice now, the software is dated and reminds me of some old Joomla backend. But I dug in and managed to import my Flickr photos without issues, polished the instance with some of their plugins, and even <a href="https://github.com/Piwigo/Flickr2Piwigo/pull/72">contributed a bug fix</a>. It looks like the community has resisted complete rewrites in some framework that needs updates and maintenance all the time. Instead, you have old-school SQL queries instead of an ORM and no compose.json. </p> <p>And since my engagement on Flickr was low, I don't expect more on my personal Piwigo instance. So I save money, had a little work to figure it all out and the Piwigo API is alright, I can work with that as well.</p> <p>Another contender was <a href="https://lycheeorg.github.io/">Lychee</a>, also a self-hosted photo gallery based on PHP. I tried it, it's pretty smooth and nice, but it couldn't convince me completely, mainly because it wasn't easy to get my Flickr photos imported and add new images in the future (I think they turned off the ability to read whole directories with images). It also is based on Laravel. While Laravel is fantastic software, as far as I know, I'm not a fan of Laravel. So it was easier to dismiss, beside the missing requirements.</p> <p>Update 2024-12-06: I sold a license for a photo that allowed be to buy another 2 years of flickr pro, but not without letting the flickr support know that I'm leaving, which resulted in a 30% discount. So ~50 EUR a year, I'm ok with. Also it turns down, selecting 1000 photos to stay on (for the free plan) is as hard as selecting which photos to upload. That's a dilemma.</p> <p>But since I'm local first, I can distribute photos to flickr and piwigo in almost the same step. So I guess I try </p> Sat, 12 Oct 2024 00:00:00 +0000 https://marcus-obst.de/blog/moving-from-flickr-to-piwigo https://marcus-obst.de/blog/moving-from-flickr-to-piwigo AtroPIM v1.9.21 API breaking changes for related assets <h1>AtroPIM v1.9.21 API breaking changes for related assets</h1> <p>Some new breaking API changes in <a href="../wiki/PIM%20-%20AtroPIM.md">my favorite Open-Source PIM AtroPIM</a>, that weren't documented (unless I overlooked it).</p> <p>[TOC]</p> <p>According to the <a href="https://github.com/atrocore/atropim/releases/tag/1.9.21">changelog</a> (with a little more context <a href="https://help.atrocore.com/release-notes/atropim/1.9/#atropim-1921">here</a>) they changed something related to how product assets are requested.</p> <p>To get all assets related to a product, I called <code>/Product/:entityId/assets</code> in the past, which resulted in a collection like this:</p> <pre><code class="language-json">{ "total": 6, "list": [ { "id": "63a41a9be351041b6", "name": "Einbauhinweis_1380_1390.png", "deleted": false, "icon": null, "url": "/upload/files/7k4dp/Einbauhinweis_1380_1390.png", "height": 1470, "width": 1500, "colorSpace": "SRGB", "colorDepth": "8", "orientation": "Landscape", "isActive": true, "tags": [], "description": "", "createdAt": "2022-12-22 08:51:39", "modifiedAt": "2022-12-28 15:44:19", "private": false, "type": [ "Description Image" ], "size": 55, "sortOrder": null, "title": "Einbau- und Betriebshinweise", "fileId": "63ac0d0cde8995351", "fileName": "Einbauhinweis_1380_1390.png", "filePathsData": { "download": "upload/files/7k4dp/Einbauhinweis_1380_1390.png", "thumbs": { "small": "upload/thumbnails//16801/small/Einbauhinweis_1380_1390.png", "medium": "upload/thumbnails//16801/medium/Einbauhinweis_1380_1390.png", "large": "upload/thumbnails//16801/large/Einbauhinweis_1380_1390.png" } } } ] }</code></pre> <p>This is gone with 1.9.21+ instead, one can use <code>/Product/:entityId/productAssets</code> which returns an entirely different collection, that includes prefixed <code>product_*</code> and <code>asset_*</code> data.</p> <pre><code class="language-json">{ "total": 6, "list": [ //... { "id": "314", "deleted": false, "isMainImage": false, "sorting": 10, "scope": "Global", "tags": null, "createdAt": null, "modifiedAt": null, "fileId": "63ac0d0cde8995351", "fileName": "Einbauhinweis_1380_1390.png", "filePathsData": { //.. }, "icon": null, "product_name": null, "product_sku": "TEST1380PR01", "product_isActive": true, "product_amount": 0, "product_price": null, "product_productStatus": "draft", "product_longDescription": null, "product_sortOrder": 110, "product_hierarchySortOrder": null, "product_data": null, "product_createdAt": "2022-12-29 08:44:14", "product_modifiedAt": "2023-08-01 08:41:33", "product_sorting": null, "product_materialcode": "TEST1380", "product_additionalInformation": null, "product_shortDescription": null, "product_packingnotice": null, "product_priceCurrency": null, "asset_preview": null, "asset_icon": null, "asset_url": "/upload/files/7k4dp/Einbauhinweis_1380_1390.png", "asset_afterSaveMessage": null, "asset_height": 1470, "asset_width": 1500, "asset_colorSpace": "SRGB", "asset_colorDepth": "8", "asset_orientation": "Landscape", "asset_isActive": true, "asset_tags": null, "asset_name": "Einbauhinweis_1380_1390.png", "asset_description": null, "asset_createdAt": "2022-12-22 08:51:39", "asset_modifiedAt": "2022-12-28 15:44:19", "asset_private": false, "asset_type": [ "5fbe2b489bf7238b3" ], "asset_size": 55, "asset_sortOrder": null, "asset_hierarchySortOrder": null, "asset_title": null, "asset_lang": "de", "asset_isRoot": true, "asset_hasChildren": true, "asset_hierarchyRoute": [], "asset_inheritedFields": null, "productId": "63ad535edbc9ad1", "productName": "Folienringe", "assetId": "63a41a9be351041b6", "assetName": "Einbauhinweis_1380_1390.png", "channelId": null, "channelName": null, "createdById": null, "createdByName": null, "modifiedById": null, "modifiedByName": null, "product_brandId": null, "product_brandName": null, "product_taxId": null, "product_taxName": null, "product_packagingId": "640f0db75bdfb", "product_productSerieId": null, "product_productSerieName": null, "product_catalogId": "5ff47082272d74ecd", "product_catalogName": "Lieferprogramm (Katalog)", "product_createdById": "1", "product_createdByName": "Admin", "product_modifiedById": "1", "product_modifiedByName": "Admin", "product_ownerUserId": "1", "product_ownerUserName": "Admin", "product_assignedUserId": "1", "product_assignedUserName": "Admin", "product_mainImageId": null, "product_mainImageName": null, "product_profileId": "61fc4387732815b9f", "product_profileName": "PR01", "asset_libraryId": null, "asset_libraryName": null, "asset_createdById": "62bdb456d7cb", "asset_createdByName": "", "asset_modifiedById": "1", "asset_modifiedByName": "Admin", "asset_fileId": "63ac0d0cde8995351", "asset_fileName": "Einbauhinweis_1380_1390.png", "asset_filePathsData": { //... } } ] }</code></pre> <p>This is a breaking change for me, since the endpoint and the return object are fundamentally different, and it wasn't documented anywhere.</p> <h2>The Quick Fix</h2> <p>I was able to replace <code>/Product/:entityId/assets</code> with <code>/Asset?where[0][type]=linkedWith&amp;where[0][attribute]=productAssets_product&amp;where[0][value][0]={{entityId}}</code> which returns the expected output.</p> <pre><code class="language-json">{ "total": 6, "list": [ { "id": "63a41a9be351041b6", "name": "Einbauhinweis_1380_1390.png", "deleted": false, "icon": null, "url": "/upload/files/7k4dp/Einbauhinweis_1380_1390.png", "height": 1470, "width": 1500, "colorSpace": "SRGB", "colorDepth": "8", "orientation": "Landscape", "isActive": true, "tags": [], "description": "", "createdAt": "2022-12-22 08:51:39", "modifiedAt": "2022-12-28 15:44:19", "private": false, "type": [ "Description Image" ], "size": 55, "sortOrder": null, "title": "Einbau- und Betriebshinweise", "fileId": "63ac0d0cde8995351", "fileName": "Einbauhinweis_1380_1390.png", "filePathsData": { //.. } } ] }</code></pre> <p>The new <code>/Product/:entityId/productAssets</code> call (and the <code>/ProductAsset</code> entity) ties assets closer to the PIM and helps to have basic product information included, which might reduce requests under certain circumstances. I'm sure the Atro devs have a reason for this decision, to me, it seems quite redundant, at least how I use it.</p> <p>At the moment (2023-08-19), there is also a bug in the localization of <code>asset_*</code> fields <a href="https://github.com/atrocore/atropim/issues/524">https://github.com/atrocore/atropim/issues/524</a></p> Sat, 19 Aug 2023 00:00:00 +0000 https://marcus-obst.de/blog/AtroPIM-v1_9_21-API-breaking-changes https://marcus-obst.de/blog/AtroPIM-v1_9_21-API-breaking-changes My Email Game (Update) <p>I love email. It was one of the first things I used when I got online back in 2000, and it's still here. It just works.</p> <p>But a lot in email gets me headaches and that is always mail client specific, like:</p> <ul> <li>Interrupted threads when someone started a new subject to discuss a previous topic, and the inability of mail clients to add a mail to an existing thread.</li> <li>HTML email is a pain in the rear and even worse is the garbage that every HTML mail editor adds (I'm looking at you Microsoft Office/Outlook however you call yourself these days)</li> <li>Email clients that can't read my mind what's important and what not (classify emails).</li> <li>No native markdown to HTML converter (given if you use basic Markdown formatting in text-only emails, the client might render it bold or italic, but no headlines or anything).</li> <li>Adding notes to email (without sending mails to myself, but I guess that's the way to do)</li> </ul> <p>So far, my journey included the following email clients:</p> <ul> <li><strong>Outlook Express</strong> did its thin, butt once I tried <strong>Eudora</strong>, there was no going back unless I had to.</li> <li>With the demise of Eudora I tried <strong>BAT!</strong> mail, an incredible powerful and ugly mail client and <strong>Pegasus Mail</strong> - I can't remember it wasn't good.</li> <li><strong>Mozilla Thunderbird</strong> entered the scene and everything was possible. Extensions for every use case and active development.</li> <li>I used Thunderbird exclusively and over time I missed some features like the two row mail listing, that was never planned to change and other features were just hacky (thread view)</li> <li><strong>Postbox</strong> claimed to solve my problems and it was based on Thunderbird, so it was compatible with Thunderbird formats and it looked fresh. The first time I spend money on a mail client.</li> <li>Sometimes I opened <strong>Gmail</strong> but never got used to it, until Googles Experiment <strong>Mailbox</strong> came on the scene. That was fantastic. Especially the classification of mails helped to deal with large amout of mail and promo mails.</li> <li>Webinterfaces never felt right, Mailbox was shut down, Gmail sucks, Postbox was still there, but facing a difficult decision, because Thunderbird was about to be rebuild and only supporting their new webextensions.</li> <li>Postbox closed their interface to use extensions, so a whole world of functionality I got used to disappear. &quot;Send later&quot;, &quot;Snooze Email&quot; and the &quot;Markdown Here&quot; extension. Big bummer, but I kept using Postbox because I paid for it and it was still nice.</li> <li>Today Postbox allows extensions again, but there are no compatible Thunderbird extensions anymore and no one cares about making them compatible. Not even Postbox. Markdown-Here got an update by the community, and it barely works. New features appear in Postbox sporadically, but nothing earth-shattering. Instead, they try to sell themes. Who cares?</li> <li>I gave <strong>eM Client</strong> and <strong>Mailbird</strong> a shot. Mailbird is some garbage subscription Electron app. No thanks. eM Client impressed me mildly but suddenly I miss some Postbox features (RSS, complex filter, inline reply) I got used to and it wants another 50 to 70 EUR for a license.</li> <li>Update 2023-07-29: I'm using my Office 365 address with <strong>Outlook Web</strong>. I have to say, it's working well so far. I even setup the <strong>native Microsoft Mail</strong> (I just wanted to connect my calendar) and it's not too bad either for just email.</li> <li>For Bulk Email I started to use <strong>Betterbird</strong>, a Thunderbird fork with some fixes and some things that make it better, according to the developer. I can't tell what it is, but it's definitely not the UI/layout of the app.</li> <li>Thunderbird announced to overhaul their ancient mail client and with <a href="https://blog.thunderbird.net/2023/07/our-fastest-most-beautiful-release-ever-thunderbird-115-supernova-is-here/">Thunderbird Supernova</a> they delivered the first version of their modernized email client. It has a multiline message list, and that sold it to me. I'm in the process of finally leaving Postbox and enjoying the integration of LanguageTool, Mailmerge, Markdown-Here.</li> </ul> <h3>eM Client as alternative?</h3> <p><a href="https://de.emclient.com/">eM Client</a> has a <strong>working calender</strong> and address book. Postbox had Lightning in the past, but they got rid of it. I used the Sunlight calendar for a while but it got bought by Microsoft and then was shut down. The Microsoft calendar is not bad, but it's unreliable with WebDav calendar on Nextcloud.</p> <p>eM Client impressed me with their thoughtful features, like showing a history of mails from the sender in an active mail, build in calendar and addressbook, notes and even chat (uses some chat protocols that I don't use, so there is that).</p> <p>Now, the <strong>HTML editor</strong> in eM Client is just a basic textfield. They didn't even bother to use a monotype font. So it's hard to create html mail in there or signatures. It has wysiwyg tools though, but I don't trust them. </p> <p>A thing I always ignored in email is encryption. I think the fact that no normal person is able to understand and setup <strong>PGP</strong> keys left this feature unused by 98% of mail users.</p> <p>I've never received an encrypted email.</p> <p>eM Client seems to make the setup easy, but I believe if nobody is using, why bother.</p> <p>The filter capabilities in eM Client seem to be inferior to Postbox/Thunderbird. No time based filter (delete messages older than x days).</p> <p>Another downside and a <strong>show stopper</strong> is the absence of an update of the <strong>&quot;unread Mails&quot; count on IMAP subfolders</strong>, according to this forum post: <a href="https://forum.emclient.com/t/no-updates-or-notifications-from-subfolders/36803/55">No updates or notifications from subfolders? - #55 by curious - Mail - eM Client</a>. That's bad and I guess won't make me using it.</p> <h2>Other alternatives?</h2> <p>I can think of two: <a href="https://hey.com/"><strong>Hey Mail</strong></a> from the creators of Basecamp. It's a paid browser based client that seems to become very popular and I guess you have to use their hey.com mail address.</p> <p>And there is <a href="https://plummail.co/"><strong>Plum Mail</strong></a> which looks like a web client with some nice helpers, to pin mails and make notes and keep a conversation together. Subscription based and in beta right now. So I'll pass again.</p> <h2>Email tools I use or wish that existed</h2> <h4>1. Classification of mails</h4> <p>Every mail client has a trainable spam filter. You would think that this technology could be implemented to train the algo to classify other topics? Nope. Nowhere to find in offline mail clients. That's only offered by cloud based mail systems like Gmail, Hey etc.</p> <p>But there is <strong><a href="http://getpopfile.org/">Popfile</a></strong> - it's an ancient piece of software, last updated 2015 that sits between your mail client and the mailserver and analyzes mails and sort it into folders. It comes with a webinterface to train your mail and just works fine. Only thing to criticize is it works with only one IMAP connection. That's a bummer, but for my main mailbox it kind of works well to differenciate between notification mail, spam and important stuff. </p> <h4>2. Merging loose mail threads</h4> <p>There was a Thunderbird extension that aimed to do that, but as far as I know, mail threads are identified by a message-id and I think it's up to the mail client to be lax and group by subject line or whatever. Oh well.</p> <h4>3. Markdown</h4> <p>It seems like such a niche feature that probably is easy to implement (especially for Postbox, since there is an existing extension they just need to fix), but so far there is only one MacOS client that offers Markdown out of the box, so I won't hold my breath that eM Client or any other client will see that feature ever. (There is Markdown-Here Revival which works nicely)</p> <h4>4. Procmail filter editor in client</h4> <p>If I setup a local email filter I would love to see that reflected optionally as procmail filter on the server. But I guess that's too special and works only if your provider gives you access or an interface to that (as <a href="https://all-inkl.com/wichtig/anleitungen/verwaltung/vertrag/spam-und-virenfilter/einrichten-des-spam-und-virenfilters_150.html">all-inkl.com</a> does) combined with a Bayes filter like Popfile - that would be perfect.</p> <h4>5. Exchange support</h4> <p>I have to use at least one Exchange mail account and I don't want to know too much about this weird Microsoft product, just so much, it is not easy to setup if you don't use Outlook or some other MS software, especially if an ancient version of Exchange is used. I have to run a little proxy called <a href="http://davmail.sourceforge.net/"><strong>DavMail</strong></a> that sits between Postbox or eM Client and deals with Exchange server. Works alright, but y tho? </p> <p>Update: with Office 365 it's better integrated, until they changed something with their IMAP/SMTP connections. Either I use Outlook Web or in Thunderbid I now have to use this <a href="https://www.beonex.com/owl/"><strong>Owl</strong></a> extension because Thunderbird or MS messes up Unicode characters and umlauts when sending via SMTP. (Ä turns to ?). Y tho?</p> <p><img src="assets/2023-07-29.my-email-game/91999240b5fecba28f44f2526cc816f5_MD5.jpg" alt="blog/91999240b5fecba28f44f2526cc816f5\_MD5.jpg" /></p> <h4>Encrypted chat via email</h4> <p>A well setup mail client is one thing, but what if you could use email as chat app? There is the <a href="https://delta.chat/en/"><strong>Delta Chat</strong></a> messenger app, that connects to your email account, and offers you a nice chat interface as you know it from Signal or WhatsApp or Skype. Messages are simple emails that your chat partner can read in their email client. And if they use Delta Chat as well, it's even better. And if both partys youse Delta Chat, then they can use the Autoencrypt feature so messages are only readable by the client and nowhere else.</p> <p>This is a really privacy friendly way to use a proven, robust and decentral infrastructure to communicate. </p> <p>As far as I understand, this is not as secure messaging as Signal, because email metadata is not encrypted and therefore you leave traces. But I'm no expert here, I just think Delta Chat is a good thing to have.</p> <h2>tl;tr or conclusion</h2> <p>There isn't one. I guess I <del>keep working around the pain I have with Postbox and won't inflict pain in buying eM Client at this point and work around other issues there</del> switch back to Thunderbird, now that they have multiline message list (or cards as they call it).</p> <p>As Chris Coyier states:</p> <blockquote> <p>In fact, you come to love [email], because of how effective of a communication method it is: it’s public, it’s async, it can hold files, it can be of any length, it’s threaded.</p> <p>Tooling isn’t needed to “fix” email. Email clients these days, for the most part, are interchangeably good.</p> <p><a href="https://email-is-good.com/2020/10/26/not-a-big-deal/">Not a Big Deal &#8211; Email is Good</a></p> </blockquote> Sat, 29 Jul 2023 00:00:00 +0000 https://marcus-obst.de/blog/my-email-game https://marcus-obst.de/blog/my-email-game Web Components and Internationalization <h1>Web Components and Internationalization</h1> <p>While exploring Web Components for a current project, came to a halt when faced with the question of language versions of a component.</p> <p>As advertised, Web Components are portable pieces that can be inserted into any project. Nevertheless, Web Components can be linked directly to a project, but why would you use them instead of using Vue, React or any server-side template language? These are questions I still have to answer myself.</p> <p>But back to my initial problem: What about the internationalization of Web Components?</p> <p>I found a blog post<sup id="fnref1:1"><a href="#fn:1" class="footnote-ref">1</a></sup> that goes much deeper, and from there I cobbled together my low effort version below.</p> <p>The gist is, if you have a bunch of components with strings that can be translated, you handle them in the bundle and load external language files when the component starts up.</p> <p>If you have a simple, single component, then you can add the language strings inside the component definition, query the document language <code>&lt;html lang="en"&gt;</code> or the <code>navigator.language</code> and be done with it. This is still not the most portable solution, but it works for that use case.</p> <p>More complex implementation of that idea here: <a href="https://github.com/RolandDreger/web-components/blob/master/note-list/src/note-list.js">web-components/note-list.js at master · RolandDreger/web-components · GitHub</a></p> <pre><code class="language-js">const FALLBACK_LANG = "en"; class MyBookmark extends HTMLElement { constructor() { super(); this._saved = false; } getDocumentLang() { return ( document.body.getAttribute("xml:lang") || document.body.getAttribute("lang") || document.documentElement.getAttribute("xml:lang") || document.documentElement.getAttribute("lang") || FALLBACK_LANG ); } render() { const lang = this.getDocumentLang(); const state = this._saved ? 'bookmarks.button.bookmarked' : 'bookmarks.button.bookmark'; const messages = { 'de': { 'bookmarks.button.bookmark': 'merken', 'bookmarks.button.bookmarked': 'gemerkt' }, 'en': { 'bookmarks.button.bookmark': 'save', 'bookmarks.button.bookmarked': 'saved' }, }; const html = `${messages[lang][state]}`; } }</code></pre> <div class="footnotes"> <hr /> <ol> <li id="fn:1"> <p><a href="https://dev.to/btopro/reviewing-i18n-solutions-a10">https://dev.to/btopro/reviewing-i18n-solutions-a10</a>&#160;<a href="#fnref1:1" rev="footnote" class="footnote-backref">&#8617;</a></p> </li> </ol> </div> Wed, 31 May 2023 00:00:00 +0000 https://marcus-obst.de/blog/webcomponents-i18n-internationalization https://marcus-obst.de/blog/webcomponents-i18n-internationalization Using Web Components and HTMX <h1>Using Web Components and HTMX</h1> <p class="lead">A little excursion in using Web Components with and in HTMX. The results will... confuse you (or me).</p> <p>I was wondering if Web Components are rendered automatically when they appear in an HTMX response, or if I need to &quot;activate&quot; them somehow. Yes, they are rendered when they are added to the DOM, which is great. Simply send them, and they will take care of themselves.</p> <p>In the following Codepen demo, I reused my <a href="/blog/use-bootstrap-5x-tabs-with-htmx">HTMX/Bootstrap tabs demo</a> and return two custom HTML elements. First <a href="https://github.com/paulirish/lite-youtube-embed/">Paul Irish's lite-youtube-embed</a> and second a <a href="https://codepen.io/localhorst/pen/dygrEmZ">custom element that renders a list of my Github Gists</a>.</p> <pre><code class="language-html">&lt;lite-youtube videoid="RgA81-HvXVE" playlabel="Play"&gt;&lt;/lite-youtube&gt;</code></pre> <pre><code class="language-html">&lt;mo-gist username="marcus-at-localhost"&gt;&lt;/mo-gist&gt;</code></pre> <iframe class="full mb-5" height="500" style="width: 100%;" scrolling="no" title="HTMX Bootstrap Tabs (Extension) with Web Components" src="https://codepen.io/localhorst/embed/qBJvvjy?default-tab=html%2Cresult&amp;editable=true&amp;theme-id=light" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> See the Pen <a href="https://codepen.io/localhorst/pen/qBJvvjy"> HTMX Bootstrap Tabs (Extension) with Web Components</a> by Marcus at Localhost (<a href="https://codepen.io/localhorst">@localhorst </a>) on <a href="https://codepen.io">CodePen</a>. </iframe> <p>I guess the next step is to <a href="#update-2023-06-01">figure out when not to use Web Components</a>, what's up with the shadow DOM, and whether it's all worth it. (see the update below)</p> <p>When I consider the previous usage, the lite YouTube embed is a perfect fit for Web Components. A single element that improves HTML (rather than using <code>&lt;iframe&gt;</code>).</p> <p>On the other hand, my list of the most recent GitHub Gists is doing too much and is possibly not flexible enough. In this case, I treat the Web Component more like a template engine.</p> <p>Should Web Components be used only for portable bits of HTML that can be used in another context? Can they be linked to my project without?</p> <p>I'm still undecided. </p> <ul> <li><a href="https://levelup.gitconnected.com/are-web-components-dead-12e404e0f4b0">Are Web Components Dead?. Recently, I published an article with a… by Marius Bongarts</a></li> <li><a href="https://css-tricks.com/making-web-components-for-different-contexts/">Making Web Components for Different Contexts | CSS-Tricks</a></li> <li><a href="https://www.hjorthhansen.dev/you-might-not-need-shadow-dom/">You might not need shadow DOM — Hjorthhansen</a></li> <li><a href="https://itnext.io/everything-you-always-wanted-to-know-about-web-components-but-were-afraid-to-ask-d553a504a328">Everything You Always Wanted To Know About Web Components (But Were Afraid To Ask) | by Danny Moerkerke</a></li> <li><a href="https://github.com/web-padawan/awesome-web-components">web-padawan/awesome-web-components: A curated list of awesome Web Components resources.</a> </li> </ul> <h3>Update 2023-06-01</h3> <p>I was building a Web Component to reduce noise in the rendered page.</p> <p>Noise in terms of having this:</p> <pre><code class="language-html">&lt;mo-bookmark id="12345678" saved="true"&gt;</code></pre> <p>compared to this:</p> <pre><code class="language-html">&lt;button id="btn_bookmark_item_463556218" class="btn btn-light p-0 border-0 d-inline-flex align-items-center justify-content-center" hx-delete="/bookmark/61f7e4a8b3c66bcf4"&gt; &lt;span class="fs-8 text-uppercase mx-2"&gt;saved&lt;/span&gt; &lt;span class="fs-6 bg-dark text-white d-flex align-items-center justify-content-center" style="width:1.75rem;height:1.75rem"&gt; &lt;svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"&gt;&lt;path fill="currentColor" d="M24 2H8a2 2 0 0 0-2 2v26l10-5.054L26 30V4a2 2 0 0 0-2-2Z"&gt;&lt;/path&gt;&lt;/svg&gt; &lt;span class="htmx-indicator fs-7 spinner-border spinner-border-sm" role="status" aria-hidden="true"&gt;&lt;/span&gt; &lt;/span&gt; &lt;/button&gt;</code></pre> <p>...several times on the page.</p> <p>All the styling and functionality will be implemented in JavaScript and executed on the client-side.</p> <p>However, how can you maintain the component framework-agnostic approach when using HTMX? I prefer not to include <code>hx-*</code> attributes within the component definition (although, why not?).</p> <p>Then, I discovered a method to inject custom attributes into the component, like <code>&lt;mo-bookmark hx-post="/bookmark/1234" saved="true"&gt;</code>. It seemed to work to some extent, but it led to additional complications that significantly increased the complexity. In the end, the effort to save a few kilobytes of redundant HTML wasn't worth it.</p> <p>During my exploration, I came across two intriguing blog posts by Christopher Prohm. He created htmx Web Components, enabling reusability (<a href="https://cprohm.de/blog/htmx-custom-elements/">Reusable htmx components with custom elements - Christopher Prohm</a>), and even integrated htmx with React (<a href="https://cprohm.de/blog/htmx-react/">Mixing React and htmx - Christopher Prohm</a>), which is quite a bold move, given the ongoing trend to <a href="https://htmx.org/essays/a-real-world-react-to-htmx-port/">port React to htmx</a>.</p> <p>Taking it a step further, there are React components with HTMX available (<a href="https://github.com/reggi/htmx-components">reggi/htmx-components: 🧩 Async HTMX + JSX</a>). Although I don't have experience with React, I find it amusing that HTMX aims to replace certain uses of React, only to potentially be absorbed by it.</p> <p>Nevertheless, these are just my thoughts expressed spontaneously, and they may have deviated slightly from the main topic. However, I now have a clearer understanding, and it appears that <strong>integrating Web Components within HTMX works effectively</strong>. The real question is, do you truly require Web Components?</p> Fri, 26 May 2023 00:00:00 +0000 https://marcus-obst.de/blog/htmx-web-components https://marcus-obst.de/blog/htmx-web-components How to Load Content into a Bootstrap Offcanvas Component with HTMX and Save State as a Hash in the URL <h1>How to Load Content into a Bootstrap Offcanvas Component with HTMX and Save State as a Hash in the URL</h1> <p>To open a <a href="https://getbootstrap.com/docs/5.3/components/offcanvas/">Bootstrap Offcanvas Component</a> and load some HTML fragment with HTMX, the first thing I tried was the following: </p> <ul> <li>Equip the canvas (or modal) opener with the HTMX attributes</li> <li>call it a day</li> </ul> <pre><code class="language-html">&lt;a class="btn btn-light" href="/sidebar" hx-get="/sidebar" hx-select=".bookmark-list" hx-target=".offcanvas-body" data-bs-toggle="offcanvas" data-bs-target="#offcanvas"&gt; Open Sidebar &lt;/a&gt; &lt;div id="offcanvas" class="offcanvas offcanvas-start"&gt; &lt;div class="offcanvas-body"&gt; Loading... &lt;/div&gt; &lt;/div&gt;</code></pre> <p>This approach is completely decoupled; the click event triggers both the Bootstrap behavior and the HTMX ajax call. Both are unaware of one another. </p> <p>If the canvas fails to open, the Ajax request may succeed, but the result cannot be viewed.</p> <p>Back to the drawing board!</p> <h3>Connecting the behavior of HTMX and Bootstrap JS</h3> <p>The first thought is to use HTMX and listen for the 'htmx:load' event, then call <a href="https://getbootstrap.com/docs/5.3/components/offcanvas/#methods"><code>.show()</code></a> to open the Offcanvas component. That would necessitate some UI to indicate that the loading is complete before the canvas appears.</p> <p>Or the other way around, listen to <code>show.bs.offcanvas</code> and then trigger <code>htmx.ajax()</code> to pull in the server-rendered HTML. This is better because it shows, something is happening right away.</p> <h3>Saving open state in the URL</h3> <p>That previous approach makes the button or link responsible for opening the canvas (or modal)?</p> <p>If I navigate to <code>/sidebar#offcanvas</code> I want the sidebar to be open on page load with the HTMX ajax request triggered. The problem with this approach is, the button is the single source of truth that holds the URL that's getting loaded by HTMX via <code>hx-get</code> (or <code>href</code>). </p> <p>I could go and <code>htmx.find('a[href="/sidebar"]').href</code> and then use that in the Ajax request. Or I trigger a click on the button, that triggers the behavior. But that seems weird and too tightly coupled.</p> <pre><code class="language-js">document.addEventListener('DOMContentLoaded', function () { const el = '#offcanvas'; let bookmarksOffcanvasInstance = bootstrap.Offcanvas.getOrCreateInstance(el); if(location.hash === '#offcanvas'){ // open the sidebar bookmarksOffcanvasInstance.show(); // find the button and make an // ajax call via htmx.ajax('GET', htmx.find('a[href="/sidebar"]').href, {/* target etc */}) // -- OR -- htmx.trigger('a[href="/sidebar"]', "click"); } });</code></pre> <h3>Make the Offcanvas component the single source of truth</h3> <p>There is another way I found by watching a video<sup id="fnref1:1"><a href="#fn:1" class="footnote-ref">2</a></sup> about doing something similar with AlpineJS and an open issue with HTMX<sup id="fnref1:2"><a href="#fn:2" class="footnote-ref">3</a></sup> that brought me to the following solution:</p> <h4>1. The button or link to open the sidebar is only responsible for that</h4> <pre><code class="language-html">&lt;a class="btn btn-light" href="/sidebar" data-bs-toggle="offcanvas" data-bs-target="#offcanvas"&gt; Open Sidebar &lt;/a&gt;</code></pre> <h4>2. Tie the HTMX logic to the canvas itself and trigger a HTMX custom event</h4> <pre><code class="language-html">&lt;div id="offcanvas" class="offcanvas offcanvas-start" hx-get="/sidebar" hx-select=".sidebar" hx-target=".offcanvas-body" hx-trigger="filter-event"&gt; &lt;div class="offcanvas-body"&gt; Loading... &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;script&gt; const el = document.getElementById("offcanvas"); // after the canvas was opened, trigger the hx-get with // the custom event and add the url with the state of the canvas // into the history el.addEventListener('shown.bs.offcanvas', event =&gt; { htmx.trigger(event.target, "filter-event"); history.pushState(null, null, '#' + event.target.id); }) // on hiding the sidebar, remove the hash el.addEventListener('hide.bs.offcanvas', event =&gt; { history.pushState("", document.title, window.location.pathname); }) &lt;/script&gt;</code></pre> <iframe class="full mb-5" height="600" style="width: 100%;" scrolling="no" title="Untitled" src="https://codepen.io/localhorst/embed/RwYEXNz?default-tab=html%2Cresult&amp;editable=true&amp;theme-id=light" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> See the Pen <a href="https://codepen.io/localhorst/pen/RwYEXNz"> Untitled</a> by Marcus at Localhost (<a href="https://codepen.io/localhorst">@localhorst</a>) on <a href="https://codepen.io">CodePen</a>. </iframe> <p>Now, all the information for the Ajax request is associated with the offcanvas component, and it does not matter what triggers the opening of the sidebar; everything is contained in a single location.</p> <p>My initial thought probably stems from my synchronous consideration of requests. A click alters the appearance of another element <strong>after</strong> a page reload. </p> <p>With JavaScript's asynchronous nature, this behavior is out the window.</p> <h3>Bonus: Offcanvas activated by AlpineJS that triggers HTMX custom event</h3> <p>I adopted the whole script for the AlpineJS inline JavaScript style and here it is:</p> <pre><code class="language-html">&lt;a class="btn btn-light" href="#offcanvas"&gt; Open Sidebar &lt;/a&gt; &lt;div id="offcanvas" class="offcanvas offcanvas-start" x-data x-init="()=&gt;{ const oc = new bootstrap.Offcanvas('#offcanvas'); if(location.hash === '#offcanvas') oc.show(); }" @hashchange.window="if(location.hash === '#offcanvas') { bootstrap.Offcanvas.getOrCreateInstance(location.hash).show() }" @shown-bs-offcanvas.dot=" htmx.trigger($event.target, 'filter-event'); history.pushState(null, null, '#' + $event.target.id);" @hide-bs-offcanvas.dot="history.pushState('', document.title, window.location.pathname);" hx-get="/sidebar" hx-select=".sidebar" hx-target=".offcanvas-body" hx-trigger="filter-event"&gt; &lt;div class="offcanvas-body"&gt; Loading... &lt;/div&gt; &lt;/div&gt; &lt;template url="/sidebar" delay="1500"&gt; &lt;h2&gt;Sidebar Headline only visible when /sidebar is directly requested&lt;/h2&gt; &lt;div class="sidebar"&gt; Sidebar &lt;/div&gt; &lt;/template&gt;</code></pre> <p>A working Codepen can be found under <a href="https://codepen.io/localhorst/pen/RwYvWyE">https://codepen.io/localhorst/pen/RwYvWyE</a> (log in, switch to debug mode to see that URL hash change).</p> <p>What I like about this approach is, it's very compact. Everything is in one place. No snippets here and bits there. You look at the markup of that component and that's all there is. At least for this demo. At the same time, it's ugly, hard to format and as complexity grows you'll end up putting stuff in a dedicated script block or so.</p> <p>I don't know if there is any need to add AlpineJS in the mix as a third abstraction of code.</p> <p>It's a matter of style and maintenance I guess. But now I know how to listen for events from Bootstrap components (<a href="https://alpinejs.dev/directives/on#dot">see the <code>.dot</code> modifier</a>) in AlpineJS.</p> <p>If you have correction or thoughts about it, please let me know. I don't claim, that's the way to do it. I just made it work that way.</p> <div class="footnotes"> <hr /> <ol> <li id="fn:1"> <p><a href="https://laracasts.com/series/modals-with-the-tall-stack/episodes/3">https://laracasts.com/series/modals-with-the-tall-stack/episodes/3</a>&#160;<a href="#fnref1:1" rev="footnote" class="footnote-backref">&#8617;</a></p> </li> <li id="fn:2"> <p><a href="https://github.com/bigskysoftware/htmx/issues/701">https://github.com/bigskysoftware/htmx/issues/701</a>&#160;<a href="#fnref1:2" rev="footnote" class="footnote-backref">&#8617;</a></p> </li> </ol> </div> Sat, 25 Mar 2023 00:00:00 +0000 https://marcus-obst.de/blog/htmx-bootstrap-5-offcanvas https://marcus-obst.de/blog/htmx-bootstrap-5-offcanvas Notes and Links <h1>Notes and Links</h1> <p>I'm still in search for a good title for this series, where I just post links. Maybe one day.</p> <ul> <li><a href="https://www.theverge.com/23513418/bring-back-personal-blogging">Bring back personal blogging - The Verge</a> - I'm old, and I'm all nostalgic for Web 1.0 as well. Not really, but it was a good time to surf and build for the web in 2001.</li> <li><a href="https://catgpt.wvd.io/">CatGPT</a> - chat with a cat.</li> <li><a href="https://max.engineer/cms-trap">CMS Trap - Max Chernyak</a> - yeah, been there, done that! The article warns about the dangers of overcomplicating web applications by building content management systems that hinder the development of the actual content.</li> </ul> Thu, 02 Feb 2023 00:00:00 +0000 https://marcus-obst.de/blog/notes-and-links https://marcus-obst.de/blog/notes-and-links Use Bootstrap 5.x Tabs with HTMX <h1>Use Bootstrap 5.x Tabs with HTMX</h1> <p>Yes, we all love <a href="https://htmx.com">HTMX</a> because after years of rolling our eyes at React&amp;co<sup id="fnref1:1"><a href="#fn:1" class="footnote-ref">4</a></sup>, it's our time to shine with our knowledge from the good old days of server-rendered HTML, pulled in via jQuery ajax calls.</p> <p>No need to introduce HTMX because you are probably here to find out how to <code>hx-get</code> HTML fragments into <a href="https://getbootstrap.com/docs/5.3/components/navs-tabs/#javascript-behavior">Bootstrap tabs</a>.</p> <p>The HTMX docs offer two ways to create tabs:</p> <ul> <li><a href="https://htmx.org/examples/tabs-hateoas/">Tabs (Using HATEOAS)</a> - the state of the tabs is being rendered on the server</li> <li><a href="https://htmx.org/examples/tabs-hyperscript/">Tabs (Using Hyperscript)</a> - the state is managed by esoteric Hyperscript declarations.</li> </ul> <p>Personally, I dislike both approaches and prefer to work within Bootstrap's constraints and use as many of their tools as possible. In this case for the tabbed interface, to control active state.</p> <p>First I setup a really simple example and then I introduce a HTMX-Extension to avoid code duplication.</p> <h2>1. Basic Example</h2> <p>A typical Bootstrap <code>.nav-link</code> tab and corresponding <code>.tab-pane</code> looks like this: (I'll leave off all the attribute noise)</p> <pre><code class="language-html">&lt;nav&gt; &lt;button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-home"&gt;Home&lt;/button&gt; &lt;/nav&gt; &lt;div&gt; &lt;div class="tab-pane" id="nav-home"&gt; Home Content &lt;/div&gt; &lt;/div&gt;</code></pre> <p><code>[data-bs-toggle="tab"]</code> initializes the tabs, <code>[data-bs-target="#nav-home"]</code> points to <code>.tab-pane#nav-home</code> and <code>.show.active</code> said tab on click. Bootstrap takes care of the styles that reflect the state of the tabs, adds the <code>.active</code> class to the <code>.nav-link</code> and the <code>.tab-pane</code> and changes the state if you click another tab.</p> <p>Now loading a remote html fragment into a <code>.tab-pane</code> could be done with a <a href="https://getbootstrap.com/docs/5.3/components/navs-tabs/#events">Bootstrap event</a>:</p> <pre><code class="language-js">const tabEl = document.querySelector('button[data-bs-toggle="tab"]') tabEl.addEventListener('show.bs.tab', event =&gt; { // ajax call to the server, inject html into pane... })</code></pre> <p>Instead we use HTMX. Here again the very simplified code example:</p> <pre><code class="language-html">&lt;nav&gt; &lt;button class="nav-link" hx-get="/home" hx-target="#nav-home" data-bs-toggle="tab" data-bs-target="#nav-home"&gt;Home&lt;/button&gt; &lt;/nav&gt; &lt;div&gt; &lt;div class="tab-pane" id="nav-home"&gt;&lt;/div&gt; &lt;/div&gt;</code></pre> <p>The attribute <code>[hx-get="/home"]</code> is the link to an HTML fragment, and <code>[hx-target="#nav-home"]</code> tells HTMX in which pane the fragment will be injected.</p> <p>This should all work out of the box. There might be some extra work to do, to smooth out CSS transitions. The tab will be activated by Bootstrap immediately, and stays blank until the HTML fragment was loaded!</p> <iframe class="full" height="300" style="width: 100%;" scrolling="no" title="HTMX Bootstrap Tabs (Basic)" src="https://codepen.io/localhorst/embed/RwBZoXy?default-tab=html%2Cresult" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> See the Pen <a href="https://codepen.io/localhorst/pen/RwBZoXy"> HTMX Bootstrap Tabs (Basic)</a> by Marcus at Localhost (<a href="https://codepen.io/localhorst">@localhorst</a>) on <a href="https://codepen.io">CodePen</a>. </iframe> <p>To prevent the tab from being loaded from the server on every activation, I added this little snippet, which checks if there are HTML elements from a previous call already in the <code>.tab-pane</code> and if yes, then just prevent the request. </p> <pre><code class="language-js">htmx.on("htmx:beforeRequest", function(evt) { if(evt.detail.target.hasChildNodes()){ // prevent another request evt.preventDefault() } });</code></pre> <h2>2. HTMX Extension</h2> <p>To reduce code duplication, <code>[hx-target]</code> and <code>[data-bs-target]</code> point to the same <code>.tab-pane</code> <code>[id]</code>, I just marked up the tabs with <code>[data-bs-*]</code> attributes if I'm not using <code>[hx-*]</code></p> <p>Here is a ultra-minimal example:</p> <pre><code class="language-html">&lt;script src="/bs-tabs.js"&gt;&lt;/script&gt; &lt;div hx-ext="bs-tabs"&gt; &lt;nav&gt; &lt;button class="nav-link" hx-get="/home" hx-target="#nav-home"&gt;Home&lt;/button&gt; &lt;/nav&gt; &lt;div&gt; &lt;div class="tab-pane" id="nav-home"&gt;&lt;/div&gt; &lt;/div&gt; &lt;/div&gt;</code></pre> <p>HTMX extensions are <a href="https://htmx.org/extensions/#:~:text=Typically%2C%20this%20is%20done%20in%20a%20stand%2Dalone%20javascript%20file%2C%20rather%20than%20in%20an%20inline%20script%20tag.">typically pulled in via an external script file</a>. With <code>[hx-ext="bs-tabs"]</code> I declare this part of the DOM as part of the extension.</p> <p>Then I add the <code>[hx-*]</code> attributes and the extension takes care of the rest of activating the Bootstrap behavior. The comments in the following code should help you to understand what's going on.</p> <pre><code class="language-js">(function(){ htmx.defineExtension('bs-tabs', { onEvent: function (name, evt) { // before a request is made, // check if there the content was already loaded into the tab pane if (name === "htmx:beforeRequest") { if(evt.detail.target.hasChildNodes()){ // stop request return false; } } if (name === "htmx:afterProcessNode") { let allLinks = htmx.findAll(htmx.find('[hx-ext="bs-tabs"]'), '.nav-link'); // loop through all .nav-links allLinks.forEach(triggerEl =&gt; { // if the bootstrap data attribute for the target pane is missing... if(!triggerEl.hasAttribute('data-bs-target')){ // ... add it. It's the same as the hx-target attribute. triggerEl.setAttribute('data-bs-target', triggerEl.getAttribute('hx-target')); } // add the attribute that tells initializes the bootstrap functionality triggerEl.setAttribute('data-bs-toggle', 'tab'); }); } return true; } }); })();</code></pre> <p><a href="https://codepen.io/localhorst/pen/BaPdReG">Here is a working example</a>. Btw. the HTMX folks way to set up <a href="https://htmx.org/docs/#creating-demos">demos that mock remote calls</a> is genius!</p> <iframe class="full mb-5" height="300" style="width: 100%;" scrolling="no" title="HTMX Bootstrap Tabs (Extension)" src="https://codepen.io/localhorst/embed/BaPdReG?default-tab=html%2Cresult" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> See the Pen <a href="https://codepen.io/localhorst/pen/BaPdReG"> HTMX Bootstrap Tabs (Extension)</a> by Marcus at Localhost (<a href="https://codepen.io/localhorst">@localhorst</a>) on <a href="https://codepen.io">CodePen</a>. </iframe> <p>These are very simple examples. They don't address the state of the tabs if you need to load a fragment on page load. I might add this later.</p> <p>That's it. Let me know if there is something wrong or a better way. I'm still wrapping my head around the whole JS events and states. Also I hope this inspires more posts and examples of HTMX being used with other frontend frameworks.</p> <p><a href="/blog/bootstrap-validation-with-htmx">Here is an example how to use Bootstraps form validation styles with HTMX</a>!</p> <div class="footnotes"> <hr /> <ol> <li id="fn:1"> <p>because we couldn't understand how it works, at least I didn't&#160;<a href="#fnref1:1" rev="footnote" class="footnote-backref">&#8617;</a></p> </li> </ol> </div> Sun, 15 Jan 2023 00:00:00 +0000 https://marcus-obst.de/blog/use-bootstrap-5x-tabs-with-htmx https://marcus-obst.de/blog/use-bootstrap-5x-tabs-with-htmx